From b28422f25f56ba9db5f4d6191e88500ecb3f102b Mon Sep 17 00:00:00 2001 From: "shih.zhang" Date: Fri, 29 Jul 2022 09:58:56 +0800 Subject: [PATCH 01/86] Update zh-CN.json --- .../app_flowy/assets/translations/zh-CN.json | 79 ++++++++++++++++++- 1 file changed, 76 insertions(+), 3 deletions(-) diff --git a/frontend/app_flowy/assets/translations/zh-CN.json b/frontend/app_flowy/assets/translations/zh-CN.json index 81b0ad13b1..3a61d5987a 100644 --- a/frontend/app_flowy/assets/translations/zh-CN.json +++ b/frontend/app_flowy/assets/translations/zh-CN.json @@ -93,8 +93,14 @@ "highlight": "高亮" }, "tooltip": { - "lightMode": "切换到灯光模式", - "darkMode": "切换到暗模式" + "lightMode": "切换到亮色模式", + "darkMode": "切换到暗色模式" + }, + "notifications": { + "export": { + "markdown": "导出笔记为Markdown文档", + "path": "Documents/flowy" + } }, "contactsPage": { "title": "联系人", @@ -135,11 +141,78 @@ "menu": { "appearance": "外观", "language": "语言", + "user": "用户", "open": "打开设置" }, "appearance": { "lightLabel": "日间模式", "darkLabel": "夜间模式" } + }, + "grid": { + "settings": { + "filter": "过滤器", + "sortBy": "排序", + "Properties": "属性" + }, + "field": { + "hide": "隐藏", + "insertLeft": "左侧插入", + "insertRight": "右侧插入", + "duplicate": "拷贝", + "delete": "删除", + "textFieldName": "文本", + "checkboxFieldName": "勾选框", + "dateFieldName": "日期", + "numberFieldName": "数字", + "singleSelectFieldName": "单项选择器", + "multiSelectFieldName": "多项选择器", + "urlFieldName": "链接", + "numberFormat": " 数字格式", + "dateFormat": " 日期格式", + "includeTime": " 包含时间", + "dateFormatFriendly": "月 日,年", + "dateFormatISO": "年-月-日", + "dateFormatLocal": "年/月/日", + "dateFormatUS": "年/月/日", + "timeFormat": " 时间格式", + "invalidTimeFormat": "时间格式错误", + "timeFormatTwelveHour": "12小时制", + "timeFormatTwentyFourHour": "24小时制", + "addSelectOption": "添加一个标签", + "optionTitle": "标签", + "addOption": "添加标签", + "editProperty": "编辑列属性" + }, + "row": { + "duplicate": "复制", + "delete": "删除", + "textPlaceholder": "空", + "copyProperty": "复制列" + }, + "selectOption": { + "create": "新建", + "purpleColor": "紫色", + "pinkColor": "粉色", + "lightPinkColor": "浅粉色", + "orangeColor": "橙色", + "yellowColor": "黄色", + "limeColor": "鲜绿色", + "greenColor": "绿色", + "aquaColor": "水蓝色", + "blueColor": "蓝色", + "deleteTag": "删除标签", + "colorPannelTitle": "颜色", + "pannelTitle": "选择或新建一个标签", + "searchOption": "搜索标签" + }, + "menuName": "网格" + }, + "document": { + "menuName": "文档", + "date": { + "timeHintTextInTwelveHour": "12:00 AM", + "timeHintTextInTwentyFourHour": "12:00" + } } -} +} \ No newline at end of file From 5d022eb369f8ef15991b67c13fe5f7d8d826124d Mon Sep 17 00:00:00 2001 From: yodatak Date: Thu, 18 Aug 2022 16:39:25 +0200 Subject: [PATCH 02/86] Update fr-FR transation to support grid --- .../app_flowy/assets/translations/fr-FR.json | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/frontend/app_flowy/assets/translations/fr-FR.json b/frontend/app_flowy/assets/translations/fr-FR.json index ed1d56bce8..df4b795fa5 100644 --- a/frontend/app_flowy/assets/translations/fr-FR.json +++ b/frontend/app_flowy/assets/translations/fr-FR.json @@ -141,5 +141,71 @@ "lightLabel": "Mode clair", "darkLabel": "Mode sombre" } + }, + "grid": { + "settings": { + "filter": "Filtrer", + "sortBy": "Trier par", + "Properties": "Propriétés" + }, + "field": { + "hide": "Cacher", + "insertLeft": "Insérer à gauche", + "insertRight": "Insérer à droite", + "duplicate": "Dupliquer", + "delete": "Supprimer", + "textFieldName": "Texte", + "checkboxFieldName": "Case à cocher", + "dateFieldName": "Date", + "numberFieldName": "Nombres", + "singleSelectFieldName": "Sélectionner", + "multiSelectFieldName": "Multisélection", + "urlFieldName": "URL", + "numberFormat": " Format des nombres", + "dateFormat": " Format de la date", + "includeTime": " Inclure l'heure", + "dateFormatFriendly": "Mois Jour, Année", + "dateFormatISO": "Année-Mois-Jour", + "dateFormatLocal": "Année/Mois/Jour", + "dateFormatUS": "Année/Mois/Jour", + "timeFormat": " Format du temps", + "invalidTimeFormat": "Format invalide", + "timeFormatTwelveHour": "12 heures", + "timeFormatTwentyFourHour": "24 heures", + "addSelectOption": "Ajouter une option", + "optionTitle": "Options", + "addOption": "Ajouter une option", + "editProperty": "Modifier la propriété" + }, + "row": { + "duplicate": "Dupliquer", + "delete": "Supprimer", + "textPlaceholder": "Vide", + "copyProperty": "Copie de la propriété dans le presse-papiers" + }, + "selectOption": { + "create": "Créer", + "purpleColor": "Violet", + "pinkColor": "Rose", + "lightPinkColor": "Rose clair", + "orangeColor": "Orange", + "yellowColor": "Jaune", + "limeColor": "Citron vert", + "greenColor": "Vert", + "aquaColor": "Aqua", + "blueColor": "Bleu", + "deleteTag": "Supprimer l'étiquette", + "colorPannelTitle": "Couleurs", + "pannelTitle": "Sélectionnez une option ou créez-en une", + "searchOption": "Rechercher une option" + }, + "menuName": "Grille" + }, + "document": { + "menuName": "Doc", + "date": { + "timeHintTextInTwelveHour": "12:00 AM", + "timeHintTextInTwentyFourHour": "12:00" + } } } From c81b6f10fa3ff421775d52a15ee3b8e7950407c3 Mon Sep 17 00:00:00 2001 From: appflowy Date: Fri, 19 Aug 2022 19:59:09 +0800 Subject: [PATCH 03/86] refactor: baord configuration --- .../flowy-grid/src/entities/field_entities.rs | 21 ++++++ .../src/entities/filter_entities/util.rs | 22 ++++-- .../entities/group_entities/configuration.rs | 31 +++++++++ .../src/entities/group_entities/group.rs | 24 ++++--- .../group_entities/group_changeset.rs | 56 +++++++++++++++- .../src/entities/setting_entities.rs | 22 +++++- .../flowy-grid/src/entities/sort_entities.rs | 11 +-- .../rust-lib/flowy-grid/src/event_handler.rs | 11 +++ frontend/rust-lib/flowy-grid/src/event_map.rs | 4 ++ .../flowy-grid/src/services/grid_editor.rs | 5 ++ .../src/services/grid_view_editor.rs | 9 ++- .../src/services/grid_view_manager.rs | 2 + .../group/group_generator/group_controller.rs | 10 +-- .../group_generator/select_option_group.rs | 1 - .../src/services/group/group_service.rs | 53 ++++++++++----- .../src/services/setting/setting_builder.rs | 4 +- .../tests/grid/filter_test/script.rs | 4 +- .../flowy-grid/tests/grid/grid_editor.rs | 2 +- .../src/revision/grid_setting_rev.rs | 19 +++++- .../src/revision/group_rev.rs | 59 ++++++++++++++++ .../flowy-grid-data-model/src/revision/mod.rs | 2 + .../src/client_grid/view_revision_pad.rs | 55 +-------------- shared-lib/flowy-sync/src/entities/grid.rs | 67 ------------------- shared-lib/flowy-sync/src/entities/mod.rs | 1 - 24 files changed, 326 insertions(+), 169 deletions(-) create mode 100644 shared-lib/flowy-grid-data-model/src/revision/group_rev.rs delete mode 100644 shared-lib/flowy-sync/src/entities/grid.rs diff --git a/frontend/rust-lib/flowy-grid/src/entities/field_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/field_entities.rs index 8875b3fc26..0993853b8e 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/field_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/field_entities.rs @@ -491,6 +491,27 @@ impl TryInto for FieldChangesetPayloadPB { } } +#[derive(Debug, Clone, Default)] +pub struct FieldChangesetParams { + pub field_id: String, + + pub grid_id: String, + + pub name: Option, + + pub desc: Option, + + pub field_type: Option, + + pub frozen: Option, + + pub visibility: Option, + + pub width: Option, + + pub type_option_data: Option>, +} + #[derive( Debug, Clone, diff --git a/frontend/rust-lib/flowy-grid/src/entities/filter_entities/util.rs b/frontend/rust-lib/flowy-grid/src/entities/filter_entities/util.rs index 079b6fd6dc..7dff00bf56 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/filter_entities/util.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/filter_entities/util.rs @@ -5,8 +5,7 @@ use crate::entities::{ use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; -use flowy_grid_data_model::revision::{FieldRevision, FilterConfigurationRevision}; -use flowy_sync::entities::grid::{CreateGridFilterParams, DeleteFilterParams}; +use flowy_grid_data_model::revision::{FieldRevision, FieldTypeRevision, FilterConfigurationRevision}; use std::convert::TryInto; use std::sync::Arc; @@ -72,6 +71,12 @@ impl TryInto for DeleteFilterPayloadPB { } } +pub struct DeleteFilterParams { + pub field_id: String, + pub filter_id: String, + pub field_type_rev: FieldTypeRevision, +} + #[derive(ProtoBuf, Debug, Default, Clone)] pub struct CreateGridFilterPayloadPB { #[pb(index = 1)] @@ -99,10 +104,10 @@ impl CreateGridFilterPayloadPB { } } -impl TryInto for CreateGridFilterPayloadPB { +impl TryInto for CreateGridFilterPayloadPB { type Error = ErrorCode; - fn try_into(self) -> Result { + fn try_into(self) -> Result { let field_id = NotEmptyStr::parse(self.field_id) .map_err(|_| ErrorCode::FieldIdIsEmpty)? .0; @@ -125,7 +130,7 @@ impl TryInto for CreateGridFilterPayloadPB { } } - Ok(CreateGridFilterParams { + Ok(CreateFilterParams { field_id, field_type_rev: self.field_type.into(), condition, @@ -133,3 +138,10 @@ impl TryInto for CreateGridFilterPayloadPB { }) } } + +pub struct CreateFilterParams { + pub field_id: String, + pub field_type_rev: FieldTypeRevision, + pub condition: u8, + pub content: Option, +} diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/configuration.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/configuration.rs index baa39d91a2..5797da3517 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/configuration.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/configuration.rs @@ -1,4 +1,5 @@ use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; +use flowy_grid_data_model::revision::{GroupRecordRevision, SelectOptionGroupConfigurationRevision}; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct UrlGroupConfigurationPB { @@ -16,6 +17,36 @@ pub struct TextGroupConfigurationPB { pub struct SelectOptionGroupConfigurationPB { #[pb(index = 1)] hide_empty: bool, + + #[pb(index = 2)] + groups: Vec, +} + +impl std::convert::From for SelectOptionGroupConfigurationPB { + fn from(rev: SelectOptionGroupConfigurationRevision) -> Self { + Self { + hide_empty: rev.hide_empty, + groups: rev.groups.into_iter().map(GroupRecordPB::from).collect(), + } + } +} + +#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +pub struct GroupRecordPB { + #[pb(index = 1)] + group_id: String, + + #[pb(index = 2)] + visible: bool, +} + +impl std::convert::From for GroupRecordPB { + fn from(rev: GroupRecordRevision) -> Self { + Self { + group_id: rev.group_id, + visible: rev.visible, + } + } } #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] 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 32f7c4543a..0024040078 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 @@ -2,8 +2,7 @@ use crate::entities::{FieldType, RowPB}; use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; -use flowy_grid_data_model::revision::GroupConfigurationRevision; -use flowy_sync::entities::grid::{CreateGridGroupParams, DeleteGroupParams}; +use flowy_grid_data_model::revision::{FieldTypeRevision, GroupConfigurationRevision}; use std::convert::TryInto; use std::sync::Arc; @@ -86,27 +85,28 @@ pub struct CreateGridGroupPayloadPB { #[pb(index = 2)] pub field_type: FieldType, - - #[pb(index = 3, one_of)] - pub content: Option>, } -impl TryInto for CreateGridGroupPayloadPB { +impl TryInto for CreateGridGroupPayloadPB { type Error = ErrorCode; - fn try_into(self) -> Result { + fn try_into(self) -> Result { let field_id = NotEmptyStr::parse(self.field_id) .map_err(|_| ErrorCode::FieldIdIsEmpty)? .0; - Ok(CreateGridGroupParams { + Ok(CreatGroupParams { field_id, field_type_rev: self.field_type.into(), - content: self.content, }) } } +pub struct CreatGroupParams { + pub field_id: String, + pub field_type_rev: FieldTypeRevision, +} + #[derive(ProtoBuf, Debug, Default, Clone)] pub struct DeleteGroupPayloadPB { #[pb(index = 1)] @@ -137,3 +137,9 @@ impl TryInto for DeleteGroupPayloadPB { }) } } + +pub struct DeleteGroupParams { + pub field_id: String, + pub group_id: String, + pub field_type_rev: FieldTypeRevision, +} diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs index 743da70444..41d46e3cc7 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs @@ -1,5 +1,7 @@ -use crate::entities::{InsertedRowPB, RowPB}; +use crate::entities::{GroupPB, InsertedRowPB, RowPB}; use flowy_derive::ProtoBuf; +use flowy_error::ErrorCode; +use flowy_grid_data_model::parser::NotEmptyStr; use std::fmt::Formatter; #[derive(Debug, Default, ProtoBuf)] @@ -62,3 +64,55 @@ impl GroupRowsChangesetPB { } } } +#[derive(Debug, Default, ProtoBuf)] +pub struct MoveGroupPayloadPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub from_group_id: String, + + #[pb(index = 3)] + pub to_group_id: String, +} + +pub struct MoveGroupParams { + pub view_id: String, + pub from_group_id: String, + pub to_group_id: String, +} + +impl TryInto for MoveGroupPayloadPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let view_id = NotEmptyStr::parse(self.view_id) + .map_err(|_| ErrorCode::GridViewIdIsEmpty)? + .0; + let from_group_id = NotEmptyStr::parse(self.from_group_id) + .map_err(|_| ErrorCode::GroupIdIsEmpty)? + .0; + let to_group_id = NotEmptyStr::parse(self.to_group_id) + .map_err(|_| ErrorCode::GroupIdIsEmpty)? + .0; + Ok(MoveGroupParams { + view_id, + from_group_id, + to_group_id, + }) + } +} + +#[derive(Debug, Default, ProtoBuf)] +pub struct GroupsChangesetPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub inserted_groups: Vec, + + #[pb(index = 3)] + pub deleted_groups: Vec, +} + +impl GroupsChangesetPB {} diff --git a/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs index 89f66dd433..67fcc884e9 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs @@ -1,6 +1,7 @@ use crate::entities::{ - CreateGridFilterPayloadPB, CreateGridGroupPayloadPB, CreateGridSortPayloadPB, DeleteFilterPayloadPB, - DeleteGroupPayloadPB, RepeatedGridConfigurationFilterPB, RepeatedGridGroupConfigurationPB, RepeatedGridSortPB, + CreatGroupParams, CreateFilterParams, CreateGridFilterPayloadPB, CreateGridGroupPayloadPB, CreateGridSortPayloadPB, + CreateSortParams, DeleteFilterParams, DeleteFilterPayloadPB, DeleteGroupParams, DeleteGroupPayloadPB, + RepeatedGridConfigurationFilterPB, RepeatedGridGroupConfigurationPB, RepeatedGridSortPB, }; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; @@ -156,3 +157,20 @@ impl TryInto for GridSettingChangesetPayloadPB { }) } } + +pub struct GridSettingChangesetParams { + pub grid_id: String, + pub layout_type: LayoutRevision, + pub insert_filter: Option, + pub delete_filter: Option, + pub insert_group: Option, + pub delete_group: Option, + pub insert_sort: Option, + pub delete_sort: Option, +} + +impl GridSettingChangesetParams { + pub fn is_filter_changed(&self) -> bool { + self.insert_filter.is_some() || self.delete_filter.is_some() + } +} diff --git a/frontend/rust-lib/flowy-grid/src/entities/sort_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/sort_entities.rs index f844b75066..28486a1ee6 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/sort_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/sort_entities.rs @@ -2,7 +2,6 @@ use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; use flowy_grid_data_model::revision::SortConfigurationRevision; -use flowy_sync::entities::grid::CreateGridSortParams; use std::convert::TryInto; use std::sync::Arc; @@ -51,15 +50,19 @@ pub struct CreateGridSortPayloadPB { pub field_id: Option, } -impl TryInto for CreateGridSortPayloadPB { +impl TryInto for CreateGridSortPayloadPB { type Error = ErrorCode; - fn try_into(self) -> Result { + fn try_into(self) -> Result { let field_id = match self.field_id { None => None, Some(field_id) => Some(NotEmptyStr::parse(field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?.0), }; - Ok(CreateGridSortParams { field_id }) + Ok(CreateSortParams { field_id }) } } + +pub struct CreateSortParams { + pub field_id: Option, +} diff --git a/frontend/rust-lib/flowy-grid/src/event_handler.rs b/frontend/rust-lib/flowy-grid/src/event_handler.rs index 3108acd977..f9f5e39399 100644 --- a/frontend/rust-lib/flowy-grid/src/event_handler.rs +++ b/frontend/rust-lib/flowy-grid/src/event_handler.rs @@ -437,3 +437,14 @@ pub(crate) async fn create_board_card_handler( let row = editor.create_row(params).await?; data_result(row) } + +#[tracing::instrument(level = "debug", skip(data, manager), err)] +pub(crate) async fn move_group_handler( + data: Data, + manager: AppData>, +) -> DataResult { + let params: MoveGroupParams = data.into_inner().try_into()?; + let editor = manager.get_grid_editor(params.view_id.as_ref())?; + let changeset = editor.move_group(params).await?; + data_result(changeset) +} diff --git a/frontend/rust-lib/flowy-grid/src/event_map.rs b/frontend/rust-lib/flowy-grid/src/event_map.rs index 55ef3ff4db..76087fe64b 100644 --- a/frontend/rust-lib/flowy-grid/src/event_map.rs +++ b/frontend/rust-lib/flowy-grid/src/event_map.rs @@ -41,6 +41,7 @@ pub fn create(grid_manager: Arc) -> Module { .event(GridEvent::UpdateDateCell, update_date_cell_handler) // Group .event(GridEvent::CreateBoardCard, create_board_card_handler) + .event(GridEvent::MoveGroup, move_group_handler) .event(GridEvent::GetGroup, get_groups_handler); module @@ -217,4 +218,7 @@ pub enum GridEvent { #[event(input = "CreateBoardCardPayloadPB", output = "RowPB")] CreateBoardCard = 110, + + #[event(input = "MoveGroupPayloadPB", output = "GroupsChangesetPB")] + MoveGroup = 111, } diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index 92cf9da550..581f735994 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -294,6 +294,11 @@ impl GridRevisionEditor { Ok(row_pb) } + pub async fn move_group(&self, params: MoveGroupParams) -> FlowyResult { + // + todo!() + } + pub async fn insert_rows(&self, row_revs: Vec) -> FlowyResult> { let block_id = self.block_id().await?; let mut rows_by_block_id: HashMap> = HashMap::new(); diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs index da3ea1be2d..ac1e905d8a 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs @@ -172,10 +172,15 @@ impl GridViewRevisionEditor { } pub(crate) async fn update_setting(&self, changeset: GridSettingChangesetParams) -> FlowyResult<()> { - let _ = self.modify(|pad| Ok(pad.update_setting(changeset)?)).await; - Ok(()) + // let _ = self.modify(|pad| Ok(pad.update_setting(changeset)?)).await; + // Ok(()) + todo!() } + // pub(crate) async fn insert_group(&self, params: CreateGroupParams) -> FlowyResult<()> { + // + // } + pub(crate) async fn get_filters(&self) -> Vec { let field_revs = self.field_delegate.get_field_revs().await; match self.pad.read().await.get_setting_rev().get_all_filters(&field_revs) { 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 5a8faad6a0..178befc10a 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 @@ -130,6 +130,8 @@ impl GridViewManager { } } + pub(crate) async fn move_group(&self) {} + pub(crate) async fn get_view_editor(&self, view_id: &str) -> FlowyResult> { debug_assert!(!view_id.is_empty()); match self.view_editors.get(view_id) { diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/group_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/group_controller.rs index a1d5df2154..07dcff3f77 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/group_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/group_controller.rs @@ -163,10 +163,12 @@ where G: GroupGenerator, { pub fn new(field_rev: &Arc, configuration: GroupConfigurationRevision) -> FlowyResult { - let configuration = match configuration.content { - None => None, - Some(content) => Some(C::try_from(Bytes::from(content))?), - }; + // let configuration = match configuration.content { + // None => None, + // Some(content) => Some(C::try_from(Bytes::from(content))?), + // }; + + let configuration = None; let field_type_rev = field_rev.ty; let type_option = field_rev.get_type_option_entry::(field_type_rev); let groups = G::generate_groups(&field_rev.id, &configuration, &type_option); diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs index 6e569a7b54..29dc95891f 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs @@ -4,7 +4,6 @@ use crate::services::field::{ MultiSelectTypeOptionPB, SelectOptionCellDataPB, SelectOptionCellDataParser, SingleSelectTypeOptionPB, }; use crate::services::group::{GenericGroupController, Group, GroupController, GroupGenerator, Groupable}; - use flowy_grid_data_model::revision::{FieldRevision, RowChangeset, RowRevision}; // SingleSelect diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs index 0e152bf27c..c07b3acec4 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs @@ -8,7 +8,9 @@ use crate::services::group::{ use bytes::Bytes; use flowy_error::FlowyResult; use flowy_grid_data_model::revision::{ - gen_grid_group_id, FieldRevision, GroupConfigurationRevision, RowChangeset, RowRevision, + gen_grid_group_id, CheckboxGroupConfigurationRevision, DateGroupConfigurationRevision, FieldRevision, + GroupConfigurationRevision, NumberGroupConfigurationRevision, RowChangeset, RowRevision, + SelectOptionGroupConfigurationRevision, TextGroupConfigurationRevision, UrlGroupConfigurationRevision, }; use lib_infra::future::AFFuture; use std::future::Future; @@ -208,20 +210,41 @@ fn find_group_field(field_revs: &[Arc]) -> Option GroupConfigurationRevision { + let field_id = field_rev.id.clone(); + let field_type_rev = field_rev.ty.clone(); let field_type: FieldType = field_rev.ty.into(); - let bytes: Bytes = match field_type { - FieldType::RichText => TextGroupConfigurationPB::default().try_into().unwrap(), - FieldType::Number => NumberGroupConfigurationPB::default().try_into().unwrap(), - FieldType::DateTime => DateGroupConfigurationPB::default().try_into().unwrap(), - FieldType::SingleSelect => SelectOptionGroupConfigurationPB::default().try_into().unwrap(), - FieldType::MultiSelect => SelectOptionGroupConfigurationPB::default().try_into().unwrap(), - FieldType::Checkbox => CheckboxGroupConfigurationPB::default().try_into().unwrap(), - FieldType::URL => UrlGroupConfigurationPB::default().try_into().unwrap(), - }; - GroupConfigurationRevision { - id: gen_grid_group_id(), - field_id: field_rev.id.clone(), - field_type_rev: field_rev.ty, - content: Some(bytes.to_vec()), + match field_type { + FieldType::RichText => { + GroupConfigurationRevision::new(field_id, field_type_rev, TextGroupConfigurationRevision::default()) + .unwrap() + } + FieldType::Number => { + GroupConfigurationRevision::new(field_id, field_type_rev, NumberGroupConfigurationRevision::default()) + .unwrap() + } + FieldType::DateTime => { + GroupConfigurationRevision::new(field_id, field_type_rev, DateGroupConfigurationRevision::default()) + .unwrap() + } + + FieldType::SingleSelect => GroupConfigurationRevision::new( + field_id, + field_type_rev, + SelectOptionGroupConfigurationRevision::default(), + ) + .unwrap(), + FieldType::MultiSelect => GroupConfigurationRevision::new( + field_id, + field_type_rev, + SelectOptionGroupConfigurationRevision::default(), + ) + .unwrap(), + FieldType::Checkbox => { + GroupConfigurationRevision::new(field_id, field_type_rev, CheckboxGroupConfigurationRevision::default()) + .unwrap() + } + FieldType::URL => { + GroupConfigurationRevision::new(field_id, field_type_rev, UrlGroupConfigurationRevision::default()).unwrap() + } } } diff --git a/frontend/rust-lib/flowy-grid/src/services/setting/setting_builder.rs b/frontend/rust-lib/flowy-grid/src/services/setting/setting_builder.rs index 7d285fbe2b..1cc16f7da7 100644 --- a/frontend/rust-lib/flowy-grid/src/services/setting/setting_builder.rs +++ b/frontend/rust-lib/flowy-grid/src/services/setting/setting_builder.rs @@ -3,7 +3,7 @@ use crate::entities::{ RepeatedGridSortPB, }; use flowy_grid_data_model::revision::{FieldRevision, SettingRevision}; -use flowy_sync::entities::grid::{CreateGridFilterParams, DeleteFilterParams, GridSettingChangesetParams}; +use flowy_sync::entities::grid::{CreateFilterParams, DeleteFilterParams, GridSettingChangesetParams}; use std::collections::HashMap; use std::sync::Arc; @@ -26,7 +26,7 @@ impl GridSettingChangesetBuilder { Self { params } } - pub fn insert_filter(mut self, params: CreateGridFilterParams) -> Self { + pub fn insert_filter(mut self, params: CreateFilterParams) -> Self { self.params.insert_filter = Some(params); self } diff --git a/frontend/rust-lib/flowy-grid/tests/grid/filter_test/script.rs b/frontend/rust-lib/flowy-grid/tests/grid/filter_test/script.rs index 650564d8d5..459cf5f82d 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/filter_test/script.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/filter_test/script.rs @@ -6,7 +6,7 @@ use flowy_grid::entities::{CreateGridFilterPayloadPB, GridLayout, GridSettingPB}; use flowy_grid::services::setting::GridSettingChangesetBuilder; use flowy_grid_data_model::revision::{FieldRevision, FieldTypeRevision}; -use flowy_sync::entities::grid::{CreateGridFilterParams, DeleteFilterParams, GridSettingChangesetParams}; +use flowy_sync::entities::grid::{CreateFilterParams, DeleteFilterParams, GridSettingChangesetParams}; use crate::grid::grid_editor::GridEditorTest; pub enum FilterScript { @@ -54,7 +54,7 @@ impl GridFilterTest { let _ = self.editor.update_grid_setting(params).await.unwrap(); } FilterScript::InsertGridTableFilter { payload } => { - let params: CreateGridFilterParams = payload.try_into().unwrap(); + let params: CreateFilterParams = payload.try_into().unwrap(); let layout_type = GridLayout::Table; let params = GridSettingChangesetBuilder::new(&self.grid_id, &layout_type) .insert_filter(params) diff --git a/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs b/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs index dbd5df1484..ea8de69056 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs @@ -13,7 +13,7 @@ use flowy_grid_data_model::revision::*; use flowy_revision::REVISION_WRITE_INTERVAL_IN_MILLIS; use flowy_sync::client_grid::GridBuilder; use flowy_sync::entities::grid::{ - CreateGridFilterParams, DeleteFilterParams, FieldChangesetParams, GridSettingChangesetParams, + CreateFilterParams, DeleteFilterParams, FieldChangesetParams, GridSettingChangesetParams, }; use flowy_test::helper::ViewTest; use flowy_test::FlowySDKTest; diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs index da366f39ab..f91ec4c88b 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs @@ -240,12 +240,29 @@ where } } +pub trait GroupConfigurationSerde {} + #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] pub struct GroupConfigurationRevision { pub id: String, pub field_id: String, pub field_type_rev: FieldTypeRevision, - pub content: Option>, + pub content: String, +} + +impl GroupConfigurationRevision { + pub fn new(field_id: String, field_type: FieldTypeRevision, content: T) -> Result + where + T: serde::Serialize, + { + let content = serde_json::to_string(&content)?; + Ok(Self { + id: gen_grid_group_id(), + field_id, + field_type_rev: field_type, + content, + }) + } } #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq, Hash)] diff --git a/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs new file mode 100644 index 0000000000..932cc60033 --- /dev/null +++ b/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs @@ -0,0 +1,59 @@ +use serde::{Deserialize, Serialize}; +use serde_repr::*; + +#[derive(Default, Serialize, Deserialize)] +pub struct TextGroupConfigurationRevision { + pub hide_empty: bool, +} + +#[derive(Default, Serialize, Deserialize)] +pub struct NumberGroupConfigurationRevision { + pub hide_empty: bool, +} + +#[derive(Default, Serialize, Deserialize)] +pub struct UrlGroupConfigurationRevision { + pub hide_empty: bool, +} + +#[derive(Default, Serialize, Deserialize)] +pub struct CheckboxGroupConfigurationRevision { + pub hide_empty: bool, +} + +#[derive(Default, Serialize, Deserialize)] +pub struct SelectOptionGroupConfigurationRevision { + pub hide_empty: bool, + pub groups: Vec, +} + +#[derive(Default, Serialize, Deserialize)] +pub struct GroupRecordRevision { + pub group_id: String, + + #[serde(default = "DEFAULT_GROUP_RECORD_VISIBILITY")] + pub visible: bool, +} +const DEFAULT_GROUP_RECORD_VISIBILITY: fn() -> bool = || true; + +#[derive(Default, Serialize, Deserialize)] +pub struct DateGroupConfigurationRevision { + pub hide_empty: bool, + pub condition: DateCondition, +} + +#[derive(Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum DateCondition { + Relative = 0, + Day = 1, + Week = 2, + Month = 3, + Year = 4, +} + +impl std::default::Default for DateCondition { + fn default() -> Self { + DateCondition::Relative + } +} diff --git a/shared-lib/flowy-grid-data-model/src/revision/mod.rs b/shared-lib/flowy-grid-data-model/src/revision/mod.rs index 7ea98d78e3..d9df0910bd 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/mod.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/mod.rs @@ -2,8 +2,10 @@ mod grid_block; mod grid_rev; mod grid_setting_rev; mod grid_view; +mod group_rev; pub use grid_block::*; pub use grid_rev::*; pub use grid_setting_rev::*; pub use grid_view::*; +pub use group_rev::*; diff --git a/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs b/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs index dd6cc6f977..4220a4367b 100644 --- a/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs +++ b/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs @@ -1,4 +1,4 @@ -use crate::entities::grid::{CreateGridFilterParams, CreateGridGroupParams, GridSettingChangesetParams}; +use crate::entities::grid::{CreatGroupParams, CreateFilterParams, GridSettingChangesetParams}; use crate::entities::revision::{md5, Revision}; use crate::errors::{internal_error, CollaborateError, CollaborateResult}; use crate::util::{cal_diff, make_text_delta_from_revisions}; @@ -54,55 +54,6 @@ impl GridViewRevisionPad { &self.view.setting } - pub fn update_setting( - &mut self, - changeset: GridSettingChangesetParams, - ) -> CollaborateResult> { - self.modify(|view| { - let mut is_changed = None; - if let Some(params) = changeset.insert_filter { - view.setting.filters.insert_object( - ¶ms.field_id, - ¶ms.field_type_rev, - make_filter_revision(¶ms), - ); - is_changed = Some(()) - } - if let Some(params) = changeset.delete_filter { - if let Some(filters) = view - .setting - .filters - .get_mut_objects(¶ms.field_id, ¶ms.field_type_rev) - { - filters.retain(|filter| filter.id != params.filter_id); - is_changed = Some(()) - } - } - if let Some(params) = changeset.insert_group { - view.setting.groups.remove_all(); - view.setting.groups.insert_object( - ¶ms.field_id, - ¶ms.field_type_rev, - make_group_revision(¶ms), - ); - - is_changed = Some(()); - } - if let Some(params) = changeset.delete_group { - if let Some(groups) = view - .setting - .groups - .get_mut_objects(¶ms.field_id, ¶ms.field_type_rev) - { - groups.retain(|group| group.id != params.group_id); - is_changed = Some(()); - } - } - - Ok(is_changed) - }) - } - pub fn get_all_groups(&self, field_revs: &[Arc]) -> Option { self.setting.groups.get_all_objects(field_revs) } @@ -216,7 +167,7 @@ impl GridViewRevisionPad { } } -fn make_filter_revision(params: &CreateGridFilterParams) -> FilterConfigurationRevision { +fn make_filter_revision(params: &CreateFilterParams) -> FilterConfigurationRevision { FilterConfigurationRevision { id: gen_grid_filter_id(), field_id: params.field_id.clone(), @@ -225,7 +176,7 @@ fn make_filter_revision(params: &CreateGridFilterParams) -> FilterConfigurationR } } -fn make_group_revision(params: &CreateGridGroupParams) -> GroupConfigurationRevision { +fn make_group_revision(params: &CreatGroupParams) -> GroupConfigurationRevision { GroupConfigurationRevision { id: gen_grid_group_id(), field_id: params.field_id.clone(), diff --git a/shared-lib/flowy-sync/src/entities/grid.rs b/shared-lib/flowy-sync/src/entities/grid.rs deleted file mode 100644 index 3be3d98267..0000000000 --- a/shared-lib/flowy-sync/src/entities/grid.rs +++ /dev/null @@ -1,67 +0,0 @@ -use flowy_grid_data_model::revision::{FieldTypeRevision, LayoutRevision}; - -pub struct GridSettingChangesetParams { - pub grid_id: String, - pub layout_type: LayoutRevision, - pub insert_filter: Option, - pub delete_filter: Option, - pub insert_group: Option, - pub delete_group: Option, - pub insert_sort: Option, - pub delete_sort: Option, -} - -impl GridSettingChangesetParams { - pub fn is_filter_changed(&self) -> bool { - self.insert_filter.is_some() || self.delete_filter.is_some() - } -} -pub struct CreateGridFilterParams { - pub field_id: String, - pub field_type_rev: FieldTypeRevision, - pub condition: u8, - pub content: Option, -} - -pub struct DeleteFilterParams { - pub field_id: String, - pub filter_id: String, - pub field_type_rev: FieldTypeRevision, -} - -pub struct CreateGridGroupParams { - pub field_id: String, - pub field_type_rev: FieldTypeRevision, - pub content: Option>, -} - -pub struct DeleteGroupParams { - pub field_id: String, - pub group_id: String, - pub field_type_rev: FieldTypeRevision, -} - -pub struct CreateGridSortParams { - pub field_id: Option, -} - -#[derive(Debug, Clone, Default)] -pub struct FieldChangesetParams { - pub field_id: String, - - pub grid_id: String, - - pub name: Option, - - pub desc: Option, - - pub field_type: Option, - - pub frozen: Option, - - pub visibility: Option, - - pub width: Option, - - pub type_option_data: Option>, -} diff --git a/shared-lib/flowy-sync/src/entities/mod.rs b/shared-lib/flowy-sync/src/entities/mod.rs index 768640454b..1c357df94c 100644 --- a/shared-lib/flowy-sync/src/entities/mod.rs +++ b/shared-lib/flowy-sync/src/entities/mod.rs @@ -1,5 +1,4 @@ pub mod folder; -pub mod grid; pub mod parser; pub mod revision; pub mod text_block; From 5fac1f044a24e281a5578bccd5ada8900af83127 Mon Sep 17 00:00:00 2001 From: appflowy Date: Fri, 19 Aug 2022 23:11:05 +0800 Subject: [PATCH 04/86] chore: move some structs --- .../src/entities/group_entities/board_card.rs | 28 ---- .../src/entities/group_entities/group.rs | 26 +++- .../src/entities/group_entities/mod.rs | 2 - .../flowy-grid/src/services/grid_editor.rs | 127 ++++++++++++------ .../checkbox_controller.rs} | 0 .../group_controller.rs | 0 .../src/services/group/controllers/mod.rs | 7 + .../select_option_controller.rs} | 0 .../src/services/group/group_generator/mod.rs | 7 - .../flowy-grid/src/services/group/mod.rs | 4 +- .../src/revision/grid_setting_rev.rs | 4 +- .../src/client_grid/grid_revision_pad.rs | 58 +------- .../src/client_grid/view_revision_pad.rs | 24 +--- 13 files changed, 128 insertions(+), 159 deletions(-) delete mode 100644 frontend/rust-lib/flowy-grid/src/entities/group_entities/board_card.rs rename frontend/rust-lib/flowy-grid/src/services/group/{group_generator/checkbox_group.rs => controllers/checkbox_controller.rs} (100%) rename frontend/rust-lib/flowy-grid/src/services/group/{group_generator => controllers}/group_controller.rs (100%) create mode 100644 frontend/rust-lib/flowy-grid/src/services/group/controllers/mod.rs rename frontend/rust-lib/flowy-grid/src/services/group/{group_generator/select_option_group.rs => controllers/select_option_controller.rs} (100%) delete mode 100644 frontend/rust-lib/flowy-grid/src/services/group/group_generator/mod.rs diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/board_card.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/board_card.rs deleted file mode 100644 index 1ba3991f96..0000000000 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/board_card.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::entities::{CreateRowParams, GridLayout}; -use flowy_derive::ProtoBuf; -use flowy_error::ErrorCode; -use flowy_grid_data_model::parser::NotEmptyStr; - -#[derive(ProtoBuf, Debug, Default, Clone)] -pub struct CreateBoardCardPayloadPB { - #[pb(index = 1)] - pub grid_id: String, - - #[pb(index = 2)] - pub group_id: String, -} - -impl TryInto for CreateBoardCardPayloadPB { - type Error = ErrorCode; - - fn try_into(self) -> Result { - let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?; - let group_id = NotEmptyStr::parse(self.group_id).map_err(|_| ErrorCode::GroupIdIsEmpty)?; - Ok(CreateRowParams { - grid_id: grid_id.0, - start_row_id: None, - group_id: Some(group_id.0), - layout: GridLayout::Board, - }) - } -} 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 0024040078..b769b18154 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 @@ -1,4 +1,4 @@ -use crate::entities::{FieldType, RowPB}; +use crate::entities::{CreateRowParams, FieldType, GridLayout, RowPB}; use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; @@ -6,6 +6,30 @@ use flowy_grid_data_model::revision::{FieldTypeRevision, GroupConfigurationRevis use std::convert::TryInto; use std::sync::Arc; +#[derive(ProtoBuf, Debug, Default, Clone)] +pub struct CreateBoardCardPayloadPB { + #[pb(index = 1)] + pub grid_id: String, + + #[pb(index = 2)] + pub group_id: String, +} + +impl TryInto for CreateBoardCardPayloadPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?; + let group_id = NotEmptyStr::parse(self.group_id).map_err(|_| ErrorCode::GroupIdIsEmpty)?; + Ok(CreateRowParams { + grid_id: grid_id.0, + start_row_id: None, + group_id: Some(group_id.0), + layout: GridLayout::Board, + }) + } +} + #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct GridGroupConfigurationPB { #[pb(index = 1)] diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/mod.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/mod.rs index f5daa803bc..778eff4cc9 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/mod.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/mod.rs @@ -1,9 +1,7 @@ -mod board_card; mod configuration; mod group; mod group_changeset; -pub use board_card::*; pub use configuration::*; pub use group::*; pub use group_changeset::*; diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index 581f735994..055501e117 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -96,23 +96,19 @@ impl GridRevisionEditor { } = params; let field_id = field.id.clone(); if self.contain_field(&field_id).await { - let _ = self - .modify(|grid| { - let deserializer = TypeOptionJsonDeserializer(field.field_type.clone()); - let changeset = FieldChangesetParams { - field_id: field.id, - grid_id, - name: Some(field.name), - desc: Some(field.desc), - field_type: Some(field.field_type.into()), - frozen: Some(field.frozen), - visibility: Some(field.visibility), - width: Some(field.width), - type_option_data: Some(type_option_data), - }; - Ok(grid.update_field_rev(changeset, deserializer)?) - }) - .await?; + let changeset = FieldChangesetParams { + field_id: field.id, + grid_id, + name: Some(field.name), + desc: Some(field.desc), + field_type: Some(field.field_type.into()), + frozen: Some(field.frozen), + visibility: Some(field.visibility), + width: Some(field.width), + type_option_data: Some(type_option_data), + }; + + let _ = self.update_field_rev(changeset, field.field_type.clone()).await?; let _ = self.notify_did_update_grid_field(&field_id).await?; } else { let _ = self @@ -140,19 +136,13 @@ impl GridRevisionEditor { return Ok(()); } let field_rev = result.unwrap(); - let _ = self - .modify(|grid| { - let field_type = field_rev.ty.into(); - let deserializer = TypeOptionJsonDeserializer(field_type); - let changeset = FieldChangesetParams { - field_id: field_id.to_owned(), - grid_id: grid_id.to_owned(), - type_option_data: Some(type_option_data), - ..Default::default() - }; - Ok(grid.update_field_rev(changeset, deserializer)?) - }) - .await?; + let changeset = FieldChangesetParams { + field_id: field_id.to_owned(), + grid_id: grid_id.to_owned(), + type_option_data: Some(type_option_data), + ..Default::default() + }; + let _ = self.update_field_rev(changeset, field_rev.ty.into()).await?; let _ = self.notify_did_update_grid_field(field_id).await?; Ok(()) } @@ -179,17 +169,19 @@ impl GridRevisionEditor { pub async fn update_field(&self, params: FieldChangesetParams) -> FlowyResult<()> { let field_id = params.field_id.clone(); - let json_deserializer = match self.grid_pad.read().await.get_field_rev(params.field_id.as_str()) { - None => return Err(ErrorCode::FieldDoesNotExist.into()), - Some((_, field_rev)) => TypeOptionJsonDeserializer(field_rev.ty.into()), + let field_type: Option = match self.grid_pad.read().await.get_field_rev(params.field_id.as_str()) { + None => None, + Some((_, field_rev)) => Some(field_rev.ty.into()), }; - let _ = self - .modify(|grid| Ok(grid.update_field_rev(params, json_deserializer)?)) - .await?; - - let _ = self.notify_did_update_grid_field(&field_id).await?; - Ok(()) + match field_type { + None => Err(ErrorCode::FieldDoesNotExist.into()), + Some(field_type) => { + let _ = self.update_field_rev(params, field_type).await?; + let _ = self.notify_did_update_grid_field(&field_id).await?; + Ok(()) + } + } } pub async fn replace_field(&self, field_rev: Arc) -> FlowyResult<()> { @@ -269,6 +261,63 @@ impl GridRevisionEditor { Ok(field_revs) } + async fn update_field_rev(&self, params: FieldChangesetParams, field_type: FieldType) -> FlowyResult<()> { + let _ = self + .modify(|grid| { + let deserializer = TypeOptionJsonDeserializer(field_type); + + let changeset = grid.modify_field(¶ms.field_id, |field| { + let mut is_changed = None; + if let Some(name) = changeset.name { + field.name = name; + is_changed = Some(()) + } + + if let Some(desc) = changeset.desc { + field.desc = desc; + is_changed = Some(()) + } + + if let Some(field_type) = changeset.field_type { + field.ty = field_type; + is_changed = Some(()) + } + + if let Some(frozen) = changeset.frozen { + field.frozen = frozen; + is_changed = Some(()) + } + + if let Some(visibility) = changeset.visibility { + field.visibility = visibility; + is_changed = Some(()) + } + + if let Some(width) = changeset.width { + field.width = width; + is_changed = Some(()) + } + + if let Some(type_option_data) = changeset.type_option_data { + match deserializer.deserialize(type_option_data) { + Ok(json_str) => { + let field_type = field.ty; + field.insert_type_option_str(&field_type, json_str); + is_changed = Some(()) + } + Err(err) => { + tracing::error!("Deserialize data to type option json failed: {}", err); + } + } + } + + Ok(is_changed) + })?; + Ok(changeset) + }) + .await?; + } + pub async fn create_block(&self, block_meta_rev: GridBlockMetaRevision) -> FlowyResult<()> { let _ = self .modify(|grid_pad| Ok(grid_pad.create_block_meta_rev(block_meta_rev)?)) diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/controllers/checkbox_controller.rs similarity index 100% rename from frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs rename to frontend/rust-lib/flowy-grid/src/services/group/controllers/checkbox_controller.rs diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/group_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controllers/group_controller.rs similarity index 100% rename from frontend/rust-lib/flowy-grid/src/services/group/group_generator/group_controller.rs rename to frontend/rust-lib/flowy-grid/src/services/group/controllers/group_controller.rs diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controllers/mod.rs b/frontend/rust-lib/flowy-grid/src/services/group/controllers/mod.rs new file mode 100644 index 0000000000..29ce5d091e --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/group/controllers/mod.rs @@ -0,0 +1,7 @@ +mod checkbox_controller; +mod group_controller; +mod select_option_controller; + +pub use checkbox_controller::*; +pub use group_controller::*; +pub use select_option_controller::*; diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/controllers/select_option_controller.rs similarity index 100% rename from frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs rename to frontend/rust-lib/flowy-grid/src/services/group/controllers/select_option_controller.rs diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/mod.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/mod.rs deleted file mode 100644 index 08e691a75e..0000000000 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod checkbox_group; -mod group_controller; -mod select_option_group; - -pub use checkbox_group::*; -pub use group_controller::*; -pub use select_option_group::*; diff --git a/frontend/rust-lib/flowy-grid/src/services/group/mod.rs b/frontend/rust-lib/flowy-grid/src/services/group/mod.rs index 15b401f4c0..f8b96c8bab 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/mod.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/mod.rs @@ -1,5 +1,5 @@ -mod group_generator; +mod controllers; mod group_service; -pub(crate) use group_generator::*; +pub(crate) use controllers::*; pub(crate) use group_service::*; diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs index f91ec4c88b..ff15170932 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs @@ -240,7 +240,9 @@ where } } -pub trait GroupConfigurationSerde {} +pub trait GroupConfigurationSerde { + fn from_configuration(s: &str) -> Result; +} #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] pub struct GroupConfigurationRevision { diff --git a/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs b/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs index aad9c8b5f3..e079929aee 100644 --- a/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs +++ b/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs @@ -1,4 +1,3 @@ -use crate::entities::grid::FieldChangesetParams; use crate::entities::revision::{md5, RepeatedRevision, Revision}; use crate::errors::{internal_error, CollaborateError, CollaborateResult}; use crate::util::{cal_diff, make_text_delta_from_revisions}; @@ -162,61 +161,6 @@ impl GridRevisionPad { }) } - pub fn update_field_rev( - &mut self, - changeset: FieldChangesetParams, - deserializer: T, - ) -> CollaborateResult> { - let field_id = changeset.field_id.clone(); - self.modify_field(&field_id, |field| { - let mut is_changed = None; - if let Some(name) = changeset.name { - field.name = name; - is_changed = Some(()) - } - - if let Some(desc) = changeset.desc { - field.desc = desc; - is_changed = Some(()) - } - - if let Some(field_type) = changeset.field_type { - field.ty = field_type; - is_changed = Some(()) - } - - if let Some(frozen) = changeset.frozen { - field.frozen = frozen; - is_changed = Some(()) - } - - if let Some(visibility) = changeset.visibility { - field.visibility = visibility; - is_changed = Some(()) - } - - if let Some(width) = changeset.width { - field.width = width; - is_changed = Some(()) - } - - if let Some(type_option_data) = changeset.type_option_data { - match deserializer.deserialize(type_option_data) { - Ok(json_str) => { - let field_type = field.ty; - field.insert_type_option_str(&field_type, json_str); - is_changed = Some(()) - } - Err(err) => { - tracing::error!("Deserialize data to type option json failed: {}", err); - } - } - } - - Ok(is_changed) - }) - } - pub fn get_field_rev(&self, field_id: &str) -> Option<(usize, &Arc)> { self.grid_rev .fields @@ -399,7 +343,7 @@ impl GridRevisionPad { ) } - fn modify_field(&mut self, field_id: &str, f: F) -> CollaborateResult> + pub fn modify_field(&mut self, field_id: &str, f: F) -> CollaborateResult> where F: FnOnce(&mut FieldRevision) -> CollaborateResult>, { diff --git a/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs b/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs index 4220a4367b..db52d0b38b 100644 --- a/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs +++ b/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs @@ -1,11 +1,9 @@ -use crate::entities::grid::{CreatGroupParams, CreateFilterParams, GridSettingChangesetParams}; use crate::entities::revision::{md5, Revision}; use crate::errors::{internal_error, CollaborateError, CollaborateResult}; use crate::util::{cal_diff, make_text_delta_from_revisions}; use flowy_grid_data_model::revision::{ - gen_grid_filter_id, gen_grid_group_id, FieldRevision, FieldTypeRevision, FilterConfigurationRevision, - FilterConfigurationsByFieldId, GridViewRevision, GroupConfigurationRevision, GroupConfigurationsByFieldId, - SettingRevision, SortConfigurationsByFieldId, + FieldRevision, FieldTypeRevision, FilterConfigurationRevision, FilterConfigurationsByFieldId, GridViewRevision, + GroupConfigurationRevision, GroupConfigurationsByFieldId, SettingRevision, SortConfigurationsByFieldId, }; use lib_ot::core::{OperationTransform, PhantomAttributes, TextDelta, TextDeltaBuilder}; use std::sync::Arc; @@ -167,24 +165,6 @@ impl GridViewRevisionPad { } } -fn make_filter_revision(params: &CreateFilterParams) -> FilterConfigurationRevision { - FilterConfigurationRevision { - id: gen_grid_filter_id(), - field_id: params.field_id.clone(), - condition: params.condition, - content: params.content.clone(), - } -} - -fn make_group_revision(params: &CreatGroupParams) -> GroupConfigurationRevision { - GroupConfigurationRevision { - id: gen_grid_group_id(), - field_id: params.field_id.clone(), - field_type_rev: params.field_type_rev, - content: params.content.clone(), - } -} - pub struct GridViewRevisionChangeset { pub delta: TextDelta, pub md5: String, From 91b2917d603b9dc674bca8f282abe34f0e7a1b42 Mon Sep 17 00:00:00 2001 From: appflowy Date: Sat, 20 Aug 2022 15:40:13 +0800 Subject: [PATCH 05/86] chore: reader and writter for group configuration --- .../flowy-grid/src/entities/field_entities.rs | 1 - .../rust-lib/flowy-grid/src/entities/mod.rs | 2 - .../src/entities/setting_entities.rs | 30 +- .../flowy-grid/src/entities/sort_entities.rs | 68 ----- .../rust-lib/flowy-grid/src/event_handler.rs | 1 - .../src/services/filter/filter_service.rs | 3 +- .../flowy-grid/src/services/grid_editor.rs | 92 +++--- .../src/services/grid_view_editor.rs | 143 ++++++--- .../src/services/grid_view_manager.rs | 5 +- .../flowy-grid/src/services/group/action.rs | 23 ++ .../src/services/group/configuration.rs | 76 +++++ .../checkbox_controller.rs | 44 ++- .../{controllers => controller_impls}/mod.rs | 2 - .../select_option_controller/mod.rs | 6 + .../multi_select_controller.rs | 110 +++++++ .../single_select_controller.rs | 111 +++++++ .../select_option_controller/util.rs | 108 +++++++ .../controllers/select_option_controller.rs | 286 ------------------ .../flowy-grid/src/services/group/entities.rs | 73 +++++ .../{controllers => }/group_controller.rs | 190 ++++-------- .../src/services/group/group_service.rs | 113 ++++--- .../flowy-grid/src/services/group/mod.rs | 10 +- .../src/services/setting/setting_builder.rs | 50 +-- .../src/revision/filter_rev.rs | 9 + .../src/revision/grid_setting_rev.rs | 147 +-------- .../src/revision/grid_view.rs | 104 ++++++- .../src/revision/group_rev.rs | 65 ++++ .../flowy-grid-data-model/src/revision/mod.rs | 2 + .../src/client_grid/view_revision_pad.rs | 36 +-- 29 files changed, 1015 insertions(+), 895 deletions(-) delete mode 100644 frontend/rust-lib/flowy-grid/src/entities/sort_entities.rs create mode 100644 frontend/rust-lib/flowy-grid/src/services/group/action.rs create mode 100644 frontend/rust-lib/flowy-grid/src/services/group/configuration.rs rename frontend/rust-lib/flowy-grid/src/services/group/{controllers => controller_impls}/checkbox_controller.rs (59%) rename frontend/rust-lib/flowy-grid/src/services/group/{controllers => controller_impls}/mod.rs (71%) create mode 100644 frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/mod.rs create mode 100644 frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs create mode 100644 frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs create mode 100644 frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs delete mode 100644 frontend/rust-lib/flowy-grid/src/services/group/controllers/select_option_controller.rs create mode 100644 frontend/rust-lib/flowy-grid/src/services/group/entities.rs rename frontend/rust-lib/flowy-grid/src/services/group/{controllers => }/group_controller.rs (57%) create mode 100644 shared-lib/flowy-grid-data-model/src/revision/filter_rev.rs diff --git a/frontend/rust-lib/flowy-grid/src/entities/field_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/field_entities.rs index 0993853b8e..271cbcf424 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/field_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/field_entities.rs @@ -2,7 +2,6 @@ use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; use flowy_grid_data_model::revision::{FieldRevision, FieldTypeRevision}; -use flowy_sync::entities::grid::FieldChangesetParams; use serde_repr::*; use std::sync::Arc; diff --git a/frontend/rust-lib/flowy-grid/src/entities/mod.rs b/frontend/rust-lib/flowy-grid/src/entities/mod.rs index 96c7a9ba60..eb11d982a4 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/mod.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/mod.rs @@ -6,7 +6,6 @@ mod grid_entities; mod group_entities; mod row_entities; mod setting_entities; -mod sort_entities; pub use block_entities::*; pub use cell_entities::*; @@ -16,4 +15,3 @@ pub use grid_entities::*; pub use group_entities::*; pub use row_entities::*; pub use setting_entities::*; -pub use sort_entities::*; diff --git a/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs index 67fcc884e9..9c02a2c692 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs @@ -1,13 +1,12 @@ use crate::entities::{ - CreatGroupParams, CreateFilterParams, CreateGridFilterPayloadPB, CreateGridGroupPayloadPB, CreateGridSortPayloadPB, - CreateSortParams, DeleteFilterParams, DeleteFilterPayloadPB, DeleteGroupParams, DeleteGroupPayloadPB, - RepeatedGridConfigurationFilterPB, RepeatedGridGroupConfigurationPB, RepeatedGridSortPB, + CreatGroupParams, CreateFilterParams, CreateGridFilterPayloadPB, CreateGridGroupPayloadPB, DeleteFilterParams, + DeleteFilterPayloadPB, DeleteGroupParams, DeleteGroupPayloadPB, RepeatedGridConfigurationFilterPB, + RepeatedGridGroupConfigurationPB, }; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; use flowy_grid_data_model::revision::LayoutRevision; -use flowy_sync::entities::grid::GridSettingChangesetParams; use std::collections::HashMap; use std::convert::TryInto; use strum::IntoEnumIterator; @@ -27,9 +26,6 @@ pub struct GridSettingPB { #[pb(index = 4)] pub group_configuration_by_field_id: HashMap, - - #[pb(index = 5)] - pub sorts_by_field_id: HashMap, } #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] @@ -99,12 +95,6 @@ pub struct GridSettingChangesetPayloadPB { #[pb(index = 6, one_of)] pub delete_group: Option, - - #[pb(index = 7, one_of)] - pub insert_sort: Option, - - #[pb(index = 8, one_of)] - pub delete_sort: Option, } impl TryInto for GridSettingChangesetPayloadPB { @@ -135,16 +125,6 @@ impl TryInto for GridSettingChangesetPayloadPB { None => None, }; - let insert_sort = match self.insert_sort { - None => None, - Some(payload) => Some(payload.try_into()?), - }; - - let delete_sort = match self.delete_sort { - None => None, - Some(filter_id) => Some(NotEmptyStr::parse(filter_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?.0), - }; - Ok(GridSettingChangesetParams { grid_id: view_id, layout_type: self.layout_type.into(), @@ -152,8 +132,6 @@ impl TryInto for GridSettingChangesetPayloadPB { delete_filter, insert_group, delete_group, - insert_sort, - delete_sort, }) } } @@ -165,8 +143,6 @@ pub struct GridSettingChangesetParams { pub delete_filter: Option, pub insert_group: Option, pub delete_group: Option, - pub insert_sort: Option, - pub delete_sort: Option, } impl GridSettingChangesetParams { diff --git a/frontend/rust-lib/flowy-grid/src/entities/sort_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/sort_entities.rs deleted file mode 100644 index 28486a1ee6..0000000000 --- a/frontend/rust-lib/flowy-grid/src/entities/sort_entities.rs +++ /dev/null @@ -1,68 +0,0 @@ -use flowy_derive::ProtoBuf; -use flowy_error::ErrorCode; -use flowy_grid_data_model::parser::NotEmptyStr; -use flowy_grid_data_model::revision::SortConfigurationRevision; -use std::convert::TryInto; -use std::sync::Arc; - -#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] -pub struct GridSort { - #[pb(index = 1)] - pub id: String, - - #[pb(index = 2, one_of)] - pub field_id: Option, -} - -impl std::convert::From<&SortConfigurationRevision> for GridSort { - fn from(rev: &SortConfigurationRevision) -> Self { - GridSort { - id: rev.id.clone(), - - field_id: rev.field_id.clone(), - } - } -} - -#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] -pub struct RepeatedGridSortPB { - #[pb(index = 1)] - pub items: Vec, -} - -impl std::convert::From>> for RepeatedGridSortPB { - fn from(revs: Vec>) -> Self { - RepeatedGridSortPB { - items: revs.into_iter().map(|rev| rev.as_ref().into()).collect(), - } - } -} - -impl std::convert::From> for RepeatedGridSortPB { - fn from(items: Vec) -> Self { - Self { items } - } -} - -#[derive(ProtoBuf, Debug, Default, Clone)] -pub struct CreateGridSortPayloadPB { - #[pb(index = 1, one_of)] - pub field_id: Option, -} - -impl TryInto for CreateGridSortPayloadPB { - type Error = ErrorCode; - - fn try_into(self) -> Result { - let field_id = match self.field_id { - None => None, - Some(field_id) => Some(NotEmptyStr::parse(field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?.0), - }; - - Ok(CreateSortParams { field_id }) - } -} - -pub struct CreateSortParams { - pub field_id: Option, -} diff --git a/frontend/rust-lib/flowy-grid/src/event_handler.rs b/frontend/rust-lib/flowy-grid/src/event_handler.rs index f9f5e39399..47df88b976 100644 --- a/frontend/rust-lib/flowy-grid/src/event_handler.rs +++ b/frontend/rust-lib/flowy-grid/src/event_handler.rs @@ -10,7 +10,6 @@ use crate::services::field::{ use crate::services::row::make_row_from_row_rev; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_grid_data_model::revision::FieldRevision; -use flowy_sync::entities::grid::{FieldChangesetParams, GridSettingChangesetParams}; use lib_dispatch::prelude::{data_result, AppData, Data, DataResult}; use std::sync::Arc; diff --git a/frontend/rust-lib/flowy-grid/src/services/filter/filter_service.rs b/frontend/rust-lib/flowy-grid/src/services/filter/filter_service.rs index a2831c0ad9..8335a36fb2 100644 --- a/frontend/rust-lib/flowy-grid/src/services/filter/filter_service.rs +++ b/frontend/rust-lib/flowy-grid/src/services/filter/filter_service.rs @@ -4,7 +4,7 @@ #![allow(unused_imports)] #![allow(unused_results)] use crate::dart_notification::{send_dart_notification, GridNotification}; -use crate::entities::{FieldType, GridBlockChangesetPB}; +use crate::entities::{FieldType, GridBlockChangesetPB, GridSettingChangesetParams}; use crate::services::block_manager::GridBlockManager; use crate::services::cell::{AnyCellData, CellFilterOperation}; use crate::services::field::{ @@ -20,7 +20,6 @@ use crate::services::tasks::{FilterTaskContext, Task, TaskContent}; use flowy_error::FlowyResult; use flowy_grid_data_model::revision::{CellRevision, FieldId, FieldRevision, RowRevision}; use flowy_sync::client_grid::GridRevisionPad; -use flowy_sync::entities::grid::GridSettingChangesetParams; use rayon::prelude::*; use std::collections::HashMap; use std::sync::Arc; diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index 055501e117..0559cfa364 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -15,7 +15,6 @@ use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_grid_data_model::revision::*; use flowy_revision::{RevisionCloudService, RevisionCompactor, RevisionManager, RevisionObjectBuilder}; use flowy_sync::client_grid::{GridRevisionChangeset, GridRevisionPad, JsonDeserializer}; -use flowy_sync::entities::grid::{FieldChangesetParams, GridSettingChangesetParams}; use flowy_sync::entities::revision::Revision; use flowy_sync::errors::CollaborateResult; use flowy_sync::util::make_text_delta_from_revisions; @@ -101,14 +100,14 @@ impl GridRevisionEditor { grid_id, name: Some(field.name), desc: Some(field.desc), - field_type: Some(field.field_type.into()), + field_type: Some(field.field_type.clone().into()), frozen: Some(field.frozen), visibility: Some(field.visibility), width: Some(field.width), type_option_data: Some(type_option_data), }; - let _ = self.update_field_rev(changeset, field.field_type.clone()).await?; + let _ = self.update_field_rev(changeset, field.field_type).await?; let _ = self.notify_did_update_grid_field(&field_id).await?; } else { let _ = self @@ -262,60 +261,59 @@ impl GridRevisionEditor { } async fn update_field_rev(&self, params: FieldChangesetParams, field_type: FieldType) -> FlowyResult<()> { - let _ = self - .modify(|grid| { - let deserializer = TypeOptionJsonDeserializer(field_type); + self.modify(|grid| { + let deserializer = TypeOptionJsonDeserializer(field_type); - let changeset = grid.modify_field(¶ms.field_id, |field| { - let mut is_changed = None; - if let Some(name) = changeset.name { - field.name = name; - is_changed = Some(()) - } + let changeset = grid.modify_field(¶ms.field_id, |field| { + let mut is_changed = None; + if let Some(name) = params.name { + field.name = name; + is_changed = Some(()) + } - if let Some(desc) = changeset.desc { - field.desc = desc; - is_changed = Some(()) - } + if let Some(desc) = params.desc { + field.desc = desc; + is_changed = Some(()) + } - if let Some(field_type) = changeset.field_type { - field.ty = field_type; - is_changed = Some(()) - } + if let Some(field_type) = params.field_type { + field.ty = field_type; + is_changed = Some(()) + } - if let Some(frozen) = changeset.frozen { - field.frozen = frozen; - is_changed = Some(()) - } + if let Some(frozen) = params.frozen { + field.frozen = frozen; + is_changed = Some(()) + } - if let Some(visibility) = changeset.visibility { - field.visibility = visibility; - is_changed = Some(()) - } + if let Some(visibility) = params.visibility { + field.visibility = visibility; + is_changed = Some(()) + } - if let Some(width) = changeset.width { - field.width = width; - is_changed = Some(()) - } + if let Some(width) = params.width { + field.width = width; + is_changed = Some(()) + } - if let Some(type_option_data) = changeset.type_option_data { - match deserializer.deserialize(type_option_data) { - Ok(json_str) => { - let field_type = field.ty; - field.insert_type_option_str(&field_type, json_str); - is_changed = Some(()) - } - Err(err) => { - tracing::error!("Deserialize data to type option json failed: {}", err); - } + if let Some(type_option_data) = params.type_option_data { + match deserializer.deserialize(type_option_data) { + Ok(json_str) => { + let field_type = field.ty; + field.insert_type_option_str(&field_type, json_str); + is_changed = Some(()) + } + Err(err) => { + tracing::error!("Deserialize data to type option json failed: {}", err); } } + } - Ok(is_changed) - })?; - Ok(changeset) - }) - .await?; + Ok(is_changed) + })?; + Ok(changeset) + }) + .await } pub async fn create_block(&self, block_meta_rev: GridBlockMetaRevision) -> FlowyResult<()> { diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs index ac1e905d8a..9a7173035a 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs @@ -1,18 +1,23 @@ use crate::dart_notification::{send_dart_notification, GridNotification}; use crate::entities::{ - CreateRowParams, GridFilterConfiguration, GridSettingPB, GroupPB, GroupRowsChangesetPB, InsertedRowPB, RowPB, + CreateRowParams, GridFilterConfiguration, GridLayout, GridLayoutPB, GridSettingChangesetParams, GridSettingPB, + GroupPB, GroupRowsChangesetPB, InsertedRowPB, RepeatedGridConfigurationFilterPB, RepeatedGridGroupConfigurationPB, + RowPB, }; use crate::services::grid_editor_task::GridServiceTaskScheduler; use crate::services::grid_view_manager::{GridViewFieldDelegate, GridViewRowDelegate}; -use crate::services::group::{default_group_configuration, GroupConfigurationDelegate, GroupService}; -use crate::services::setting::make_grid_setting; +use crate::services::group::{ + default_group_configuration, GroupConfigurationReader, GroupConfigurationWriter, GroupService, +}; use flowy_error::{FlowyError, FlowyResult}; -use flowy_grid_data_model::revision::{FieldRevision, GroupConfigurationRevision, RowChangeset, RowRevision}; +use flowy_grid_data_model::revision::{ + FieldRevision, FieldTypeRevision, GroupConfigurationRevision, RowChangeset, RowRevision, +}; use flowy_revision::{RevisionCloudService, RevisionManager, RevisionObjectBuilder}; use flowy_sync::client_grid::{GridViewRevisionChangeset, GridViewRevisionPad}; -use flowy_sync::entities::grid::GridSettingChangesetParams; use flowy_sync::entities::revision::Revision; use lib_infra::future::{wrap_future, AFFuture, FutureResult}; +use std::collections::HashMap; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use tokio::sync::RwLock; @@ -46,7 +51,14 @@ impl GridViewRevisionEditor { let view_revision_pad = rev_manager.load::(Some(cloud)).await?; let pad = Arc::new(RwLock::new(view_revision_pad)); let rev_manager = Arc::new(rev_manager); - let group_service = GroupService::new(Box::new(pad.clone())).await; + + let configuration_reader = GroupConfigurationReaderImpl(pad.clone()); + let configuration_writer = GroupConfigurationWriterImpl { + user_id: user_id.to_owned(), + rev_manager: rev_manager.clone(), + view_pad: pad.clone(), + }; + let group_service = GroupService::new(configuration_reader, configuration_writer).await; let user_id = user_id.to_owned(); let did_load_group = AtomicBool::new(false); Ok(Self { @@ -167,7 +179,7 @@ impl GridViewRevisionEditor { pub(crate) async fn get_setting(&self) -> GridSettingPB { let field_revs = self.field_delegate.get_field_revs().await; - let grid_setting = make_grid_setting(self.pad.read().await.get_setting_rev(), &field_revs); + let grid_setting = make_grid_setting(&*self.pad.read().await, &field_revs); grid_setting } @@ -177,13 +189,9 @@ impl GridViewRevisionEditor { todo!() } - // pub(crate) async fn insert_group(&self, params: CreateGroupParams) -> FlowyResult<()> { - // - // } - pub(crate) async fn get_filters(&self) -> Vec { let field_revs = self.field_delegate.get_field_revs().await; - match self.pad.read().await.get_setting_rev().get_all_filters(&field_revs) { + match self.pad.read().await.get_all_filters(&field_revs) { None => vec![], Some(filters) => filters .into_values() @@ -207,28 +215,24 @@ impl GridViewRevisionEditor { match f(&mut *write_guard)? { None => {} Some(change) => { - let _ = self.apply_change(change).await?; + let _ = apply_change(&self.user_id, self.rev_manager.clone(), change).await?; } } Ok(()) } +} - async fn apply_change(&self, change: GridViewRevisionChangeset) -> FlowyResult<()> { - let GridViewRevisionChangeset { delta, md5 } = change; - let user_id = self.user_id.clone(); - let (base_rev_id, rev_id) = self.rev_manager.next_rev_id_pair(); - let delta_data = delta.json_bytes(); - let revision = Revision::new( - &self.rev_manager.object_id, - base_rev_id, - rev_id, - delta_data, - &user_id, - md5, - ); - let _ = self.rev_manager.add_local_revision(&revision).await?; - Ok(()) - } +async fn apply_change( + user_id: &str, + rev_manager: Arc, + change: GridViewRevisionChangeset, +) -> FlowyResult<()> { + let GridViewRevisionChangeset { delta, md5 } = change; + let (base_rev_id, rev_id) = rev_manager.next_rev_id_pair(); + let delta_data = delta.json_bytes(); + let revision = Revision::new(&rev_manager.object_id, base_rev_id, rev_id, delta_data, &user_id, md5); + let _ = rev_manager.add_local_revision(&revision).await?; + Ok(()) } struct GridViewRevisionCloudService { @@ -253,19 +257,82 @@ impl RevisionObjectBuilder for GridViewRevisionPadBuilder { } } -impl GroupConfigurationDelegate for Arc> { - fn get_group_configuration(&self, field_rev: Arc) -> AFFuture { - let view_pad = self.clone(); +struct GroupConfigurationReaderImpl(Arc>); + +impl GroupConfigurationReader for GroupConfigurationReaderImpl { + fn get_group_configuration(&self, field_rev: Arc) -> AFFuture> { + let view_pad = self.0.clone(); wrap_future(async move { - let grid_pad = view_pad.read().await; - let configurations = grid_pad.get_groups(&field_rev.id, &field_rev.ty); + let view_pad = view_pad.read().await; + let configurations = view_pad.get_groups(&field_rev.id, &field_rev.ty); match configurations { - None => default_group_configuration(&field_rev), - Some(mut configurations) => { - assert_eq!(configurations.len(), 1); - (&*configurations.pop().unwrap()).clone() + None => { + let default_configuration = default_group_configuration(&field_rev); + Arc::new(default_configuration) } + Some(configuration) => configuration, } }) } } + +struct GroupConfigurationWriterImpl { + user_id: String, + rev_manager: Arc, + view_pad: Arc>, +} + +impl GroupConfigurationWriter for GroupConfigurationWriterImpl { + fn save_group_configuration( + &self, + field_id: &str, + field_type: FieldTypeRevision, + configuration: Arc, + ) -> AFFuture> { + let user_id = self.user_id.clone(); + let rev_manager = self.rev_manager.clone(); + let view_pad = self.view_pad.clone(); + let field_id = field_id.to_owned(); + + wrap_future(async move { + let configuration = (&*configuration).clone(); + match view_pad + .write() + .await + .insert_group_configuration(&field_id, &field_type, configuration)? + { + None => Ok(()), + Some(changeset) => apply_change(&user_id, rev_manager, changeset).await, + } + }) + } +} + +pub fn make_grid_setting(view_pad: &GridViewRevisionPad, field_revs: &[Arc]) -> GridSettingPB { + let current_layout_type: GridLayout = view_pad.layout.clone().into(); + let filters_by_field_id = view_pad + .get_all_filters(field_revs) + .map(|filters_by_field_id| { + filters_by_field_id + .into_iter() + .map(|(k, v)| (k, v.into())) + .collect::>() + }) + .unwrap_or_default(); + let groups_by_field_id = view_pad + .get_all_groups(field_revs) + .map(|groups_by_field_id| { + groups_by_field_id + .into_iter() + .map(|(k, v)| (k, v.into())) + .collect::>() + }) + .unwrap_or_default(); + + GridSettingPB { + layouts: GridLayoutPB::all(), + current_layout_type, + filter_configuration_by_field_id: filters_by_field_id, + group_configuration_by_field_id: groups_by_field_id, + } +} 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 178befc10a..42c4ff1c65 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 @@ -1,4 +1,6 @@ -use crate::entities::{CreateRowParams, GridFilterConfiguration, GridSettingPB, RepeatedGridGroupPB, RowPB}; +use crate::entities::{ + CreateRowParams, GridFilterConfiguration, GridSettingChangesetParams, GridSettingPB, RepeatedGridGroupPB, RowPB, +}; use crate::manager::GridUser; use crate::services::grid_editor_task::GridServiceTaskScheduler; use crate::services::grid_view_editor::GridViewRevisionEditor; @@ -8,7 +10,6 @@ use flowy_error::FlowyResult; use flowy_grid_data_model::revision::{FieldRevision, RowChangeset, RowRevision}; use flowy_revision::disk::SQLiteGridViewRevisionPersistence; use flowy_revision::{RevisionCompactor, RevisionManager, RevisionPersistence, SQLiteRevisionSnapshotPersistence}; -use flowy_sync::entities::grid::GridSettingChangesetParams; use flowy_sync::entities::revision::Revision; use flowy_sync::util::make_text_delta_from_revisions; use lib_infra::future::AFFuture; diff --git a/frontend/rust-lib/flowy-grid/src/services/group/action.rs b/frontend/rust-lib/flowy-grid/src/services/group/action.rs new file mode 100644 index 0000000000..a495fe0a1f --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/group/action.rs @@ -0,0 +1,23 @@ +use crate::entities::GroupRowsChangesetPB; + +use flowy_grid_data_model::revision::{FieldRevision, RowChangeset, RowRevision}; + +pub trait GroupAction: Send + Sync { + type CellDataType; + fn can_group(&self, content: &str, cell_data: &Self::CellDataType) -> bool; + fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec; + fn remove_row_if_match( + &mut self, + row_rev: &RowRevision, + cell_data: &Self::CellDataType, + ) -> Vec; + + fn move_row_if_match( + &mut self, + field_rev: &FieldRevision, + row_rev: &RowRevision, + row_changeset: &mut RowChangeset, + cell_data: &Self::CellDataType, + to_row_id: &str, + ) -> Vec; +} diff --git a/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs b/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs new file mode 100644 index 0000000000..3d737c307f --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs @@ -0,0 +1,76 @@ +use flowy_error::FlowyResult; +use flowy_grid_data_model::revision::{ + FieldRevision, FieldTypeRevision, GroupConfigurationContentSerde, GroupConfigurationRevision, GroupRecordRevision, +}; +use lib_infra::future::AFFuture; +use std::sync::Arc; + +pub trait GroupConfigurationReader: Send + Sync + 'static { + fn get_group_configuration(&self, field_rev: Arc) -> AFFuture>; +} + +pub trait GroupConfigurationWriter: Send + Sync + 'static { + fn save_group_configuration( + &self, + field_id: &str, + field_type: FieldTypeRevision, + configuration: Arc, + ) -> AFFuture>; +} + +pub trait GroupConfigurationAction: Send + Sync { + fn group_records(&self) -> &[GroupRecordRevision]; + fn save_groups(&self) -> FlowyResult<()>; + fn hide_group(&self, group_id: &str) -> FlowyResult<()>; + fn show_group(&self, group_id: &str) -> FlowyResult<()>; +} + +pub struct GenericGroupConfiguration { + field_rev: Arc, + reader: Arc, + writer: Arc, + configuration: C, +} + +impl GenericGroupConfiguration +where + C: GroupConfigurationContentSerde, +{ + pub async fn new( + field_rev: Arc, + reader: Arc, + writer: Arc, + ) -> FlowyResult { + let configuration = reader.get_group_configuration(field_rev.clone()).await; + let configuration = C::from_configuration(&configuration.content)?; + Ok(Self { + field_rev, + reader, + writer, + configuration, + }) + } +} + +impl GroupConfigurationReader for Arc +where + T: GroupConfigurationReader, +{ + fn get_group_configuration(&self, field_rev: Arc) -> AFFuture> { + (**self).get_group_configuration(field_rev) + } +} + +impl GroupConfigurationWriter for Arc +where + T: GroupConfigurationWriter, +{ + fn save_group_configuration( + &self, + field_id: &str, + field_type: FieldTypeRevision, + configuration: Arc, + ) -> AFFuture> { + (**self).save_group_configuration(field_id, field_type, configuration) + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controllers/checkbox_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs similarity index 59% rename from frontend/rust-lib/flowy-grid/src/services/group/controllers/checkbox_controller.rs rename to frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs index c1284dd659..fca49513c8 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controllers/checkbox_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs @@ -1,20 +1,42 @@ -use crate::entities::{CheckboxGroupConfigurationPB, GroupRowsChangesetPB}; - -use flowy_grid_data_model::revision::{FieldRevision, RowChangeset, RowRevision}; - +use crate::entities::GroupRowsChangesetPB; use crate::services::field::{CheckboxCellData, CheckboxCellDataParser, CheckboxTypeOptionPB, CHECK, UNCHECK}; -use crate::services::group::{GenericGroupController, Group, GroupController, GroupGenerator, Groupable}; +use crate::services::group::action::GroupAction; +use crate::services::group::configuration::{GenericGroupConfiguration, GroupConfigurationAction}; +use crate::services::group::entities::Group; +use crate::services::group::group_controller::{GenericGroupController, GroupController, GroupGenerator}; +use flowy_error::FlowyResult; +use flowy_grid_data_model::revision::{ + CheckboxGroupConfigurationRevision, FieldRevision, GroupRecordRevision, RowChangeset, RowRevision, +}; pub type CheckboxGroupController = GenericGroupController< - CheckboxGroupConfigurationPB, + CheckboxGroupConfigurationRevision, CheckboxTypeOptionPB, CheckboxGroupGenerator, CheckboxCellDataParser, >; -impl Groupable for CheckboxGroupController { - type CellDataType = CheckboxCellData; +pub type CheckboxGroupConfiguration = GenericGroupConfiguration; +impl GroupConfigurationAction for CheckboxGroupConfiguration { + fn group_records(&self) -> &[GroupRecordRevision] { + todo!() + } + fn save_groups(&self) -> FlowyResult<()> { + todo!() + } + + fn hide_group(&self, group_id: &str) -> FlowyResult<()> { + todo!() + } + + fn show_group(&self, group_id: &str) -> FlowyResult<()> { + todo!() + } +} + +impl GroupAction for CheckboxGroupController { + type CellDataType = CheckboxCellData; fn can_group(&self, _content: &str, _cell_data: &Self::CellDataType) -> bool { false } @@ -55,13 +77,13 @@ impl GroupController for CheckboxGroupController { pub struct CheckboxGroupGenerator(); impl GroupGenerator for CheckboxGroupGenerator { - type ConfigurationType = CheckboxGroupConfigurationPB; + type ConfigurationType = CheckboxGroupConfiguration; type TypeOptionType = CheckboxTypeOptionPB; fn generate_groups( field_id: &str, - _configuration: &Option, - _type_option: &Option, + configuration: &Self::ConfigurationType, + type_option: &Option, ) -> Vec { let check_group = Group::new( "true".to_string(), diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controllers/mod.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/mod.rs similarity index 71% rename from frontend/rust-lib/flowy-grid/src/services/group/controllers/mod.rs rename to frontend/rust-lib/flowy-grid/src/services/group/controller_impls/mod.rs index 29ce5d091e..974f311a48 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controllers/mod.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/mod.rs @@ -1,7 +1,5 @@ mod checkbox_controller; -mod group_controller; mod select_option_controller; pub use checkbox_controller::*; -pub use group_controller::*; pub use select_option_controller::*; diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/mod.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/mod.rs new file mode 100644 index 0000000000..a2f393d093 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/mod.rs @@ -0,0 +1,6 @@ +mod multi_select_controller; +mod single_select_controller; +mod util; + +pub use multi_select_controller::*; +pub use single_select_controller::*; diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs new file mode 100644 index 0000000000..33cd0613cd --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs @@ -0,0 +1,110 @@ +use crate::entities::GroupRowsChangesetPB; +use crate::services::cell::insert_select_option_cell; +use crate::services::field::{MultiSelectTypeOptionPB, SelectOptionCellDataPB, SelectOptionCellDataParser}; +use crate::services::group::action::GroupAction; + +use crate::services::group::controller_impls::select_option_controller::util::*; +use crate::services::group::entities::Group; +use crate::services::group::group_controller::{GenericGroupController, GroupController, GroupGenerator}; +use flowy_grid_data_model::revision::{ + FieldRevision, RowChangeset, RowRevision, SelectOptionGroupConfigurationRevision, +}; + +// MultiSelect +pub type MultiSelectGroupController = GenericGroupController< + SelectOptionGroupConfigurationRevision, + MultiSelectTypeOptionPB, + MultiSelectGroupGenerator, + SelectOptionCellDataParser, +>; + +impl GroupAction for MultiSelectGroupController { + type CellDataType = SelectOptionCellDataPB; + + fn can_group(&self, content: &str, cell_data: &SelectOptionCellDataPB) -> bool { + cell_data.select_options.iter().any(|option| option.id == content) + } + + fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec { + let mut changesets = vec![]; + self.groups_map.iter_mut().for_each(|(_, group): (_, &mut Group)| { + add_row(group, &mut changesets, cell_data, row_rev); + }); + changesets + } + + fn remove_row_if_match( + &mut self, + row_rev: &RowRevision, + cell_data: &Self::CellDataType, + ) -> Vec { + let mut changesets = vec![]; + self.groups_map.iter_mut().for_each(|(_, group): (_, &mut Group)| { + remove_row(group, &mut changesets, cell_data, row_rev); + }); + changesets + } + + fn move_row_if_match( + &mut self, + field_rev: &FieldRevision, + row_rev: &RowRevision, + row_changeset: &mut RowChangeset, + cell_data: &Self::CellDataType, + to_row_id: &str, + ) -> Vec { + let mut group_changeset = vec![]; + self.groups_map.iter_mut().for_each(|(_, group): (_, &mut Group)| { + move_row( + group, + &mut group_changeset, + field_rev, + row_rev, + row_changeset, + cell_data, + to_row_id, + ); + }); + group_changeset + } +} + +impl GroupController for MultiSelectGroupController { + fn will_create_row(&mut self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str) { + let group: Option<&Group> = self.groups_map.get(group_id); + match group { + None => tracing::warn!("Can not find the group: {}", group_id), + Some(group) => { + let cell_rev = insert_select_option_cell(group.id.clone(), field_rev); + row_rev.cells.insert(field_rev.id.clone(), cell_rev); + } + } + } +} + +pub struct MultiSelectGroupGenerator(); +impl GroupGenerator for MultiSelectGroupGenerator { + type ConfigurationType = SelectOptionGroupConfiguration; + type TypeOptionType = MultiSelectTypeOptionPB; + fn generate_groups( + field_id: &str, + configuration: &Self::ConfigurationType, + type_option: &Option, + ) -> Vec { + match type_option { + None => vec![], + Some(type_option) => type_option + .options + .iter() + .map(|option| { + Group::new( + option.id.clone(), + field_id.to_owned(), + option.name.clone(), + option.id.clone(), + ) + }) + .collect(), + } + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs new file mode 100644 index 0000000000..4c8360f427 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs @@ -0,0 +1,111 @@ +use crate::entities::{GroupRowsChangesetPB, RowPB}; +use crate::services::cell::insert_select_option_cell; +use crate::services::field::{SelectOptionCellDataPB, SelectOptionCellDataParser, SingleSelectTypeOptionPB}; +use crate::services::group::action::GroupAction; + +use crate::services::group::controller_impls::select_option_controller::util::*; +use crate::services::group::entities::Group; +use crate::services::group::group_controller::{GenericGroupController, GroupController, GroupGenerator}; + +use flowy_grid_data_model::revision::{ + FieldRevision, RowChangeset, RowRevision, SelectOptionGroupConfigurationRevision, +}; + +// SingleSelect +pub type SingleSelectGroupController = GenericGroupController< + SelectOptionGroupConfigurationRevision, + SingleSelectTypeOptionPB, + SingleSelectGroupGenerator, + SelectOptionCellDataParser, +>; + +impl GroupAction for SingleSelectGroupController { + type CellDataType = SelectOptionCellDataPB; + fn can_group(&self, content: &str, cell_data: &SelectOptionCellDataPB) -> bool { + cell_data.select_options.iter().any(|option| option.id == content) + } + + fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec { + let mut changesets = vec![]; + self.groups_map.iter_mut().for_each(|(_, group): (_, &mut Group)| { + add_row(group, &mut changesets, cell_data, row_rev); + }); + changesets + } + + fn remove_row_if_match( + &mut self, + row_rev: &RowRevision, + cell_data: &Self::CellDataType, + ) -> Vec { + let mut changesets = vec![]; + self.groups_map.iter_mut().for_each(|(_, group): (_, &mut Group)| { + remove_row(group, &mut changesets, cell_data, row_rev); + }); + changesets + } + + fn move_row_if_match( + &mut self, + field_rev: &FieldRevision, + row_rev: &RowRevision, + row_changeset: &mut RowChangeset, + cell_data: &Self::CellDataType, + to_row_id: &str, + ) -> Vec { + let mut group_changeset = vec![]; + self.groups_map.iter_mut().for_each(|(_, group): (_, &mut Group)| { + move_row( + group, + &mut group_changeset, + field_rev, + row_rev, + row_changeset, + cell_data, + to_row_id, + ); + }); + group_changeset + } +} + +impl GroupController for SingleSelectGroupController { + fn will_create_row(&mut self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str) { + let group: Option<&mut Group> = self.groups_map.get_mut(group_id); + match group { + None => {} + Some(group) => { + let cell_rev = insert_select_option_cell(group.id.clone(), field_rev); + row_rev.cells.insert(field_rev.id.clone(), cell_rev); + group.add_row(RowPB::from(row_rev)); + } + } + } +} + +pub struct SingleSelectGroupGenerator(); +impl GroupGenerator for SingleSelectGroupGenerator { + type ConfigurationType = SelectOptionGroupConfiguration; + type TypeOptionType = SingleSelectTypeOptionPB; + fn generate_groups( + field_id: &str, + configuration: &Self::ConfigurationType, + type_option: &Option, + ) -> Vec { + match type_option { + None => vec![], + Some(type_option) => type_option + .options + .iter() + .map(|option| { + Group::new( + option.id.clone(), + field_id.to_owned(), + option.name.clone(), + option.id.clone(), + ) + }) + .collect(), + } + } +} 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 new file mode 100644 index 0000000000..d95a39f60c --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs @@ -0,0 +1,108 @@ +use crate::entities::{GroupRowsChangesetPB, InsertedRowPB, RowPB}; +use crate::services::cell::insert_select_option_cell; +use crate::services::field::SelectOptionCellDataPB; +use crate::services::group::configuration::{GenericGroupConfiguration, GroupConfigurationAction}; +use crate::services::group::Group; +use flowy_error::FlowyResult; +use flowy_grid_data_model::revision::{ + FieldRevision, GroupRecordRevision, RowChangeset, RowRevision, SelectOptionGroupConfigurationRevision, +}; + +pub type SelectOptionGroupConfiguration = GenericGroupConfiguration; +impl GroupConfigurationAction for SelectOptionGroupConfiguration { + fn group_records(&self) -> &[GroupRecordRevision] { + todo!() + } + + fn save_groups(&self) -> FlowyResult<()> { + todo!() + } + + fn hide_group(&self, group_id: &str) -> FlowyResult<()> { + todo!() + } + + fn show_group(&self, group_id: &str) -> FlowyResult<()> { + todo!() + } +} + +pub fn add_row( + group: &mut Group, + changesets: &mut Vec, + cell_data: &SelectOptionCellDataPB, + row_rev: &RowRevision, +) { + 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); + changesets.push(GroupRowsChangesetPB::insert( + group.id.clone(), + vec![InsertedRowPB::new(row_pb.clone())], + )); + group.add_row(row_pb); + } + } else if group.contains_row(&row_rev.id) { + changesets.push(GroupRowsChangesetPB::delete(group.id.clone(), vec![row_rev.id.clone()])); + group.remove_row(&row_rev.id); + } + }); +} + +pub fn remove_row( + group: &mut Group, + changesets: &mut Vec, + cell_data: &SelectOptionCellDataPB, + row_rev: &RowRevision, +) { + cell_data.select_options.iter().for_each(|option| { + if option.id == group.id && group.contains_row(&row_rev.id) { + changesets.push(GroupRowsChangesetPB::delete(group.id.clone(), vec![row_rev.id.clone()])); + group.remove_row(&row_rev.id); + } + }); +} + +pub fn move_row( + group: &mut Group, + group_changeset: &mut Vec, + field_rev: &FieldRevision, + row_rev: &RowRevision, + row_changeset: &mut RowChangeset, + cell_data: &SelectOptionCellDataPB, + to_row_id: &str, +) { + cell_data.select_options.iter().for_each(|option| { + // Remove the row in which group contains the row + let is_group_contains = group.contains_row(&row_rev.id); + let to_index = group.index_of_row(to_row_id); + + if option.id == group.id && is_group_contains { + group_changeset.push(GroupRowsChangesetPB::delete(group.id.clone(), vec![row_rev.id.clone()])); + group.remove_row(&row_rev.id); + } + + // Find the inserted group + if let Some(to_index) = to_index { + let row_pb = RowPB::from(row_rev); + let inserted_row = InsertedRowPB { + row: row_pb.clone(), + index: Some(to_index as i32), + }; + group_changeset.push(GroupRowsChangesetPB::insert(group.id.clone(), vec![inserted_row])); + if group.number_of_row() == to_index { + group.add_row(row_pb); + } else { + group.insert_row(to_index, row_pb); + } + } + + // If the inserted row comes from other group, it needs to update the corresponding cell content. + if to_index.is_some() && option.id != group.id { + // Update the corresponding row's cell content. + let cell_rev = insert_select_option_cell(group.id.clone(), field_rev); + row_changeset.cell_by_field_id.insert(field_rev.id.clone(), cell_rev); + } + }); +} diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controllers/select_option_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controllers/select_option_controller.rs deleted file mode 100644 index 29dc95891f..0000000000 --- a/frontend/rust-lib/flowy-grid/src/services/group/controllers/select_option_controller.rs +++ /dev/null @@ -1,286 +0,0 @@ -use crate::entities::{GroupRowsChangesetPB, InsertedRowPB, RowPB, SelectOptionGroupConfigurationPB}; -use crate::services::cell::insert_select_option_cell; -use crate::services::field::{ - MultiSelectTypeOptionPB, SelectOptionCellDataPB, SelectOptionCellDataParser, SingleSelectTypeOptionPB, -}; -use crate::services::group::{GenericGroupController, Group, GroupController, GroupGenerator, Groupable}; -use flowy_grid_data_model::revision::{FieldRevision, RowChangeset, RowRevision}; - -// SingleSelect -pub type SingleSelectGroupController = GenericGroupController< - SelectOptionGroupConfigurationPB, - SingleSelectTypeOptionPB, - SingleSelectGroupGenerator, - SelectOptionCellDataParser, ->; - -impl Groupable for SingleSelectGroupController { - type CellDataType = SelectOptionCellDataPB; - fn can_group(&self, content: &str, cell_data: &SelectOptionCellDataPB) -> bool { - cell_data.select_options.iter().any(|option| option.id == content) - } - - fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec { - let mut changesets = vec![]; - self.groups_map.iter_mut().for_each(|(_, group): (_, &mut Group)| { - add_row(group, &mut changesets, cell_data, row_rev); - }); - changesets - } - - fn remove_row_if_match( - &mut self, - row_rev: &RowRevision, - cell_data: &Self::CellDataType, - ) -> Vec { - let mut changesets = vec![]; - self.groups_map.iter_mut().for_each(|(_, group): (_, &mut Group)| { - remove_row(group, &mut changesets, cell_data, row_rev); - }); - changesets - } - - fn move_row_if_match( - &mut self, - field_rev: &FieldRevision, - row_rev: &RowRevision, - row_changeset: &mut RowChangeset, - cell_data: &Self::CellDataType, - to_row_id: &str, - ) -> Vec { - let mut group_changeset = vec![]; - self.groups_map.iter_mut().for_each(|(_, group): (_, &mut Group)| { - move_row( - group, - &mut group_changeset, - field_rev, - row_rev, - row_changeset, - cell_data, - to_row_id, - ); - }); - group_changeset - } -} - -impl GroupController for SingleSelectGroupController { - fn will_create_row(&mut self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str) { - let group: Option<&mut Group> = self.groups_map.get_mut(group_id); - match group { - None => {} - Some(group) => { - let cell_rev = insert_select_option_cell(group.id.clone(), field_rev); - row_rev.cells.insert(field_rev.id.clone(), cell_rev); - group.add_row(RowPB::from(row_rev)); - } - } - } -} - -pub struct SingleSelectGroupGenerator(); -impl GroupGenerator for SingleSelectGroupGenerator { - type ConfigurationType = SelectOptionGroupConfigurationPB; - type TypeOptionType = SingleSelectTypeOptionPB; - fn generate_groups( - field_id: &str, - _configuration: &Option, - type_option: &Option, - ) -> Vec { - match type_option { - None => vec![], - Some(type_option) => type_option - .options - .iter() - .map(|option| { - Group::new( - option.id.clone(), - field_id.to_owned(), - option.name.clone(), - option.id.clone(), - ) - }) - .collect(), - } - } -} - -// MultiSelect -pub type MultiSelectGroupController = GenericGroupController< - SelectOptionGroupConfigurationPB, - MultiSelectTypeOptionPB, - MultiSelectGroupGenerator, - SelectOptionCellDataParser, ->; - -impl Groupable for MultiSelectGroupController { - type CellDataType = SelectOptionCellDataPB; - - fn can_group(&self, content: &str, cell_data: &SelectOptionCellDataPB) -> bool { - cell_data.select_options.iter().any(|option| option.id == content) - } - - fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec { - let mut changesets = vec![]; - self.groups_map.iter_mut().for_each(|(_, group): (_, &mut Group)| { - add_row(group, &mut changesets, cell_data, row_rev); - }); - changesets - } - - fn remove_row_if_match( - &mut self, - row_rev: &RowRevision, - cell_data: &Self::CellDataType, - ) -> Vec { - let mut changesets = vec![]; - self.groups_map.iter_mut().for_each(|(_, group): (_, &mut Group)| { - remove_row(group, &mut changesets, cell_data, row_rev); - }); - changesets - } - - fn move_row_if_match( - &mut self, - field_rev: &FieldRevision, - row_rev: &RowRevision, - row_changeset: &mut RowChangeset, - cell_data: &Self::CellDataType, - to_row_id: &str, - ) -> Vec { - let mut group_changeset = vec![]; - self.groups_map.iter_mut().for_each(|(_, group): (_, &mut Group)| { - move_row( - group, - &mut group_changeset, - field_rev, - row_rev, - row_changeset, - cell_data, - to_row_id, - ); - }); - group_changeset - } -} - -impl GroupController for MultiSelectGroupController { - fn will_create_row(&mut self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str) { - let group: Option<&Group> = self.groups_map.get(group_id); - match group { - None => tracing::warn!("Can not find the group: {}", group_id), - Some(group) => { - let cell_rev = insert_select_option_cell(group.id.clone(), field_rev); - row_rev.cells.insert(field_rev.id.clone(), cell_rev); - } - } - } -} - -pub struct MultiSelectGroupGenerator(); -impl GroupGenerator for MultiSelectGroupGenerator { - type ConfigurationType = SelectOptionGroupConfigurationPB; - type TypeOptionType = MultiSelectTypeOptionPB; - - fn generate_groups( - field_id: &str, - _configuration: &Option, - type_option: &Option, - ) -> Vec { - match type_option { - None => vec![], - Some(type_option) => type_option - .options - .iter() - .map(|option| { - Group::new( - option.id.clone(), - field_id.to_owned(), - option.name.clone(), - option.id.clone(), - ) - }) - .collect(), - } - } -} - -fn add_row( - group: &mut Group, - changesets: &mut Vec, - cell_data: &SelectOptionCellDataPB, - row_rev: &RowRevision, -) { - 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); - changesets.push(GroupRowsChangesetPB::insert( - group.id.clone(), - vec![InsertedRowPB::new(row_pb.clone())], - )); - group.add_row(row_pb); - } - } else if group.contains_row(&row_rev.id) { - changesets.push(GroupRowsChangesetPB::delete(group.id.clone(), vec![row_rev.id.clone()])); - group.remove_row(&row_rev.id); - } - }); -} - -fn remove_row( - group: &mut Group, - changesets: &mut Vec, - cell_data: &SelectOptionCellDataPB, - row_rev: &RowRevision, -) { - cell_data.select_options.iter().for_each(|option| { - if option.id == group.id && group.contains_row(&row_rev.id) { - changesets.push(GroupRowsChangesetPB::delete(group.id.clone(), vec![row_rev.id.clone()])); - group.remove_row(&row_rev.id); - } - }); -} - -fn move_row( - group: &mut Group, - group_changeset: &mut Vec, - field_rev: &FieldRevision, - row_rev: &RowRevision, - row_changeset: &mut RowChangeset, - cell_data: &SelectOptionCellDataPB, - to_row_id: &str, -) { - cell_data.select_options.iter().for_each(|option| { - // Remove the row in which group contains the row - let is_group_contains = group.contains_row(&row_rev.id); - let to_index = group.index_of_row(to_row_id); - - if option.id == group.id && is_group_contains { - group_changeset.push(GroupRowsChangesetPB::delete(group.id.clone(), vec![row_rev.id.clone()])); - group.remove_row(&row_rev.id); - } - - // Find the inserted group - if let Some(to_index) = to_index { - let row_pb = RowPB::from(row_rev); - let inserted_row = InsertedRowPB { - row: row_pb.clone(), - index: Some(to_index as i32), - }; - group_changeset.push(GroupRowsChangesetPB::insert(group.id.clone(), vec![inserted_row])); - if group.number_of_row() == to_index { - group.add_row(row_pb); - } else { - group.insert_row(to_index, row_pb); - } - } - - // If the inserted row comes from other group, it needs to update the corresponding cell content. - if to_index.is_some() && option.id != group.id { - // Update the corresponding row's cell content. - let cell_rev = insert_select_option_cell(group.id.clone(), field_rev); - row_changeset.cell_by_field_id.insert(field_rev.id.clone(), cell_rev); - } - }); -} diff --git a/frontend/rust-lib/flowy-grid/src/services/group/entities.rs b/frontend/rust-lib/flowy-grid/src/services/group/entities.rs new file mode 100644 index 0000000000..dd4171afb0 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/group/entities.rs @@ -0,0 +1,73 @@ +use crate::entities::{GroupPB, RowPB}; + +#[derive(Clone)] +pub struct Group { + pub id: String, + pub field_id: String, + pub desc: String, + rows: Vec, + + /// [content] is used to determine which group the cell belongs to. + pub content: String, +} + +impl std::convert::From for GroupPB { + fn from(group: Group) -> Self { + Self { + field_id: group.field_id, + group_id: group.id, + desc: group.desc, + rows: group.rows, + } + } +} + +impl Group { + pub fn new(id: String, field_id: String, desc: String, content: String) -> Self { + Self { + id, + field_id, + desc, + rows: vec![], + content, + } + } + + pub fn contains_row(&self, row_id: &str) -> bool { + self.rows.iter().any(|row| row.id == row_id) + } + + pub fn remove_row(&mut self, row_id: &str) { + match self.rows.iter().position(|row| row.id == row_id) { + None => {} + Some(pos) => { + self.rows.remove(pos); + } + } + } + + pub fn add_row(&mut self, row_pb: RowPB) { + match self.rows.iter().find(|row| row.id == row_pb.id) { + None => { + self.rows.push(row_pb); + } + Some(_) => {} + } + } + + pub fn insert_row(&mut self, index: usize, row_pb: RowPB) { + if index < self.rows.len() { + self.rows.insert(index, row_pb); + } else { + tracing::error!("Insert row index:{} beyond the bounds:{},", index, self.rows.len()); + } + } + + pub fn index_of_row(&self, row_id: &str) -> Option { + self.rows.iter().position(|row| row.id == row_id) + } + + pub fn number_of_row(&self) -> usize { + self.rows.len() + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controllers/group_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_controller.rs similarity index 57% rename from frontend/rust-lib/flowy-grid/src/services/group/controllers/group_controller.rs rename to frontend/rust-lib/flowy-grid/src/services/group/group_controller.rs index 07dcff3f77..fb2c0aaa6e 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controllers/group_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_controller.rs @@ -1,54 +1,43 @@ -use crate::entities::{GroupPB, GroupRowsChangesetPB, RowPB}; +use crate::entities::{GroupRowsChangesetPB, RowPB}; use crate::services::cell::{decode_any_cell_data, CellBytesParser}; -use bytes::Bytes; +use crate::services::group::action::GroupAction; +use crate::services::group::configuration::{GenericGroupConfiguration, GroupConfigurationReader}; +use crate::services::group::entities::Group; use flowy_error::FlowyResult; use flowy_grid_data_model::revision::{ - FieldRevision, GroupConfigurationRevision, RowChangeset, RowRevision, TypeOptionDataDeserializer, + FieldRevision, GroupConfigurationContentSerde, GroupConfigurationRevision, RowChangeset, RowRevision, + TypeOptionDataDeserializer, }; use indexmap::IndexMap; +use lib_infra::future::AFFuture; 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. +pub trait GroupController: GroupControllerSharedAction + Send + Sync { + fn will_create_row(&mut self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str); +} + pub trait GroupGenerator { type ConfigurationType; type TypeOptionType; fn generate_groups( field_id: &str, - configuration: &Option, + configuration: &Self::ConfigurationType, type_option: &Option, ) -> Vec; } -pub trait Groupable: Send + Sync { - type CellDataType; - fn can_group(&self, content: &str, cell_data: &Self::CellDataType) -> bool; - fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec; - fn remove_row_if_match( - &mut self, - row_rev: &RowRevision, - cell_data: &Self::CellDataType, - ) -> Vec; - - fn move_row_if_match( - &mut self, - field_rev: &FieldRevision, - row_rev: &RowRevision, - row_changeset: &mut RowChangeset, - cell_data: &Self::CellDataType, - to_row_id: &str, - ) -> Vec; -} - -pub trait GroupController: GroupControllerSharedAction + Send + Sync { - fn will_create_row(&mut self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str); -} - +// Defines the shared actions each group controller can perform. pub trait GroupControllerSharedAction: Send + Sync { // The field that is used for grouping the rows fn field_id(&self) -> &str; - fn groups(&self) -> Vec; - fn group_rows(&mut self, row_revs: &[Arc], field_rev: &FieldRevision) -> FlowyResult<()>; + fn fill_groups(&mut self, row_revs: &[Arc], field_rev: &FieldRevision) -> FlowyResult>; fn did_update_row( &mut self, row_rev: &RowRevision, @@ -70,105 +59,35 @@ pub trait GroupControllerSharedAction: Send + Sync { ) -> FlowyResult>; } -const DEFAULT_GROUP_ID: &str = "default_group"; - -/// C: represents the group configuration structure +/// C: represents the group configuration that impl [GroupConfigurationSerde] /// T: the type option data deserializer that impl [TypeOptionDataDeserializer] /// G: the group generator, [GroupGenerator] /// P: the parser that impl [CellBytesParser] for the CellBytes pub struct GenericGroupController { pub field_id: String, pub groups_map: IndexMap, + + /// default_group is used to store the rows that don't belong to any groups. default_group: Group, pub type_option: Option, - pub configuration: Option, + pub configuration: GenericGroupConfiguration, group_action_phantom: PhantomData, cell_parser_phantom: PhantomData

, } -#[derive(Clone)] -pub struct Group { - pub id: String, - pub field_id: String, - pub desc: String, - rows: Vec, - pub content: String, -} - -impl std::convert::From for GroupPB { - fn from(group: Group) -> Self { - Self { - field_id: group.field_id, - group_id: group.id, - desc: group.desc, - rows: group.rows, - } - } -} - -impl Group { - pub fn new(id: String, field_id: String, desc: String, content: String) -> Self { - Self { - id, - field_id, - desc, - rows: vec![], - content, - } - } - - pub fn contains_row(&self, row_id: &str) -> bool { - self.rows.iter().any(|row| row.id == row_id) - } - - pub fn remove_row(&mut self, row_id: &str) { - match self.rows.iter().position(|row| row.id == row_id) { - None => {} - Some(pos) => { - self.rows.remove(pos); - } - } - } - - pub fn add_row(&mut self, row_pb: RowPB) { - match self.rows.iter().find(|row| row.id == row_pb.id) { - None => { - self.rows.push(row_pb); - } - Some(_) => {} - } - } - - pub fn insert_row(&mut self, index: usize, row_pb: RowPB) { - if index < self.rows.len() { - self.rows.insert(index, row_pb); - } else { - tracing::error!("Insert row index:{} beyond the bounds:{},", index, self.rows.len()); - } - } - - pub fn index_of_row(&self, row_id: &str) -> Option { - self.rows.iter().position(|row| row.id == row_id) - } - - pub fn number_of_row(&self) -> usize { - self.rows.len() - } -} - impl GenericGroupController where - C: TryFrom, + C: GroupConfigurationContentSerde, T: TypeOptionDataDeserializer, - G: GroupGenerator, + G: GroupGenerator, TypeOptionType = T>, { - pub fn new(field_rev: &Arc, configuration: GroupConfigurationRevision) -> FlowyResult { - // let configuration = match configuration.content { - // None => None, - // Some(content) => Some(C::try_from(Bytes::from(content))?), - // }; - - let configuration = None; + pub async fn new( + field_rev: &Arc, + configuration_reader: Arc, + configuration_writer: Arc, + ) -> FlowyResult { + let configuration = + GenericGroupConfiguration::::new(field_rev.clone(), configuration_reader, configuration_writer).await?; let field_type_rev = field_rev.ty; let type_option = field_rev.get_type_option_entry::(field_type_rev); let groups = G::generate_groups(&field_rev.id, &configuration, &type_option); @@ -195,55 +114,52 @@ where impl GroupControllerSharedAction for GenericGroupController where P: CellBytesParser, - Self: Groupable, + Self: GroupAction, { fn field_id(&self) -> &str { &self.field_id } - fn groups(&self) -> Vec { - let default_group = self.default_group.clone(); - let mut groups: Vec = self.groups_map.values().cloned().collect(); - if !default_group.rows.is_empty() { - groups.push(default_group); - } - groups - } - - fn group_rows(&mut self, row_revs: &[Arc], field_rev: &FieldRevision) -> FlowyResult<()> { - if self.configuration.is_none() { - return Ok(()); - } + fn fill_groups(&mut self, row_revs: &[Arc], field_rev: &FieldRevision) -> FlowyResult> { + // if self.configuration.is_none() { + // return Ok(vec![]); + // } for row_rev in row_revs { if let Some(cell_rev) = row_rev.cells.get(&self.field_id) { - let mut records: Vec = vec![]; + let mut group_rows: Vec = vec![]; let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), field_rev); let cell_data = cell_bytes.parser::

()?; for group in self.groups_map.values() { if self.can_group(&group.content, &cell_data) { - records.push(GroupRecord { + group_rows.push(GroupRow { row: row_rev.into(), group_id: group.id.clone(), }); } } - if records.is_empty() { - self.default_group.rows.push(row_rev.into()); + if group_rows.is_empty() { + self.default_group.add_row(row_rev.into()); } else { - for record in records { - if let Some(group) = self.groups_map.get_mut(&record.group_id) { - group.rows.push(record.row); + for group_row in group_rows { + if let Some(group) = self.groups_map.get_mut(&group_row.group_id) { + group.add_row(group_row.row); } } } } else { - self.default_group.rows.push(row_rev.into()); + self.default_group.add_row(row_rev.into()); } } - Ok(()) + let default_group = self.default_group.clone(); + let mut groups: Vec = self.groups_map.values().cloned().collect(); + if !default_group.number_of_row() == 0 { + groups.push(default_group); + } + + Ok(groups) } fn did_update_row( @@ -292,7 +208,7 @@ where } } -struct GroupRecord { +struct GroupRow { row: RowPB, group_id: String, } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs index c07b3acec4..42e687445d 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs @@ -2,44 +2,48 @@ use crate::entities::{ CheckboxGroupConfigurationPB, DateGroupConfigurationPB, FieldType, GroupRowsChangesetPB, NumberGroupConfigurationPB, SelectOptionGroupConfigurationPB, TextGroupConfigurationPB, UrlGroupConfigurationPB, }; +use crate::services::group::configuration::GroupConfigurationReader; +use crate::services::group::group_controller::GroupController; use crate::services::group::{ - CheckboxGroupController, Group, GroupController, MultiSelectGroupController, SingleSelectGroupController, + CheckboxGroupController, Group, GroupConfigurationWriter, MultiSelectGroupController, SingleSelectGroupController, }; -use bytes::Bytes; use flowy_error::FlowyResult; use flowy_grid_data_model::revision::{ - gen_grid_group_id, CheckboxGroupConfigurationRevision, DateGroupConfigurationRevision, FieldRevision, - GroupConfigurationRevision, NumberGroupConfigurationRevision, RowChangeset, RowRevision, - SelectOptionGroupConfigurationRevision, TextGroupConfigurationRevision, UrlGroupConfigurationRevision, + CheckboxGroupConfigurationRevision, DateGroupConfigurationRevision, FieldRevision, GroupConfigurationRevision, + NumberGroupConfigurationRevision, RowChangeset, RowRevision, SelectOptionGroupConfigurationRevision, + TextGroupConfigurationRevision, UrlGroupConfigurationRevision, }; -use lib_infra::future::AFFuture; + use std::future::Future; use std::sync::Arc; use tokio::sync::RwLock; -pub trait GroupConfigurationDelegate: Send + Sync + 'static { - fn get_group_configuration(&self, field_rev: Arc) -> AFFuture; -} - pub(crate) struct GroupService { - delegate: Box, + configuration_reader: Arc, + configuration_writer: Arc, group_controller: Option>>, } impl GroupService { - pub(crate) async fn new(delegate: Box) -> Self { + pub(crate) async fn new(configuration_reader: R, configuration_writer: W) -> Self + where + R: GroupConfigurationReader, + W: GroupConfigurationWriter, + { Self { - delegate, + configuration_reader: Arc::new(configuration_reader), + configuration_writer: Arc::new(configuration_writer), group_controller: None, } } pub(crate) async fn groups(&self) -> Vec { - if let Some(group_action_handler) = self.group_controller.as_ref() { - group_action_handler.read().await.groups() - } else { - vec![] - } + // if let Some(group_action_handler) = self.group_controller.as_ref() { + // group_action_handler.read().await.groups() + // } else { + // vec![] + // } + todo!() } pub(crate) async fn load_groups( @@ -49,13 +53,25 @@ impl GroupService { ) -> Option> { let field_rev = find_group_field(field_revs)?; let field_type: FieldType = field_rev.ty.into(); - let configuration = self.delegate.get_group_configuration(field_rev.clone()).await; - match self - .build_groups(&field_type, &field_rev, row_revs, configuration) - .await - { - Ok(groups) => Some(groups), - Err(_) => None, + match self.make_group_controller(&field_type, &field_rev).await { + Ok(group_controller) => { + self.group_controller = group_controller; + let mut groups = vec![]; + if let Some(group_action_handler) = self.group_controller.as_ref() { + let mut write_guard = group_action_handler.write().await; + groups = match write_guard.fill_groups(&row_revs, &field_rev) { + Ok(groups) => groups, + Err(e) => { + tracing::error!("Fill groups failed:{:?}", e); + vec![] + } + }; + + drop(write_guard); + } + Some(groups) + } + Err(_) => Some(vec![]), } } @@ -151,14 +167,13 @@ impl GroupService { } } - #[tracing::instrument(level = "trace", skip_all, err)] - async fn build_groups( + #[tracing::instrument(level = "trace", skip_all)] + async fn make_group_controller( &mut self, field_type: &FieldType, field_rev: &Arc, - row_revs: Vec>, - configuration: GroupConfigurationRevision, - ) -> FlowyResult> { + ) -> FlowyResult>>> { + let mut group_controller: Option>> = None; match field_type { FieldType::RichText => { // let generator = GroupGenerator::::from_configuration(configuration); @@ -170,31 +185,37 @@ impl GroupService { // let generator = GroupGenerator::::from_configuration(configuration); } FieldType::SingleSelect => { - let controller = SingleSelectGroupController::new(field_rev, configuration)?; - self.group_controller = Some(Arc::new(RwLock::new(controller))); + let controller = SingleSelectGroupController::new( + field_rev, + self.configuration_reader.clone(), + self.configuration_writer.clone(), + ) + .await?; + group_controller = Some(Arc::new(RwLock::new(controller))); } FieldType::MultiSelect => { - let controller = MultiSelectGroupController::new(field_rev, configuration)?; - self.group_controller = Some(Arc::new(RwLock::new(controller))); + let controller = MultiSelectGroupController::new( + field_rev, + self.configuration_reader.clone(), + self.configuration_writer.clone(), + ) + .await?; + group_controller = Some(Arc::new(RwLock::new(controller))); } FieldType::Checkbox => { - let controller = CheckboxGroupController::new(field_rev, configuration)?; - self.group_controller = Some(Arc::new(RwLock::new(controller))); + let controller = CheckboxGroupController::new( + field_rev, + self.configuration_reader.clone(), + self.configuration_writer.clone(), + ) + .await?; + group_controller = Some(Arc::new(RwLock::new(controller))) } FieldType::URL => { // let generator = GroupGenerator::::from_configuration(configuration); } - }; - - let mut groups = vec![]; - if let Some(group_action_handler) = self.group_controller.as_ref() { - let mut write_guard = group_action_handler.write().await; - let _ = write_guard.group_rows(&row_revs, field_rev)?; - groups = write_guard.groups(); - drop(write_guard); } - - Ok(groups) + Ok(group_controller) } } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/mod.rs b/frontend/rust-lib/flowy-grid/src/services/group/mod.rs index f8b96c8bab..19536b8c5b 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/mod.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/mod.rs @@ -1,5 +1,11 @@ -mod controllers; +mod action; +mod configuration; +mod controller_impls; +mod entities; +mod group_controller; mod group_service; -pub(crate) use controllers::*; +pub(crate) use configuration::*; +pub(crate) use controller_impls::*; +pub(crate) use entities::*; pub(crate) use group_service::*; diff --git a/frontend/rust-lib/flowy-grid/src/services/setting/setting_builder.rs b/frontend/rust-lib/flowy-grid/src/services/setting/setting_builder.rs index 1cc16f7da7..16ee630cc7 100644 --- a/frontend/rust-lib/flowy-grid/src/services/setting/setting_builder.rs +++ b/frontend/rust-lib/flowy-grid/src/services/setting/setting_builder.rs @@ -1,11 +1,4 @@ -use crate::entities::{ - GridLayout, GridLayoutPB, GridSettingPB, RepeatedGridConfigurationFilterPB, RepeatedGridGroupConfigurationPB, - RepeatedGridSortPB, -}; -use flowy_grid_data_model::revision::{FieldRevision, SettingRevision}; -use flowy_sync::entities::grid::{CreateFilterParams, DeleteFilterParams, GridSettingChangesetParams}; -use std::collections::HashMap; -use std::sync::Arc; +use crate::entities::{CreateFilterParams, DeleteFilterParams, GridLayout, GridSettingChangesetParams}; pub struct GridSettingChangesetBuilder { params: GridSettingChangesetParams, @@ -20,8 +13,6 @@ impl GridSettingChangesetBuilder { delete_filter: None, insert_group: None, delete_group: None, - insert_sort: None, - delete_sort: None, }; Self { params } } @@ -40,42 +31,3 @@ impl GridSettingChangesetBuilder { self.params } } - -pub fn make_grid_setting(grid_setting_rev: &SettingRevision, field_revs: &[Arc]) -> GridSettingPB { - let current_layout_type: GridLayout = grid_setting_rev.layout.clone().into(); - let filters_by_field_id = grid_setting_rev - .get_all_filters(field_revs) - .map(|filters_by_field_id| { - filters_by_field_id - .into_iter() - .map(|(k, v)| (k, v.into())) - .collect::>() - }) - .unwrap_or_default(); - let groups_by_field_id = grid_setting_rev - .get_all_groups(field_revs) - .map(|groups_by_field_id| { - groups_by_field_id - .into_iter() - .map(|(k, v)| (k, v.into())) - .collect::>() - }) - .unwrap_or_default(); - let sorts_by_field_id = grid_setting_rev - .get_all_sort() - .map(|sorts_by_field_id| { - sorts_by_field_id - .into_iter() - .map(|(k, v)| (k, v.into())) - .collect::>() - }) - .unwrap_or_default(); - - GridSettingPB { - layouts: GridLayoutPB::all(), - current_layout_type, - filter_configuration_by_field_id: filters_by_field_id, - group_configuration_by_field_id: groups_by_field_id, - sorts_by_field_id, - } -} diff --git a/shared-lib/flowy-grid-data-model/src/revision/filter_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/filter_rev.rs new file mode 100644 index 0000000000..7079b52229 --- /dev/null +++ b/shared-lib/flowy-grid-data-model/src/revision/filter_rev.rs @@ -0,0 +1,9 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq, Hash)] +pub struct FilterConfigurationRevision { + pub id: String, + pub field_id: String, + pub condition: u8, + pub content: Option, +} diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs index ff15170932..01ca80a380 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs @@ -1,8 +1,7 @@ -use crate::revision::{FieldRevision, FieldTypeRevision}; +use crate::revision::{FieldRevision, FieldTypeRevision, FilterConfigurationRevision, GroupConfigurationRevision}; use indexmap::IndexMap; use nanoid::nanoid; use serde::{Deserialize, Serialize}; -use serde_repr::*; use std::collections::HashMap; use std::fmt::Debug; use std::sync::Arc; @@ -15,6 +14,7 @@ pub fn gen_grid_group_id() -> String { nanoid!(6) } +#[allow(dead_code)] pub fn gen_grid_sort_id() -> String { nanoid!(6) } @@ -24,114 +24,6 @@ pub type FilterConfigurationsByFieldId = HashMap; pub type GroupConfigurationsByFieldId = HashMap>>; -// -pub type SortConfiguration = Configuration; -pub type SortConfigurationsByFieldId = HashMap>>; - -#[derive(Debug, Clone, Serialize, Deserialize, Default, Eq, PartialEq)] -pub struct SettingRevision { - pub layout: LayoutRevision, - - pub filters: FilterConfiguration, - - #[serde(default)] - pub groups: GroupConfiguration, - - #[serde(skip)] - pub sorts: SortConfiguration, -} - -#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize_repr, Deserialize_repr)] -#[repr(u8)] -pub enum LayoutRevision { - Table = 0, - Board = 1, -} - -impl ToString for LayoutRevision { - fn to_string(&self) -> String { - let layout_rev = self.clone() as u8; - layout_rev.to_string() - } -} - -impl std::default::Default for LayoutRevision { - fn default() -> Self { - LayoutRevision::Table - } -} - -impl SettingRevision { - pub fn get_all_groups(&self, field_revs: &[Arc]) -> Option { - self.groups.get_all_objects(field_revs) - } - - pub fn get_groups( - &self, - field_id: &str, - field_type_rev: &FieldTypeRevision, - ) -> Option>> { - self.groups.get_objects(field_id, field_type_rev) - } - - pub fn get_mut_groups( - &mut self, - field_id: &str, - field_type: &FieldTypeRevision, - ) -> Option<&mut Vec>> { - self.groups.get_mut_objects(field_id, field_type) - } - - pub fn insert_group( - &mut self, - field_id: &str, - field_type: &FieldTypeRevision, - group_rev: GroupConfigurationRevision, - ) { - // only one group can be set - self.groups.remove_all(); - self.groups.insert_object(field_id, field_type, group_rev); - } - - pub fn get_all_filters(&self, field_revs: &[Arc]) -> Option { - self.filters.get_all_objects(field_revs) - } - - pub fn get_filters( - &self, - field_id: &str, - field_type_rev: &FieldTypeRevision, - ) -> Option>> { - self.filters.get_objects(field_id, field_type_rev) - } - - pub fn get_mut_filters( - &mut self, - field_id: &str, - field_type: &FieldTypeRevision, - ) -> Option<&mut Vec>> { - self.filters.get_mut_objects(field_id, field_type) - } - - pub fn insert_filter( - &mut self, - field_id: &str, - field_type: &FieldTypeRevision, - filter_rev: FilterConfigurationRevision, - ) { - self.filters.insert_object(field_id, field_type, filter_rev); - } - - pub fn get_all_sort(&self) -> Option { - None - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] -pub struct SortConfigurationRevision { - pub id: String, - pub field_id: Option, -} #[derive(Debug, Clone, Serialize, Deserialize, Default, Eq, PartialEq)] #[serde(transparent)] @@ -239,38 +131,3 @@ where &mut self.object_by_field_type } } - -pub trait GroupConfigurationSerde { - fn from_configuration(s: &str) -> Result; -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] -pub struct GroupConfigurationRevision { - pub id: String, - pub field_id: String, - pub field_type_rev: FieldTypeRevision, - pub content: String, -} - -impl GroupConfigurationRevision { - pub fn new(field_id: String, field_type: FieldTypeRevision, content: T) -> Result - where - T: serde::Serialize, - { - let content = serde_json::to_string(&content)?; - Ok(Self { - id: gen_grid_group_id(), - field_id, - field_type_rev: field_type, - content, - }) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq, Hash)] -pub struct FilterConfigurationRevision { - pub id: String, - pub field_id: String, - pub condition: u8, - pub content: Option, -} diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_view.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_view.rs index 2fcc2fc4eb..9d4df3067e 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/grid_view.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/grid_view.rs @@ -1,19 +1,49 @@ -use crate::revision::SettingRevision; +use crate::revision::{ + FieldRevision, FieldTypeRevision, FilterConfiguration, FilterConfigurationRevision, FilterConfigurationsByFieldId, + GroupConfiguration, GroupConfigurationRevision, GroupConfigurationsByFieldId, +}; use nanoid::nanoid; use serde::{Deserialize, Serialize}; +use serde_repr::*; +use std::sync::Arc; #[allow(dead_code)] pub fn gen_grid_view_id() -> String { nanoid!(6) } +#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum LayoutRevision { + Table = 0, + Board = 1, +} + +impl ToString for LayoutRevision { + fn to_string(&self) -> String { + let layout_rev = self.clone() as u8; + layout_rev.to_string() + } +} + +impl std::default::Default for LayoutRevision { + fn default() -> Self { + LayoutRevision::Table + } +} + #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct GridViewRevision { pub view_id: String, pub grid_id: String, - pub setting: SettingRevision, + pub layout: LayoutRevision, + + pub filters: FilterConfiguration, + + #[serde(default)] + pub groups: GroupConfiguration, // For the moment, we just use the order returned from the GridRevision #[allow(dead_code)] @@ -26,10 +56,78 @@ impl GridViewRevision { GridViewRevision { view_id, grid_id, - setting: Default::default(), + layout: Default::default(), + filters: Default::default(), + groups: Default::default(), row_orders: vec![], } } + + pub fn get_all_groups(&self, field_revs: &[Arc]) -> Option { + self.groups.get_all_objects(field_revs) + } + + pub fn get_groups( + &self, + field_id: &str, + field_type_rev: &FieldTypeRevision, + ) -> Option> { + let mut groups = self.groups.get_objects(field_id, field_type_rev)?; + if groups.is_empty() { + debug_assert_eq!(groups.len(), 1); + Some(groups.pop().unwrap()) + } else { + None + } + } + + pub fn get_mut_groups( + &mut self, + field_id: &str, + field_type: &FieldTypeRevision, + ) -> Option<&mut Vec>> { + self.groups.get_mut_objects(field_id, field_type) + } + + pub fn insert_group( + &mut self, + field_id: &str, + field_type: &FieldTypeRevision, + group_rev: GroupConfigurationRevision, + ) { + // only one group can be set + self.groups.remove_all(); + self.groups.insert_object(field_id, field_type, group_rev); + } + + pub fn get_all_filters(&self, field_revs: &[Arc]) -> Option { + self.filters.get_all_objects(field_revs) + } + + pub fn get_filters( + &self, + field_id: &str, + field_type_rev: &FieldTypeRevision, + ) -> Option>> { + self.filters.get_objects(field_id, field_type_rev) + } + + pub fn get_mut_filters( + &mut self, + field_id: &str, + field_type: &FieldTypeRevision, + ) -> Option<&mut Vec>> { + self.filters.get_mut_objects(field_id, field_type) + } + + pub fn insert_filter( + &mut self, + field_id: &str, + field_type: &FieldTypeRevision, + filter_rev: FilterConfigurationRevision, + ) { + self.filters.insert_object(field_id, field_type, filter_rev); + } } #[derive(Debug, Clone, Default, Serialize, Deserialize)] diff --git a/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs index 932cc60033..83deeb2735 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs @@ -1,32 +1,91 @@ +use crate::revision::{gen_grid_group_id, FieldTypeRevision}; use serde::{Deserialize, Serialize}; +use serde_json::Error; use serde_repr::*; +pub trait GroupConfigurationContentSerde: Sized { + fn from_configuration(s: &str) -> Result; +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] +pub struct GroupConfigurationRevision { + pub id: String, + pub field_id: String, + pub field_type_rev: FieldTypeRevision, + pub content: String, +} + +impl GroupConfigurationRevision { + pub fn new(field_id: String, field_type: FieldTypeRevision, content: T) -> Result + where + T: serde::Serialize, + { + let content = serde_json::to_string(&content)?; + Ok(Self { + id: gen_grid_group_id(), + field_id, + field_type_rev: field_type, + content, + }) + } +} + #[derive(Default, Serialize, Deserialize)] pub struct TextGroupConfigurationRevision { pub hide_empty: bool, } +impl GroupConfigurationContentSerde for TextGroupConfigurationRevision { + fn from_configuration(s: &str) -> Result { + serde_json::from_str(s) + } +} + #[derive(Default, Serialize, Deserialize)] pub struct NumberGroupConfigurationRevision { pub hide_empty: bool, } +impl GroupConfigurationContentSerde for NumberGroupConfigurationRevision { + fn from_configuration(s: &str) -> Result { + serde_json::from_str(s) + } +} + #[derive(Default, Serialize, Deserialize)] pub struct UrlGroupConfigurationRevision { pub hide_empty: bool, } +impl GroupConfigurationContentSerde for UrlGroupConfigurationRevision { + fn from_configuration(s: &str) -> Result { + serde_json::from_str(s) + } +} + #[derive(Default, Serialize, Deserialize)] pub struct CheckboxGroupConfigurationRevision { pub hide_empty: bool, } +impl GroupConfigurationContentSerde for CheckboxGroupConfigurationRevision { + fn from_configuration(s: &str) -> Result { + serde_json::from_str(s) + } +} + #[derive(Default, Serialize, Deserialize)] pub struct SelectOptionGroupConfigurationRevision { pub hide_empty: bool, pub groups: Vec, } +impl GroupConfigurationContentSerde for SelectOptionGroupConfigurationRevision { + fn from_configuration(s: &str) -> Result { + serde_json::from_str(s) + } +} + #[derive(Default, Serialize, Deserialize)] pub struct GroupRecordRevision { pub group_id: String, @@ -42,6 +101,12 @@ pub struct DateGroupConfigurationRevision { pub condition: DateCondition, } +impl GroupConfigurationContentSerde for DateGroupConfigurationRevision { + fn from_configuration(s: &str) -> Result { + serde_json::from_str(s) + } +} + #[derive(Serialize_repr, Deserialize_repr)] #[repr(u8)] pub enum DateCondition { diff --git a/shared-lib/flowy-grid-data-model/src/revision/mod.rs b/shared-lib/flowy-grid-data-model/src/revision/mod.rs index d9df0910bd..460370b86b 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/mod.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/mod.rs @@ -1,9 +1,11 @@ +mod filter_rev; mod grid_block; mod grid_rev; mod grid_setting_rev; mod grid_view; mod group_rev; +pub use filter_rev::*; pub use grid_block::*; pub use grid_rev::*; pub use grid_setting_rev::*; diff --git a/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs b/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs index db52d0b38b..5ec4a87ef9 100644 --- a/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs +++ b/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs @@ -3,7 +3,7 @@ use crate::errors::{internal_error, CollaborateError, CollaborateResult}; use crate::util::{cal_diff, make_text_delta_from_revisions}; use flowy_grid_data_model::revision::{ FieldRevision, FieldTypeRevision, FilterConfigurationRevision, FilterConfigurationsByFieldId, GridViewRevision, - GroupConfigurationRevision, GroupConfigurationsByFieldId, SettingRevision, SortConfigurationsByFieldId, + GroupConfigurationRevision, GroupConfigurationsByFieldId, }; use lib_ot::core::{OperationTransform, PhantomAttributes, TextDelta, TextDeltaBuilder}; use std::sync::Arc; @@ -48,23 +48,11 @@ impl GridViewRevisionPad { Self::from_delta(delta) } - pub fn get_setting_rev(&self) -> &SettingRevision { - &self.view.setting - } - pub fn get_all_groups(&self, field_revs: &[Arc]) -> Option { - self.setting.groups.get_all_objects(field_revs) + self.groups.get_all_objects(field_revs) } - pub fn get_groups( - &self, - field_id: &str, - field_type_rev: &FieldTypeRevision, - ) -> Option>> { - self.setting.groups.get_objects(field_id, field_type_rev) - } - - pub fn insert_group( + pub fn insert_group_configuration( &mut self, field_id: &str, field_type: &FieldTypeRevision, @@ -72,8 +60,8 @@ impl GridViewRevisionPad { ) -> CollaborateResult> { self.modify(|view| { // only one group can be set - view.setting.groups.remove_all(); - view.setting.groups.insert_object(field_id, field_type, group_rev); + view.groups.remove_all(); + view.groups.insert_object(field_id, field_type, group_rev); Ok(Some(())) }) } @@ -85,7 +73,7 @@ impl GridViewRevisionPad { group_id: &str, ) -> CollaborateResult> { self.modify(|view| { - if let Some(groups) = view.setting.groups.get_mut_objects(field_id, field_type) { + if let Some(groups) = view.groups.get_mut_objects(field_id, field_type) { groups.retain(|group| group.id != group_id); Ok(Some(())) } else { @@ -95,7 +83,7 @@ impl GridViewRevisionPad { } pub fn get_all_filters(&self, field_revs: &[Arc]) -> Option { - self.setting.filters.get_all_objects(field_revs) + self.filters.get_all_objects(field_revs) } pub fn get_filters( @@ -103,7 +91,7 @@ impl GridViewRevisionPad { field_id: &str, field_type_rev: &FieldTypeRevision, ) -> Option>> { - self.setting.filters.get_objects(field_id, field_type_rev) + self.filters.get_objects(field_id, field_type_rev) } pub fn insert_filter( @@ -113,7 +101,7 @@ impl GridViewRevisionPad { filter_rev: FilterConfigurationRevision, ) -> CollaborateResult> { self.modify(|view| { - view.setting.filters.insert_object(field_id, field_type, filter_rev); + view.filters.insert_object(field_id, field_type, filter_rev); Ok(Some(())) }) } @@ -125,7 +113,7 @@ impl GridViewRevisionPad { filter_id: &str, ) -> CollaborateResult> { self.modify(|view| { - if let Some(filters) = view.setting.filters.get_mut_objects(field_id, field_type) { + if let Some(filters) = view.filters.get_mut_objects(field_id, field_type) { filters.retain(|filter| filter.id != filter_id); Ok(Some(())) } else { @@ -134,10 +122,6 @@ impl GridViewRevisionPad { }) } - pub fn get_all_sort(&self) -> Option { - None - } - pub fn json_str(&self) -> CollaborateResult { make_grid_view_rev_json_str(&self.view) } From 49601b66efb30e7bfbf3585f930091f5e7b71beb Mon Sep 17 00:00:00 2001 From: appflowy Date: Sat, 20 Aug 2022 16:44:44 +0800 Subject: [PATCH 06/86] chore: save group configuration --- .../src/services/grid_view_editor.rs | 3 +- .../src/services/group/configuration.rs | 17 +++++---- .../controller_impls/checkbox_controller.rs | 10 +++--- .../select_option_controller/util.rs | 5 +-- .../src/services/group/group_controller.rs | 8 ++--- .../src/services/group/group_service.rs | 5 +-- .../src/revision/group_rev.rs | 35 +++++++++++++++---- 7 files changed, 53 insertions(+), 30 deletions(-) diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs index 9a7173035a..3a04a9946f 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs @@ -287,7 +287,7 @@ impl GroupConfigurationWriter for GroupConfigurationWriterImpl { &self, field_id: &str, field_type: FieldTypeRevision, - configuration: Arc, + configuration: GroupConfigurationRevision, ) -> AFFuture> { let user_id = self.user_id.clone(); let rev_manager = self.rev_manager.clone(); @@ -295,7 +295,6 @@ impl GroupConfigurationWriter for GroupConfigurationWriterImpl { let field_id = field_id.to_owned(); wrap_future(async move { - let configuration = (&*configuration).clone(); match view_pad .write() .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 3d737c307f..0c120d477f 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs @@ -1,3 +1,4 @@ +use crate::services::group::Group; use flowy_error::FlowyResult; use flowy_grid_data_model::revision::{ FieldRevision, FieldTypeRevision, GroupConfigurationContentSerde, GroupConfigurationRevision, GroupRecordRevision, @@ -14,13 +15,13 @@ pub trait GroupConfigurationWriter: Send + Sync + 'static { &self, field_id: &str, field_type: FieldTypeRevision, - configuration: Arc, + configuration: GroupConfigurationRevision, ) -> AFFuture>; } pub trait GroupConfigurationAction: Send + Sync { fn group_records(&self) -> &[GroupRecordRevision]; - fn save_groups(&self) -> FlowyResult<()>; + fn merge_groups(&self, groups: Vec) -> FlowyResult<()>; fn hide_group(&self, group_id: &str) -> FlowyResult<()>; fn show_group(&self, group_id: &str) -> FlowyResult<()>; } @@ -28,8 +29,9 @@ pub trait GroupConfigurationAction: Send + Sync { pub struct GenericGroupConfiguration { field_rev: Arc, reader: Arc, + configuration_rev: Arc, writer: Arc, - configuration: C, + pub(crate) configuration: C, } impl GenericGroupConfiguration @@ -41,15 +43,18 @@ where reader: Arc, writer: Arc, ) -> FlowyResult { - let configuration = reader.get_group_configuration(field_rev.clone()).await; - let configuration = C::from_configuration(&configuration.content)?; + let configuration_rev = reader.get_group_configuration(field_rev.clone()).await; + let configuration = C::from_configuration_content(&configuration.content)?; Ok(Self { field_rev, + configuration_rev, reader, writer, configuration, }) } + + pub async fn save_configuration(&self) {} } impl GroupConfigurationReader for Arc @@ -69,7 +74,7 @@ where &self, field_id: &str, field_type: FieldTypeRevision, - configuration: Arc, + configuration: GroupConfigurationRevision, ) -> AFFuture> { (**self).save_group_configuration(field_id, field_type, configuration) } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs index fca49513c8..d78c055ab6 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs @@ -19,19 +19,19 @@ pub type CheckboxGroupController = GenericGroupController< pub type CheckboxGroupConfiguration = GenericGroupConfiguration; impl GroupConfigurationAction for CheckboxGroupConfiguration { fn group_records(&self) -> &[GroupRecordRevision] { - todo!() + vec![].as_slice() } - fn save_groups(&self) -> FlowyResult<()> { - todo!() + fn merge_groups(&self, groups: Vec) -> FlowyResult<()> { + Ok(()) } fn hide_group(&self, group_id: &str) -> FlowyResult<()> { - todo!() + Ok(()) } fn show_group(&self, group_id: &str) -> FlowyResult<()> { - todo!() + Ok(()) } } 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 d95a39f60c..a0a4c2f13e 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 @@ -11,10 +11,11 @@ use flowy_grid_data_model::revision::{ pub type SelectOptionGroupConfiguration = GenericGroupConfiguration; impl GroupConfigurationAction for SelectOptionGroupConfiguration { fn group_records(&self) -> &[GroupRecordRevision] { - todo!() + self.configuration.as_slice() } - fn save_groups(&self) -> FlowyResult<()> { + fn merge_groups(&self, groups: Vec) -> FlowyResult<()> { + // self.writer.save_group_configuration() todo!() } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_controller.rs index fb2c0aaa6e..97755fd0ee 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_controller.rs @@ -5,11 +5,11 @@ use crate::services::group::configuration::{GenericGroupConfiguration, GroupConf use crate::services::group::entities::Group; use flowy_error::FlowyResult; use flowy_grid_data_model::revision::{ - FieldRevision, GroupConfigurationContentSerde, GroupConfigurationRevision, RowChangeset, RowRevision, - TypeOptionDataDeserializer, + FieldRevision, GroupConfigurationContentSerde, RowChangeset, RowRevision, TypeOptionDataDeserializer, }; use indexmap::IndexMap; -use lib_infra::future::AFFuture; + +use crate::services::group::GroupConfigurationWriter; use std::marker::PhantomData; use std::sync::Arc; @@ -84,7 +84,7 @@ where pub async fn new( field_rev: &Arc, configuration_reader: Arc, - configuration_writer: Arc, + configuration_writer: Arc, ) -> FlowyResult { let configuration = GenericGroupConfiguration::::new(field_rev.clone(), configuration_reader, configuration_writer).await?; diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs index 42e687445d..0b60b54535 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs @@ -1,7 +1,3 @@ -use crate::entities::{ - CheckboxGroupConfigurationPB, DateGroupConfigurationPB, FieldType, GroupRowsChangesetPB, - NumberGroupConfigurationPB, SelectOptionGroupConfigurationPB, TextGroupConfigurationPB, UrlGroupConfigurationPB, -}; use crate::services::group::configuration::GroupConfigurationReader; use crate::services::group::group_controller::GroupController; use crate::services::group::{ @@ -14,6 +10,7 @@ use flowy_grid_data_model::revision::{ TextGroupConfigurationRevision, UrlGroupConfigurationRevision, }; +use crate::entities::{FieldType, GroupRowsChangesetPB}; use std::future::Future; use std::sync::Arc; use tokio::sync::RwLock; diff --git a/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs index 83deeb2735..c4d2287c0c 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs @@ -4,7 +4,8 @@ use serde_json::Error; use serde_repr::*; pub trait GroupConfigurationContentSerde: Sized { - fn from_configuration(s: &str) -> Result; + fn from_configuration_content(s: &str) -> Result; + fn to_configuration_content(&self) -> Result; } #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] @@ -36,9 +37,12 @@ pub struct TextGroupConfigurationRevision { } impl GroupConfigurationContentSerde for TextGroupConfigurationRevision { - fn from_configuration(s: &str) -> Result { + fn from_configuration_content(s: &str) -> Result { serde_json::from_str(s) } + fn to_configuration_content(&self) -> Result { + serde_json::to_string(self) + } } #[derive(Default, Serialize, Deserialize)] @@ -47,9 +51,12 @@ pub struct NumberGroupConfigurationRevision { } impl GroupConfigurationContentSerde for NumberGroupConfigurationRevision { - fn from_configuration(s: &str) -> Result { + fn from_configuration_content(s: &str) -> Result { serde_json::from_str(s) } + fn to_configuration_content(&self) -> Result { + serde_json::to_string(self) + } } #[derive(Default, Serialize, Deserialize)] @@ -58,9 +65,12 @@ pub struct UrlGroupConfigurationRevision { } impl GroupConfigurationContentSerde for UrlGroupConfigurationRevision { - fn from_configuration(s: &str) -> Result { + fn from_configuration_content(s: &str) -> Result { serde_json::from_str(s) } + fn to_configuration_content(&self) -> Result { + serde_json::to_string(self) + } } #[derive(Default, Serialize, Deserialize)] @@ -69,9 +79,13 @@ pub struct CheckboxGroupConfigurationRevision { } impl GroupConfigurationContentSerde for CheckboxGroupConfigurationRevision { - fn from_configuration(s: &str) -> Result { + fn from_configuration_content(s: &str) -> Result { serde_json::from_str(s) } + + fn to_configuration_content(&self) -> Result { + serde_json::to_string(self) + } } #[derive(Default, Serialize, Deserialize)] @@ -81,9 +95,13 @@ pub struct SelectOptionGroupConfigurationRevision { } impl GroupConfigurationContentSerde for SelectOptionGroupConfigurationRevision { - fn from_configuration(s: &str) -> Result { + fn from_configuration_content(s: &str) -> Result { serde_json::from_str(s) } + + fn to_configuration_content(&self) -> Result { + serde_json::to_string(self) + } } #[derive(Default, Serialize, Deserialize)] @@ -102,9 +120,12 @@ pub struct DateGroupConfigurationRevision { } impl GroupConfigurationContentSerde for DateGroupConfigurationRevision { - fn from_configuration(s: &str) -> Result { + fn from_configuration_content(s: &str) -> Result { serde_json::from_str(s) } + fn to_configuration_content(&self) -> Result { + serde_json::to_string(self) + } } #[derive(Serialize_repr, Deserialize_repr)] From 3c022d4de5c3b249d35f54f1a77a94fd138bdf22 Mon Sep 17 00:00:00 2001 From: appflowy Date: Sat, 20 Aug 2022 17:10:34 +0800 Subject: [PATCH 07/86] chore: save group configuration --- .../src/services/grid_view_editor.rs | 29 +++++++++++++++++-- .../src/services/group/configuration.rs | 20 +++++++++---- .../controller_impls/checkbox_controller.rs | 2 +- .../select_option_controller/util.rs | 3 +- .../src/client_grid/view_revision_pad.rs | 21 ++++++++++++++ 5 files changed, 66 insertions(+), 9 deletions(-) diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs index 3a04a9946f..9ab6f4801a 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs @@ -287,9 +287,11 @@ impl GroupConfigurationWriter for GroupConfigurationWriterImpl { &self, field_id: &str, field_type: FieldTypeRevision, - configuration: GroupConfigurationRevision, + group_id: &str, + mut_fn: impl FnOnce(&mut GroupConfigurationRevision), ) -> AFFuture> { let user_id = self.user_id.clone(); + let group_id = group_id.to_owned(); let rev_manager = self.rev_manager.clone(); let view_pad = self.view_pad.clone(); let field_id = field_id.to_owned(); @@ -298,13 +300,36 @@ impl GroupConfigurationWriter for GroupConfigurationWriterImpl { match view_pad .write() .await - .insert_group_configuration(&field_id, &field_type, configuration)? + .get_mut_group(&field_id, &field_type, &group_id, mut_fn)? { None => Ok(()), Some(changeset) => apply_change(&user_id, rev_manager, changeset).await, } }) } + // fn save_group_configuration( + // &self, + // field_id: &str, + // field_type: FieldTypeRevision, + // + // configuration: GroupConfigurationRevision, + // ) -> AFFuture> { + // let user_id = self.user_id.clone(); + // let rev_manager = self.rev_manager.clone(); + // let view_pad = self.view_pad.clone(); + // let field_id = field_id.to_owned(); + // + // wrap_future(async move { + // match view_pad + // .write() + // .await + // .insert_group_configuration(&field_id, &field_type, configuration)? + // { + // None => Ok(()), + // Some(changeset) => apply_change(&user_id, rev_manager, changeset).await, + // } + // }) + // } } pub fn make_grid_setting(view_pad: &GridViewRevisionPad, field_revs: &[Arc]) -> GridSettingPB { 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 0c120d477f..4fe59fbaca 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs @@ -10,12 +10,13 @@ pub trait GroupConfigurationReader: Send + Sync + 'static { fn get_group_configuration(&self, field_rev: Arc) -> AFFuture>; } -pub trait GroupConfigurationWriter: Send + Sync + 'static { +pub trait GroupConfigurationWriter: Send + Sync + 'static + ?Sized { fn save_group_configuration( &self, field_id: &str, field_type: FieldTypeRevision, - configuration: GroupConfigurationRevision, + group_id: &str, + mut_fn: fn(&mut GroupConfigurationRevision), ) -> AFFuture>; } @@ -44,7 +45,7 @@ where writer: Arc, ) -> FlowyResult { let configuration_rev = reader.get_group_configuration(field_rev.clone()).await; - let configuration = C::from_configuration_content(&configuration.content)?; + let configuration = C::from_configuration_content(&configuration_rev.content)?; Ok(Self { field_rev, configuration_rev, @@ -74,8 +75,17 @@ where &self, field_id: &str, field_type: FieldTypeRevision, - configuration: GroupConfigurationRevision, + group_id: &str, + mut_fn: impl FnOnce(&mut GroupConfigurationRevision), ) -> AFFuture> { - (**self).save_group_configuration(field_id, field_type, configuration) + todo!() } + // fn save_group_configuration( + // &self, + // field_id: &str, + // field_type: FieldTypeRevision, + // configuration: GroupConfigurationRevision, + // ) -> AFFuture> { + // (**self).save_group_configuration(field_id, field_type, configuration) + // } } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs index d78c055ab6..dc278af7f5 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs @@ -19,7 +19,7 @@ pub type CheckboxGroupController = GenericGroupController< pub type CheckboxGroupConfiguration = GenericGroupConfiguration; impl GroupConfigurationAction for CheckboxGroupConfiguration { fn group_records(&self) -> &[GroupRecordRevision] { - vec![].as_slice() + &[] } fn merge_groups(&self, groups: Vec) -> FlowyResult<()> { 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 a0a4c2f13e..b6439f1f12 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 @@ -11,7 +11,8 @@ use flowy_grid_data_model::revision::{ pub type SelectOptionGroupConfiguration = GenericGroupConfiguration; impl GroupConfigurationAction for SelectOptionGroupConfiguration { fn group_records(&self) -> &[GroupRecordRevision] { - self.configuration.as_slice() + // self.configuration.as_slice() + todo!() } fn merge_groups(&self, groups: Vec) -> FlowyResult<()> { diff --git a/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs b/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs index 5ec4a87ef9..3f5f799369 100644 --- a/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs +++ b/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs @@ -66,6 +66,27 @@ impl GridViewRevisionPad { }) } + pub fn get_mut_group( + &mut self, + field_id: &str, + field_type: &FieldTypeRevision, + group_id: &str, + mut_group_fn: F, + ) -> CollaborateResult> { + self.modify(|view| match view.groups.get_mut_objects(field_id, field_type) { + None => Ok(None), + Some(groups) => { + for mut group in groups { + if group.id == group_id { + mut_group_fn(Arc::make_mut(&mut group)); + return Ok(Some(())); + } + } + Ok(None) + } + }) + } + pub fn delete_group( &mut self, field_id: &str, From 6e6a8122434f2ce943761289b11d2a7e6236bfcb Mon Sep 17 00:00:00 2001 From: appflowy Date: Sat, 20 Aug 2022 23:54:51 +0800 Subject: [PATCH 08/86] chore: save group revision and fix warnings --- .../app_flowy/lib/plugins/board/board.dart | 2 +- .../grid/application/grid_service.dart | 1 - .../flowy-grid/src/services/grid_editor.rs | 12 +- .../src/services/grid_view_editor.rs | 47 +++----- .../src/services/grid_view_manager.rs | 1 + .../src/services/group/configuration.rs | 112 ++++++++++++------ .../{group_controller.rs => controller.rs} | 35 +++--- .../controller_impls/checkbox_controller.rs | 31 +---- .../select_option_controller/mod.rs | 1 + .../multi_select_controller.rs | 4 +- .../single_select_controller.rs | 4 +- .../select_option_controller/util.rs | 25 +--- .../src/services/group/group_service.rs | 26 ++-- .../flowy-grid/src/services/group/mod.rs | 2 +- .../tests/grid/field_test/script.rs | 3 +- .../flowy-grid/tests/grid/field_test/test.rs | 2 +- .../tests/grid/filter_test/script.rs | 3 +- .../flowy-grid/tests/grid/grid_editor.rs | 3 - .../src/revision/group_rev.rs | 56 +++++++-- .../src/client_grid/view_revision_pad.rs | 13 +- 20 files changed, 203 insertions(+), 180 deletions(-) rename frontend/rust-lib/flowy-grid/src/services/group/{group_controller.rs => controller.rs} (88%) diff --git a/frontend/app_flowy/lib/plugins/board/board.dart b/frontend/app_flowy/lib/plugins/board/board.dart index c55d7f2e17..213cc8bc3c 100644 --- a/frontend/app_flowy/lib/plugins/board/board.dart +++ b/frontend/app_flowy/lib/plugins/board/board.dart @@ -31,7 +31,7 @@ class BoardPluginBuilder implements PluginBuilder { class BoardPluginConfig implements PluginConfig { @override - bool get creatable => false; + bool get creatable => true; } class BoardPlugin extends Plugin { diff --git a/frontend/app_flowy/lib/plugins/grid/application/grid_service.dart b/frontend/app_flowy/lib/plugins/grid/application/grid_service.dart index 8c46ce18b2..c8b6873d91 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/grid_service.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/grid_service.dart @@ -3,7 +3,6 @@ import 'package:flowy_sdk/dispatch/dispatch.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.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/board_card.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/group.pb.dart'; diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index 0559cfa364..dabb77e1de 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -168,10 +168,12 @@ impl GridRevisionEditor { pub async fn update_field(&self, params: FieldChangesetParams) -> FlowyResult<()> { let field_id = params.field_id.clone(); - let field_type: Option = match self.grid_pad.read().await.get_field_rev(params.field_id.as_str()) { - None => None, - Some((_, field_rev)) => Some(field_rev.ty.into()), - }; + let field_type: Option = self + .grid_pad + .read() + .await + .get_field_rev(params.field_id.as_str()) + .map(|(_, field_rev)| field_rev.ty.into()); match field_type { None => Err(ErrorCode::FieldDoesNotExist.into()), @@ -341,7 +343,7 @@ impl GridRevisionEditor { Ok(row_pb) } - pub async fn move_group(&self, params: MoveGroupParams) -> FlowyResult { + pub async fn move_group(&self, _params: MoveGroupParams) -> FlowyResult { // todo!() } diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs index 9ab6f4801a..a9ade3f194 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs @@ -183,7 +183,7 @@ impl GridViewRevisionEditor { grid_setting } - pub(crate) async fn update_setting(&self, changeset: GridSettingChangesetParams) -> FlowyResult<()> { + pub(crate) async fn update_setting(&self, _changeset: GridSettingChangesetParams) -> FlowyResult<()> { // let _ = self.modify(|pad| Ok(pad.update_setting(changeset)?)).await; // Ok(()) todo!() @@ -207,6 +207,7 @@ impl GridViewRevisionEditor { .send(); } + #[allow(dead_code)] async fn modify(&self, f: F) -> FlowyResult<()> where F: for<'a> FnOnce(&'a mut GridViewRevisionPad) -> FlowyResult>, @@ -230,7 +231,7 @@ async fn apply_change( let GridViewRevisionChangeset { delta, md5 } = change; let (base_rev_id, rev_id) = rev_manager.next_rev_id_pair(); let delta_data = delta.json_bytes(); - let revision = Revision::new(&rev_manager.object_id, base_rev_id, rev_id, delta_data, &user_id, md5); + let revision = Revision::new(&rev_manager.object_id, base_rev_id, rev_id, delta_data, user_id, md5); let _ = rev_manager.add_local_revision(&revision).await?; Ok(()) } @@ -287,49 +288,29 @@ impl GroupConfigurationWriter for GroupConfigurationWriterImpl { &self, field_id: &str, field_type: FieldTypeRevision, - group_id: &str, - mut_fn: impl FnOnce(&mut GroupConfigurationRevision), + configuration_id: &str, + content: String, ) -> AFFuture> { let user_id = self.user_id.clone(); - let group_id = group_id.to_owned(); + let configuration_id = configuration_id.to_owned(); let rev_manager = self.rev_manager.clone(); let view_pad = self.view_pad.clone(); let field_id = field_id.to_owned(); wrap_future(async move { - match view_pad - .write() - .await - .get_mut_group(&field_id, &field_type, &group_id, mut_fn)? - { + match view_pad.write().await.get_mut_group( + &field_id, + &field_type, + &configuration_id, + |group_configuration| { + group_configuration.content = content; + }, + )? { None => Ok(()), Some(changeset) => apply_change(&user_id, rev_manager, changeset).await, } }) } - // fn save_group_configuration( - // &self, - // field_id: &str, - // field_type: FieldTypeRevision, - // - // configuration: GroupConfigurationRevision, - // ) -> AFFuture> { - // let user_id = self.user_id.clone(); - // let rev_manager = self.rev_manager.clone(); - // let view_pad = self.view_pad.clone(); - // let field_id = field_id.to_owned(); - // - // wrap_future(async move { - // match view_pad - // .write() - // .await - // .insert_group_configuration(&field_id, &field_type, configuration)? - // { - // None => Ok(()), - // Some(changeset) => apply_change(&user_id, rev_manager, changeset).await, - // } - // }) - // } } pub fn make_grid_setting(view_pad: &GridViewRevisionPad, field_revs: &[Arc]) -> GridSettingPB { 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 42c4ff1c65..5386bdb893 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 @@ -131,6 +131,7 @@ impl GridViewManager { } } + #[allow(dead_code)] pub(crate) async fn move_group(&self) {} pub(crate) async fn get_view_editor(&self, view_id: &str) -> FlowyResult> { 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 4fe59fbaca..15cbe6c61f 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs @@ -1,8 +1,9 @@ use crate::services::group::Group; use flowy_error::FlowyResult; use flowy_grid_data_model::revision::{ - FieldRevision, FieldTypeRevision, GroupConfigurationContentSerde, GroupConfigurationRevision, GroupRecordRevision, + FieldRevision, FieldTypeRevision, GroupConfigurationContent, GroupConfigurationRevision, GroupRecordRevision, }; + use lib_infra::future::AFFuture; use std::sync::Arc; @@ -10,34 +11,28 @@ pub trait GroupConfigurationReader: Send + Sync + 'static { fn get_group_configuration(&self, field_rev: Arc) -> AFFuture>; } -pub trait GroupConfigurationWriter: Send + Sync + 'static + ?Sized { +pub trait GroupConfigurationWriter: Send + Sync + 'static { fn save_group_configuration( &self, field_id: &str, field_type: FieldTypeRevision, - group_id: &str, - mut_fn: fn(&mut GroupConfigurationRevision), + configuration_id: &str, + content: String, ) -> AFFuture>; } -pub trait GroupConfigurationAction: Send + Sync { - fn group_records(&self) -> &[GroupRecordRevision]; - fn merge_groups(&self, groups: Vec) -> FlowyResult<()>; - fn hide_group(&self, group_id: &str) -> FlowyResult<()>; - fn show_group(&self, group_id: &str) -> FlowyResult<()>; -} - pub struct GenericGroupConfiguration { + pub configuration: C, + // pub groups_map: IndexMap, + configuration_id: String, field_rev: Arc, reader: Arc, - configuration_rev: Arc, writer: Arc, - pub(crate) configuration: C, } impl GenericGroupConfiguration where - C: GroupConfigurationContentSerde, + C: GroupConfigurationContent, { pub async fn new( field_rev: Arc, @@ -45,17 +40,58 @@ where writer: Arc, ) -> FlowyResult { let configuration_rev = reader.get_group_configuration(field_rev.clone()).await; + let configuration_id = configuration_rev.id.clone(); let configuration = C::from_configuration_content(&configuration_rev.content)?; Ok(Self { + configuration_id, field_rev, - configuration_rev, reader, writer, configuration, }) } - pub async fn save_configuration(&self) {} + #[allow(dead_code)] + fn group_records(&self) -> &[GroupRecordRevision] { + todo!() + } + pub(crate) async fn merge_groups(&mut self, groups: &[Group]) -> FlowyResult<()> { + match merge_groups(self.configuration.get_groups(), groups) { + None => Ok(()), + Some(new_groups) => { + self.configuration.set_groups(new_groups); + let _ = self.save_configuration().await?; + Ok(()) + } + } + } + + #[allow(dead_code)] + pub(crate) async fn hide_group(&mut self, group_id: &str) -> FlowyResult<()> { + self.configuration.mut_group(group_id, |group_rev| { + group_rev.visible = false; + }); + let _ = self.save_configuration().await?; + Ok(()) + } + + #[allow(dead_code)] + pub(crate) async fn show_group(&mut self, group_id: &str) -> FlowyResult<()> { + self.configuration.mut_group(group_id, |group_rev| { + group_rev.visible = true; + }); + let _ = self.save_configuration().await?; + Ok(()) + } + + pub async fn save_configuration(&self) -> FlowyResult<()> { + let content = self.configuration.to_configuration_content()?; + let _ = self + .writer + .save_group_configuration(&self.field_rev.id, self.field_rev.ty, &self.configuration_id, content) + .await?; + Ok(()) + } } impl GroupConfigurationReader for Arc @@ -67,25 +103,29 @@ where } } -impl GroupConfigurationWriter for Arc -where - T: GroupConfigurationWriter, -{ - fn save_group_configuration( - &self, - field_id: &str, - field_type: FieldTypeRevision, - group_id: &str, - mut_fn: impl FnOnce(&mut GroupConfigurationRevision), - ) -> AFFuture> { - todo!() +fn merge_groups(old_group: &[GroupRecordRevision], groups: &[Group]) -> Option> { + // tracing::trace!("Merge group: old: {}, new: {}", old_group.len(), groups.len()); + if old_group.is_empty() { + let new_groups = groups + .iter() + .map(|group| GroupRecordRevision::new(group.id.clone())) + .collect(); + return Some(new_groups); } - // fn save_group_configuration( - // &self, - // field_id: &str, - // field_type: FieldTypeRevision, - // configuration: GroupConfigurationRevision, - // ) -> AFFuture> { - // (**self).save_group_configuration(field_id, field_type, configuration) - // } + + let new_groups = groups + .iter() + .filter(|group| !old_group.iter().any(|group_rev| group_rev.group_id == group.id)) + .collect::>(); + + if new_groups.is_empty() { + return None; + } + + let mut old_group = old_group.to_vec(); + let new_groups = new_groups + .iter() + .map(|group| GroupRecordRevision::new(group.id.clone())); + old_group.extend(new_groups); + Some(old_group) } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller.rs similarity index 88% rename from frontend/rust-lib/flowy-grid/src/services/group/group_controller.rs rename to frontend/rust-lib/flowy-grid/src/services/group/controller.rs index 97755fd0ee..44344fa0a1 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller.rs @@ -1,15 +1,14 @@ use crate::entities::{GroupRowsChangesetPB, RowPB}; use crate::services::cell::{decode_any_cell_data, CellBytesParser}; use crate::services::group::action::GroupAction; -use crate::services::group::configuration::{GenericGroupConfiguration, GroupConfigurationReader}; +use crate::services::group::configuration::GenericGroupConfiguration; use crate::services::group::entities::Group; use flowy_error::FlowyResult; use flowy_grid_data_model::revision::{ - FieldRevision, GroupConfigurationContentSerde, RowChangeset, RowRevision, TypeOptionDataDeserializer, + FieldRevision, GroupConfigurationContent, RowChangeset, RowRevision, TypeOptionDataDeserializer, }; use indexmap::IndexMap; -use crate::services::group::GroupConfigurationWriter; use std::marker::PhantomData; use std::sync::Arc; @@ -18,7 +17,7 @@ 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. -pub trait GroupController: GroupControllerSharedAction + Send + Sync { +pub trait GroupController: GroupControllerSharedOperation + Send + Sync { fn will_create_row(&mut self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str); } @@ -34,9 +33,10 @@ pub trait GroupGenerator { } // Defines the shared actions each group controller can perform. -pub trait GroupControllerSharedAction: Send + Sync { +pub trait GroupControllerSharedOperation: Send + Sync { // The field that is used for grouping the rows fn field_id(&self) -> &str; + fn groups(&self) -> &[Group]; fn fill_groups(&mut self, row_revs: &[Arc], field_rev: &FieldRevision) -> FlowyResult>; fn did_update_row( &mut self, @@ -66,32 +66,28 @@ pub trait GroupControllerSharedAction: Send + Sync { pub struct GenericGroupController { pub field_id: String, pub groups_map: IndexMap, - - /// default_group is used to store the rows that don't belong to any groups. - default_group: Group, 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

, } impl GenericGroupController where - C: GroupConfigurationContentSerde, + C: GroupConfigurationContent, T: TypeOptionDataDeserializer, G: GroupGenerator, TypeOptionType = T>, { pub async fn new( field_rev: &Arc, - configuration_reader: Arc, - configuration_writer: Arc, + mut configuration: GenericGroupConfiguration, ) -> FlowyResult { - let configuration = - GenericGroupConfiguration::::new(field_rev.clone(), configuration_reader, configuration_writer).await?; let field_type_rev = field_rev.ty; 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).await?; let default_group = Group::new( DEFAULT_GROUP_ID.to_owned(), field_rev.id.clone(), @@ -111,20 +107,21 @@ where } } -impl GroupControllerSharedAction for GenericGroupController +impl GroupControllerSharedOperation for GenericGroupController where P: CellBytesParser, + C: GroupConfigurationContent, Self: GroupAction, { fn field_id(&self) -> &str { &self.field_id } - fn fill_groups(&mut self, row_revs: &[Arc], field_rev: &FieldRevision) -> FlowyResult> { - // if self.configuration.is_none() { - // return Ok(vec![]); - // } + fn groups(&self) -> &[Group] { + todo!() + } + fn fill_groups(&mut self, row_revs: &[Arc], field_rev: &FieldRevision) -> FlowyResult> { for row_rev in row_revs { if let Some(cell_rev) = row_rev.cells.get(&self.field_id) { let mut group_rows: Vec = vec![]; diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs index dc278af7f5..59a53ae72a 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs @@ -1,13 +1,11 @@ use crate::entities::GroupRowsChangesetPB; use crate::services::field::{CheckboxCellData, CheckboxCellDataParser, CheckboxTypeOptionPB, CHECK, UNCHECK}; use crate::services::group::action::GroupAction; -use crate::services::group::configuration::{GenericGroupConfiguration, GroupConfigurationAction}; +use crate::services::group::configuration::GenericGroupConfiguration; +use crate::services::group::controller::{GenericGroupController, GroupController, GroupGenerator}; use crate::services::group::entities::Group; -use crate::services::group::group_controller::{GenericGroupController, GroupController, GroupGenerator}; -use flowy_error::FlowyResult; -use flowy_grid_data_model::revision::{ - CheckboxGroupConfigurationRevision, FieldRevision, GroupRecordRevision, RowChangeset, RowRevision, -}; + +use flowy_grid_data_model::revision::{CheckboxGroupConfigurationRevision, FieldRevision, RowChangeset, RowRevision}; pub type CheckboxGroupController = GenericGroupController< CheckboxGroupConfigurationRevision, @@ -17,23 +15,6 @@ pub type CheckboxGroupController = GenericGroupController< >; pub type CheckboxGroupConfiguration = GenericGroupConfiguration; -impl GroupConfigurationAction for CheckboxGroupConfiguration { - fn group_records(&self) -> &[GroupRecordRevision] { - &[] - } - - fn merge_groups(&self, groups: Vec) -> FlowyResult<()> { - Ok(()) - } - - fn hide_group(&self, group_id: &str) -> FlowyResult<()> { - Ok(()) - } - - fn show_group(&self, group_id: &str) -> FlowyResult<()> { - Ok(()) - } -} impl GroupAction for CheckboxGroupController { type CellDataType = CheckboxCellData; @@ -82,8 +63,8 @@ impl GroupGenerator for CheckboxGroupGenerator { fn generate_groups( field_id: &str, - configuration: &Self::ConfigurationType, - type_option: &Option, + _configuration: &Self::ConfigurationType, + _type_option: &Option, ) -> Vec { let check_group = Group::new( "true".to_string(), diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/mod.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/mod.rs index a2f393d093..0d7b8fa03e 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/mod.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/mod.rs @@ -4,3 +4,4 @@ mod util; pub use multi_select_controller::*; pub use single_select_controller::*; +pub use util::*; diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs index 33cd0613cd..e2e7f13ef9 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs @@ -3,9 +3,9 @@ use crate::services::cell::insert_select_option_cell; use crate::services::field::{MultiSelectTypeOptionPB, SelectOptionCellDataPB, SelectOptionCellDataParser}; use crate::services::group::action::GroupAction; +use crate::services::group::controller::{GenericGroupController, GroupController, GroupGenerator}; use crate::services::group::controller_impls::select_option_controller::util::*; use crate::services::group::entities::Group; -use crate::services::group::group_controller::{GenericGroupController, GroupController, GroupGenerator}; use flowy_grid_data_model::revision::{ FieldRevision, RowChangeset, RowRevision, SelectOptionGroupConfigurationRevision, }; @@ -88,7 +88,7 @@ impl GroupGenerator for MultiSelectGroupGenerator { type TypeOptionType = MultiSelectTypeOptionPB; fn generate_groups( field_id: &str, - configuration: &Self::ConfigurationType, + _configuration: &Self::ConfigurationType, type_option: &Option, ) -> Vec { match type_option { diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs index 4c8360f427..44920a0f1f 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs @@ -3,9 +3,9 @@ use crate::services::cell::insert_select_option_cell; use crate::services::field::{SelectOptionCellDataPB, SelectOptionCellDataParser, SingleSelectTypeOptionPB}; use crate::services::group::action::GroupAction; +use crate::services::group::controller::{GenericGroupController, GroupController, GroupGenerator}; use crate::services::group::controller_impls::select_option_controller::util::*; use crate::services::group::entities::Group; -use crate::services::group::group_controller::{GenericGroupController, GroupController, GroupGenerator}; use flowy_grid_data_model::revision::{ FieldRevision, RowChangeset, RowRevision, SelectOptionGroupConfigurationRevision, @@ -89,7 +89,7 @@ impl GroupGenerator for SingleSelectGroupGenerator { type TypeOptionType = SingleSelectTypeOptionPB; fn generate_groups( field_id: &str, - configuration: &Self::ConfigurationType, + _configuration: &Self::ConfigurationType, type_option: &Option, ) -> Vec { match type_option { 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 b6439f1f12..6675f26aa4 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 @@ -1,33 +1,14 @@ use crate::entities::{GroupRowsChangesetPB, InsertedRowPB, RowPB}; use crate::services::cell::insert_select_option_cell; use crate::services::field::SelectOptionCellDataPB; -use crate::services::group::configuration::{GenericGroupConfiguration, GroupConfigurationAction}; +use crate::services::group::configuration::GenericGroupConfiguration; use crate::services::group::Group; -use flowy_error::FlowyResult; + use flowy_grid_data_model::revision::{ - FieldRevision, GroupRecordRevision, RowChangeset, RowRevision, SelectOptionGroupConfigurationRevision, + FieldRevision, RowChangeset, RowRevision, SelectOptionGroupConfigurationRevision, }; pub type SelectOptionGroupConfiguration = GenericGroupConfiguration; -impl GroupConfigurationAction for SelectOptionGroupConfiguration { - fn group_records(&self) -> &[GroupRecordRevision] { - // self.configuration.as_slice() - todo!() - } - - fn merge_groups(&self, groups: Vec) -> FlowyResult<()> { - // self.writer.save_group_configuration() - todo!() - } - - fn hide_group(&self, group_id: &str) -> FlowyResult<()> { - todo!() - } - - fn show_group(&self, group_id: &str) -> FlowyResult<()> { - todo!() - } -} pub fn add_row( group: &mut Group, diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs index 0b60b54535..7cd4e387b9 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs @@ -1,7 +1,8 @@ use crate::services::group::configuration::GroupConfigurationReader; -use crate::services::group::group_controller::GroupController; +use crate::services::group::controller::GroupController; use crate::services::group::{ - CheckboxGroupController, Group, GroupConfigurationWriter, MultiSelectGroupController, SingleSelectGroupController, + CheckboxGroupConfiguration, CheckboxGroupController, Group, GroupConfigurationWriter, MultiSelectGroupController, + SelectOptionGroupConfiguration, SingleSelectGroupController, }; use flowy_error::FlowyResult; use flowy_grid_data_model::revision::{ @@ -35,8 +36,8 @@ impl GroupService { } pub(crate) async fn groups(&self) -> Vec { - // if let Some(group_action_handler) = self.group_controller.as_ref() { - // group_action_handler.read().await.groups() + // if let Some(group_controller) = self.group_controller.as_ref() { + // group_controller.read().await.groups() // } else { // vec![] // } @@ -182,30 +183,33 @@ impl GroupService { // let generator = GroupGenerator::::from_configuration(configuration); } FieldType::SingleSelect => { - let controller = SingleSelectGroupController::new( - field_rev, + let configuration = SelectOptionGroupConfiguration::new( + field_rev.clone(), self.configuration_reader.clone(), self.configuration_writer.clone(), ) .await?; + let controller = SingleSelectGroupController::new(field_rev, configuration).await?; group_controller = Some(Arc::new(RwLock::new(controller))); } FieldType::MultiSelect => { - let controller = MultiSelectGroupController::new( - field_rev, + let configuration = SelectOptionGroupConfiguration::new( + field_rev.clone(), self.configuration_reader.clone(), self.configuration_writer.clone(), ) .await?; + let controller = MultiSelectGroupController::new(field_rev, configuration).await?; group_controller = Some(Arc::new(RwLock::new(controller))); } FieldType::Checkbox => { - let controller = CheckboxGroupController::new( - field_rev, + let configuration = CheckboxGroupConfiguration::new( + field_rev.clone(), self.configuration_reader.clone(), self.configuration_writer.clone(), ) .await?; + let controller = CheckboxGroupController::new(field_rev, configuration).await?; group_controller = Some(Arc::new(RwLock::new(controller))) } FieldType::URL => { @@ -229,7 +233,7 @@ fn find_group_field(field_revs: &[Arc]) -> Option GroupConfigurationRevision { let field_id = field_rev.id.clone(); - let field_type_rev = field_rev.ty.clone(); + let field_type_rev = field_rev.ty; let field_type: FieldType = field_rev.ty.into(); match field_type { FieldType::RichText => { diff --git a/frontend/rust-lib/flowy-grid/src/services/group/mod.rs b/frontend/rust-lib/flowy-grid/src/services/group/mod.rs index 19536b8c5b..2bc979c28d 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/mod.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/mod.rs @@ -1,8 +1,8 @@ mod action; mod configuration; +mod controller; mod controller_impls; mod entities; -mod group_controller; mod group_service; pub(crate) use configuration::*; diff --git a/frontend/rust-lib/flowy-grid/tests/grid/field_test/script.rs b/frontend/rust-lib/flowy-grid/tests/grid/field_test/script.rs index ebe3d3adf2..c790285fb1 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/field_test/script.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/field_test/script.rs @@ -1,7 +1,6 @@ use crate::grid::grid_editor::GridEditorTest; -use flowy_grid::entities::InsertFieldParams; +use flowy_grid::entities::{FieldChangesetParams, InsertFieldParams}; use flowy_grid_data_model::revision::FieldRevision; -use flowy_sync::entities::grid::FieldChangesetParams; pub enum FieldScript { CreateField { diff --git a/frontend/rust-lib/flowy-grid/tests/grid/field_test/test.rs b/frontend/rust-lib/flowy-grid/tests/grid/field_test/test.rs index 98cb3d0324..3e255d3662 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/field_test/test.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/field_test/test.rs @@ -1,10 +1,10 @@ use crate::grid::field_test::script::FieldScript::*; use crate::grid::field_test::script::GridFieldTest; use crate::grid::field_test::util::*; +use flowy_grid::entities::FieldChangesetParams; use flowy_grid::services::field::selection_type_option::SelectOptionPB; use flowy_grid::services::field::SingleSelectTypeOptionPB; use flowy_grid_data_model::revision::TypeOptionDataEntry; -use flowy_sync::entities::grid::FieldChangesetParams; #[tokio::test] async fn grid_create_field() { diff --git a/frontend/rust-lib/flowy-grid/tests/grid/filter_test/script.rs b/frontend/rust-lib/flowy-grid/tests/grid/filter_test/script.rs index 459cf5f82d..948528589f 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/filter_test/script.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/filter_test/script.rs @@ -3,10 +3,9 @@ #![allow(dead_code)] #![allow(unused_imports)] -use flowy_grid::entities::{CreateGridFilterPayloadPB, GridLayout, GridSettingPB}; +use flowy_grid::entities::{CreateFilterParams, CreateGridFilterPayloadPB, DeleteFilterParams, GridLayout, GridSettingChangesetParams, GridSettingPB}; use flowy_grid::services::setting::GridSettingChangesetBuilder; use flowy_grid_data_model::revision::{FieldRevision, FieldTypeRevision}; -use flowy_sync::entities::grid::{CreateFilterParams, DeleteFilterParams, GridSettingChangesetParams}; use crate::grid::grid_editor::GridEditorTest; pub enum FilterScript { diff --git a/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs b/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs index ea8de69056..78cfc17b26 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs @@ -12,9 +12,6 @@ use flowy_grid::services::setting::GridSettingChangesetBuilder; use flowy_grid_data_model::revision::*; use flowy_revision::REVISION_WRITE_INTERVAL_IN_MILLIS; use flowy_sync::client_grid::GridBuilder; -use flowy_sync::entities::grid::{ - CreateFilterParams, DeleteFilterParams, FieldChangesetParams, GridSettingChangesetParams, -}; use flowy_test::helper::ViewTest; use flowy_test::FlowySDKTest; use std::collections::HashMap; diff --git a/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs index c4d2287c0c..9d7da79e5a 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs @@ -3,8 +3,21 @@ use serde::{Deserialize, Serialize}; use serde_json::Error; use serde_repr::*; -pub trait GroupConfigurationContentSerde: Sized { +pub trait GroupConfigurationContent: Sized { fn from_configuration_content(s: &str) -> Result; + + fn get_groups(&self) -> &[GroupRecordRevision] { + &[] + } + + fn mut_group(&mut self, _group_id: &str, _f: F) + where + F: FnOnce(&mut GroupRecordRevision), + { + } + + fn set_groups(&mut self, _new_groups: Vec) {} + fn to_configuration_content(&self) -> Result; } @@ -36,7 +49,7 @@ pub struct TextGroupConfigurationRevision { pub hide_empty: bool, } -impl GroupConfigurationContentSerde for TextGroupConfigurationRevision { +impl GroupConfigurationContent for TextGroupConfigurationRevision { fn from_configuration_content(s: &str) -> Result { serde_json::from_str(s) } @@ -50,7 +63,7 @@ pub struct NumberGroupConfigurationRevision { pub hide_empty: bool, } -impl GroupConfigurationContentSerde for NumberGroupConfigurationRevision { +impl GroupConfigurationContent for NumberGroupConfigurationRevision { fn from_configuration_content(s: &str) -> Result { serde_json::from_str(s) } @@ -64,7 +77,7 @@ pub struct UrlGroupConfigurationRevision { pub hide_empty: bool, } -impl GroupConfigurationContentSerde for UrlGroupConfigurationRevision { +impl GroupConfigurationContent for UrlGroupConfigurationRevision { fn from_configuration_content(s: &str) -> Result { serde_json::from_str(s) } @@ -78,7 +91,7 @@ pub struct CheckboxGroupConfigurationRevision { pub hide_empty: bool, } -impl GroupConfigurationContentSerde for CheckboxGroupConfigurationRevision { +impl GroupConfigurationContent for CheckboxGroupConfigurationRevision { fn from_configuration_content(s: &str) -> Result { serde_json::from_str(s) } @@ -94,17 +107,35 @@ pub struct SelectOptionGroupConfigurationRevision { pub groups: Vec, } -impl GroupConfigurationContentSerde for SelectOptionGroupConfigurationRevision { +impl GroupConfigurationContent for SelectOptionGroupConfigurationRevision { fn from_configuration_content(s: &str) -> Result { serde_json::from_str(s) } + fn get_groups(&self) -> &[GroupRecordRevision] { + &self.groups + } + + fn mut_group(&mut self, group_id: &str, f: F) + where + F: FnOnce(&mut GroupRecordRevision), + { + match self.groups.iter_mut().find(|group| group.group_id == group_id) { + None => {} + Some(group) => f(group), + } + } + + fn set_groups(&mut self, new_groups: Vec) { + self.groups = new_groups; + } + fn to_configuration_content(&self) -> Result { serde_json::to_string(self) } } -#[derive(Default, Serialize, Deserialize)] +#[derive(Clone, Default, Serialize, Deserialize)] pub struct GroupRecordRevision { pub group_id: String, @@ -113,13 +144,22 @@ pub struct GroupRecordRevision { } const DEFAULT_GROUP_RECORD_VISIBILITY: fn() -> bool = || true; +impl GroupRecordRevision { + pub fn new(group_id: String) -> Self { + Self { + group_id, + visible: true, + } + } +} + #[derive(Default, Serialize, Deserialize)] pub struct DateGroupConfigurationRevision { pub hide_empty: bool, pub condition: DateCondition, } -impl GroupConfigurationContentSerde for DateGroupConfigurationRevision { +impl GroupConfigurationContent for DateGroupConfigurationRevision { fn from_configuration_content(s: &str) -> Result { serde_json::from_str(s) } diff --git a/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs b/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs index 3f5f799369..fc5fa4bf67 100644 --- a/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs +++ b/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs @@ -70,15 +70,15 @@ impl GridViewRevisionPad { &mut self, field_id: &str, field_type: &FieldTypeRevision, - group_id: &str, - mut_group_fn: F, + configuration_id: &str, + mut_configuration_fn: F, ) -> CollaborateResult> { self.modify(|view| match view.groups.get_mut_objects(field_id, field_type) { None => Ok(None), - Some(groups) => { - for mut group in groups { - if group.id == group_id { - mut_group_fn(Arc::make_mut(&mut group)); + Some(configurations_revs) => { + for configuration_rev in configurations_revs { + if configuration_rev.id == configuration_id { + mut_configuration_fn(Arc::make_mut(configuration_rev)); return Ok(Some(())); } } @@ -170,6 +170,7 @@ impl GridViewRevisionPad { } } +#[derive(Debug)] pub struct GridViewRevisionChangeset { pub delta: TextDelta, pub md5: String, From 23efbc00c166dd28adda7a77ffa6098318604569 Mon Sep 17 00:00:00 2001 From: appflowy Date: Sun, 21 Aug 2022 13:56:06 +0800 Subject: [PATCH 09/86] ci: fix unit test --- .../flowy-grid/src/dart_notification.rs | 3 +- .../group_entities/group_changeset.rs | 4 +- .../rust-lib/flowy-grid/src/event_handler.rs | 17 +--- frontend/rust-lib/flowy-grid/src/event_map.rs | 4 +- .../flowy-grid/src/services/grid_editor.rs | 15 ++-- .../src/services/grid_view_editor.rs | 62 ++++++++++---- .../src/services/grid_view_manager.rs | 28 ++++--- .../src/services/group/configuration.rs | 84 ++++++++++++------- .../src/services/group/controller.rs | 17 ++-- .../multi_select_controller.rs | 8 +- .../single_select_controller.rs | 8 +- .../src/services/group/group_service.rs | 11 ++- .../tests/grid/filter_test/script.rs | 21 +---- .../tests/grid/group_test/script.rs | 21 ++++- .../src/revision/group_rev.rs | 29 +++---- .../src/client_grid/view_revision_pad.rs | 14 ---- 16 files changed, 193 insertions(+), 153 deletions(-) diff --git a/frontend/rust-lib/flowy-grid/src/dart_notification.rs b/frontend/rust-lib/flowy-grid/src/dart_notification.rs index 0bba5bbc11..a0030c6773 100644 --- a/frontend/rust-lib/flowy-grid/src/dart_notification.rs +++ b/frontend/rust-lib/flowy-grid/src/dart_notification.rs @@ -11,7 +11,8 @@ pub enum GridNotification { DidUpdateRow = 30, DidUpdateCell = 40, DidUpdateField = 50, - DidUpdateGroup = 60, + DidUpdateGroupView = 60, + DidUpdateGroup = 61, } impl std::default::Default for GridNotification { diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs index 41d46e3cc7..dbee60dca9 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs @@ -104,7 +104,7 @@ impl TryInto for MoveGroupPayloadPB { } #[derive(Debug, Default, ProtoBuf)] -pub struct GroupsChangesetPB { +pub struct GroupViewChangesetPB { #[pb(index = 1)] pub view_id: String, @@ -115,4 +115,4 @@ pub struct GroupsChangesetPB { pub deleted_groups: Vec, } -impl GroupsChangesetPB {} +impl GroupViewChangesetPB {} diff --git a/frontend/rust-lib/flowy-grid/src/event_handler.rs b/frontend/rust-lib/flowy-grid/src/event_handler.rs index 47df88b976..149f7017a3 100644 --- a/frontend/rust-lib/flowy-grid/src/event_handler.rs +++ b/frontend/rust-lib/flowy-grid/src/event_handler.rs @@ -35,17 +35,6 @@ pub(crate) async fn get_grid_setting_handler( data_result(grid_setting) } -#[tracing::instrument(level = "trace", skip(data, manager), err)] -pub(crate) async fn update_grid_setting_handler( - data: Data, - manager: AppData>, -) -> Result<(), FlowyError> { - let params: GridSettingChangesetParams = data.into_inner().try_into()?; - let editor = manager.open_grid(¶ms.grid_id).await?; - let _ = editor.update_grid_setting(params).await?; - Ok(()) -} - #[tracing::instrument(level = "debug", skip(data, manager), err)] pub(crate) async fn get_grid_blocks_handler( data: Data, @@ -441,9 +430,9 @@ pub(crate) async fn create_board_card_handler( pub(crate) async fn move_group_handler( data: Data, manager: AppData>, -) -> DataResult { +) -> FlowyResult<()> { let params: MoveGroupParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(params.view_id.as_ref())?; - let changeset = editor.move_group(params).await?; - data_result(changeset) + let _ = editor.move_group(params).await?; + Ok(()) } diff --git a/frontend/rust-lib/flowy-grid/src/event_map.rs b/frontend/rust-lib/flowy-grid/src/event_map.rs index 76087fe64b..844ae7f5c3 100644 --- a/frontend/rust-lib/flowy-grid/src/event_map.rs +++ b/frontend/rust-lib/flowy-grid/src/event_map.rs @@ -11,7 +11,7 @@ pub fn create(grid_manager: Arc) -> Module { .event(GridEvent::GetGrid, get_grid_handler) .event(GridEvent::GetGridBlocks, get_grid_blocks_handler) .event(GridEvent::GetGridSetting, get_grid_setting_handler) - .event(GridEvent::UpdateGridSetting, update_grid_setting_handler) + // .event(GridEvent::UpdateGridSetting, update_grid_setting_handler) // Field .event(GridEvent::GetFields, get_fields_handler) .event(GridEvent::UpdateField, update_field_handler) @@ -219,6 +219,6 @@ pub enum GridEvent { #[event(input = "CreateBoardCardPayloadPB", output = "RowPB")] CreateBoardCard = 110, - #[event(input = "MoveGroupPayloadPB", output = "GroupsChangesetPB")] + #[event(input = "MoveGroupPayloadPB")] MoveGroup = 111, } diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index dabb77e1de..93c48b0880 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -343,9 +343,9 @@ impl GridRevisionEditor { Ok(row_pb) } - pub async fn move_group(&self, _params: MoveGroupParams) -> FlowyResult { - // - todo!() + pub async fn move_group(&self, params: MoveGroupParams) -> FlowyResult<()> { + let _ = self.view_manager.move_group(params).await?; + Ok(()) } pub async fn insert_rows(&self, row_revs: Vec) -> FlowyResult> { @@ -514,8 +514,13 @@ impl GridRevisionEditor { self.view_manager.get_filters().await } - pub async fn update_grid_setting(&self, params: GridSettingChangesetParams) -> FlowyResult<()> { - let _ = self.view_manager.update_setting(params).await?; + pub async fn update_filter(&self, params: CreateFilterParams) -> FlowyResult<()> { + let _ = self.view_manager.update_filter(params).await?; + Ok(()) + } + + pub async fn delete_filter(&self, params: DeleteFilterParams) -> FlowyResult<()> { + let _ = self.view_manager.delete_filter(params).await?; Ok(()) } diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs index a9ade3f194..ff58585584 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs @@ -1,8 +1,8 @@ use crate::dart_notification::{send_dart_notification, GridNotification}; use crate::entities::{ - CreateRowParams, GridFilterConfiguration, GridLayout, GridLayoutPB, GridSettingChangesetParams, GridSettingPB, - GroupPB, GroupRowsChangesetPB, InsertedRowPB, RepeatedGridConfigurationFilterPB, RepeatedGridGroupConfigurationPB, - RowPB, + CreateFilterParams, CreateRowParams, DeleteFilterParams, GridFilterConfiguration, GridLayout, GridLayoutPB, + GridSettingChangesetParams, GridSettingPB, GroupPB, GroupRowsChangesetPB, GroupViewChangesetPB, InsertedRowPB, + MoveGroupParams, RepeatedGridConfigurationFilterPB, RepeatedGridGroupConfigurationPB, RowPB, }; use crate::services::grid_editor_task::GridServiceTaskScheduler; use crate::services::grid_view_manager::{GridViewFieldDelegate, GridViewRowDelegate}; @@ -11,7 +11,8 @@ use crate::services::group::{ }; use flowy_error::{FlowyError, FlowyResult}; use flowy_grid_data_model::revision::{ - FieldRevision, FieldTypeRevision, GroupConfigurationRevision, RowChangeset, RowRevision, + gen_grid_filter_id, FieldRevision, FieldTypeRevision, FilterConfigurationRevision, GroupConfigurationRevision, + RowChangeset, RowRevision, }; use flowy_revision::{RevisionCloudService, RevisionManager, RevisionObjectBuilder}; use flowy_sync::client_grid::{GridViewRevisionChangeset, GridViewRevisionPad}; @@ -99,7 +100,7 @@ impl GridViewRevisionEditor { index: None, }; let changeset = GroupRowsChangesetPB::insert(group_id.clone(), vec![inserted_row]); - self.notify_did_update_group(changeset).await; + self.notify_did_update_group_rows(changeset).await; } } } @@ -114,7 +115,7 @@ impl GridViewRevisionEditor { .await { for changeset in changesets { - self.notify_did_update_group(changeset).await; + self.notify_did_update_group_rows(changeset).await; } } } @@ -128,7 +129,7 @@ impl GridViewRevisionEditor { .await { for changeset in changesets { - self.notify_did_update_group(changeset).await; + self.notify_did_update_group_rows(changeset).await; } } } @@ -150,7 +151,7 @@ impl GridViewRevisionEditor { { for changeset in changesets { tracing::trace!("Group: {} changeset: {}", changeset.group_id, changeset); - self.notify_did_update_group(changeset).await; + self.notify_did_update_group_rows(changeset).await; } } } @@ -173,22 +174,19 @@ impl GridViewRevisionEditor { } else { self.group_service.read().await.groups().await }; - Ok(groups.into_iter().map(GroupPB::from).collect()) } + pub(crate) async fn move_group(&self, params: MoveGroupParams) -> FlowyResult<()> { + todo!() + } + pub(crate) async fn get_setting(&self) -> GridSettingPB { let field_revs = self.field_delegate.get_field_revs().await; let grid_setting = make_grid_setting(&*self.pad.read().await, &field_revs); grid_setting } - pub(crate) async fn update_setting(&self, _changeset: GridSettingChangesetParams) -> FlowyResult<()> { - // let _ = self.modify(|pad| Ok(pad.update_setting(changeset)?)).await; - // Ok(()) - todo!() - } - pub(crate) async fn get_filters(&self) -> Vec { let field_revs = self.field_delegate.get_field_revs().await; match self.pad.read().await.get_all_filters(&field_revs) { @@ -201,12 +199,44 @@ impl GridViewRevisionEditor { } } - async fn notify_did_update_group(&self, changeset: GroupRowsChangesetPB) { + pub(crate) async fn insert_filter(&self, insert_filter: CreateFilterParams) -> FlowyResult<()> { + self.modify(|pad| { + let filter_rev = FilterConfigurationRevision { + id: gen_grid_filter_id(), + field_id: insert_filter.field_id.clone(), + condition: insert_filter.condition, + content: insert_filter.content, + }; + let changeset = pad.insert_filter(&insert_filter.field_id, &insert_filter.field_type_rev, filter_rev)?; + Ok(changeset) + }) + .await + } + + pub(crate) async fn delete_filter(&self, delete_filter: DeleteFilterParams) -> FlowyResult<()> { + self.modify(|pad| { + let changeset = pad.delete_filter( + &delete_filter.field_id, + &delete_filter.field_type_rev, + &delete_filter.filter_id, + )?; + Ok(changeset) + }) + .await + } + + async fn notify_did_update_group_rows(&self, changeset: GroupRowsChangesetPB) { send_dart_notification(&changeset.group_id, GridNotification::DidUpdateGroup) .payload(changeset) .send(); } + async fn notify_did_update_view(&self, changeset: GroupViewChangesetPB) { + send_dart_notification(&self.view_id, GridNotification::DidUpdateGroupView) + .payload(changeset) + .send(); + } + #[allow(dead_code)] async fn modify(&self, f: F) -> FlowyResult<()> where 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 5386bdb893..802560190a 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 @@ -1,5 +1,6 @@ use crate::entities::{ - CreateRowParams, GridFilterConfiguration, GridSettingChangesetParams, GridSettingPB, RepeatedGridGroupPB, RowPB, + CreateFilterParams, CreateRowParams, DeleteFilterParams, GridFilterConfiguration, GridSettingChangesetParams, + GridSettingPB, MoveGroupParams, RepeatedGridGroupPB, RowPB, }; use crate::manager::GridUser; use crate::services::grid_editor_task::GridServiceTaskScheduler; @@ -98,23 +99,33 @@ impl GridViewManager { Ok(view_editor.get_setting().await) } - pub(crate) async fn update_setting(&self, params: GridSettingChangesetParams) -> FlowyResult<()> { - let view_editor = self.get_default_view_editor().await?; - let _ = view_editor.update_setting(params).await?; - Ok(()) - } - pub(crate) async fn get_filters(&self) -> FlowyResult> { let view_editor = self.get_default_view_editor().await?; Ok(view_editor.get_filters().await) } + pub(crate) async fn update_filter(&self, insert_filter: CreateFilterParams) -> FlowyResult<()> { + let view_editor = self.get_default_view_editor().await?; + view_editor.insert_filter(insert_filter).await + } + + pub(crate) async fn delete_filter(&self, delete_filter: DeleteFilterParams) -> FlowyResult<()> { + let view_editor = self.get_default_view_editor().await?; + view_editor.delete_filter(delete_filter).await + } + pub(crate) async fn load_groups(&self) -> FlowyResult { let view_editor = self.get_default_view_editor().await?; let groups = view_editor.load_groups().await?; Ok(RepeatedGridGroupPB { items: groups }) } + pub(crate) async fn move_group(&self, params: MoveGroupParams) -> FlowyResult<()> { + let view_editor = self.get_default_view_editor().await?; + let _s = view_editor.move_group(params).await?; + Ok(()) + } + /// It may generate a RowChangeset when the Row was moved from one group to another. /// The return value, [RowChangeset], contains the changes made by the groups. /// @@ -131,9 +142,6 @@ impl GridViewManager { } } - #[allow(dead_code)] - pub(crate) async fn move_group(&self) {} - pub(crate) async fn get_view_editor(&self, view_id: &str) -> FlowyResult> { debug_assert!(!view_id.is_empty()); match self.view_editors.get(view_id) { 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 15cbe6c61f..9c1562769c 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs @@ -4,6 +4,7 @@ use flowy_grid_data_model::revision::{ FieldRevision, FieldTypeRevision, GroupConfigurationContent, GroupConfigurationRevision, GroupRecordRevision, }; +use indexmap::IndexMap; use lib_infra::future::AFFuture; use std::sync::Arc; @@ -23,10 +24,9 @@ pub trait GroupConfigurationWriter: Send + Sync + 'static { pub struct GenericGroupConfiguration { pub configuration: C, - // pub groups_map: IndexMap, configuration_id: String, field_rev: Arc, - reader: Arc, + groups_map: IndexMap, writer: Arc, } @@ -45,30 +45,33 @@ where Ok(Self { configuration_id, field_rev, - reader, + groups_map: IndexMap::new(), writer, configuration, }) } - #[allow(dead_code)] - fn group_records(&self) -> &[GroupRecordRevision] { - todo!() + pub(crate) fn groups(&self) -> Vec<&Group> { + self.groups_map.values().collect() } - pub(crate) async fn merge_groups(&mut self, groups: &[Group]) -> FlowyResult<()> { - match merge_groups(self.configuration.get_groups(), groups) { - None => Ok(()), - Some(new_groups) => { - self.configuration.set_groups(new_groups); - let _ = self.save_configuration().await?; - Ok(()) - } - } + + pub(crate) fn clone_groups(&self) -> Vec { + self.groups_map.values().cloned().collect() + } + + pub(crate) async fn merge_groups(&mut self, groups: Vec) -> FlowyResult<()> { + let (group_revs, groups) = merge_groups(self.configuration.get_groups(), groups); + self.configuration.set_groups(group_revs); + let _ = self.save_configuration().await?; + groups.into_iter().for_each(|group| { + self.groups_map.insert(group.id.clone(), group); + }); + Ok(()) } #[allow(dead_code)] pub(crate) async fn hide_group(&mut self, group_id: &str) -> FlowyResult<()> { - self.configuration.mut_group(group_id, |group_rev| { + self.configuration.with_mut_group(group_id, |group_rev| { group_rev.visible = false; }); let _ = self.save_configuration().await?; @@ -77,13 +80,27 @@ where #[allow(dead_code)] pub(crate) async fn show_group(&mut self, group_id: &str) -> FlowyResult<()> { - self.configuration.mut_group(group_id, |group_rev| { + self.configuration.with_mut_group(group_id, |group_rev| { group_rev.visible = true; }); let _ = self.save_configuration().await?; Ok(()) } + pub(crate) fn with_mut_groups(&mut self, mut mut_groups_fn: impl FnMut(&mut Group)) { + self.groups_map.iter_mut().for_each(|(_, group)| { + mut_groups_fn(group); + }) + } + + pub(crate) fn get_mut_group(&mut self, group_id: &str) -> Option<&mut Group> { + self.groups_map.get_mut(group_id) + } + + pub(crate) fn get_group(&mut self, group_id: &str) -> Option<&Group> { + self.groups_map.get(group_id) + } + pub async fn save_configuration(&self) -> FlowyResult<()> { let content = self.configuration.to_configuration_content()?; let _ = self @@ -103,29 +120,32 @@ where } } -fn merge_groups(old_group: &[GroupRecordRevision], groups: &[Group]) -> Option> { +fn merge_groups(old_group_revs: &[GroupRecordRevision], groups: Vec) -> (Vec, Vec) { // tracing::trace!("Merge group: old: {}, new: {}", old_group.len(), groups.len()); - if old_group.is_empty() { + if old_group_revs.is_empty() { let new_groups = groups .iter() .map(|group| GroupRecordRevision::new(group.id.clone())) .collect(); - return Some(new_groups); + return (new_groups, groups); } - let new_groups = groups - .iter() - .filter(|group| !old_group.iter().any(|group_rev| group_rev.group_id == group.id)) - .collect::>(); + let mut group_map: IndexMap = IndexMap::new(); + groups.into_iter().for_each(|group| { + group_map.insert(group.id.clone(), group); + }); - if new_groups.is_empty() { - return None; + let mut sorted_groups: Vec = vec![]; + for group_rev in old_group_revs { + if let Some(group) = group_map.remove(&group_rev.group_id) { + sorted_groups.push(group); + } } - - let mut old_group = old_group.to_vec(); - let new_groups = new_groups + sorted_groups.extend(group_map.into_values().collect::>()); + let new_group_revs = sorted_groups .iter() - .map(|group| GroupRecordRevision::new(group.id.clone())); - old_group.extend(new_groups); - Some(old_group) + .map(|group| GroupRecordRevision::new(group.id.clone())) + .collect::>(); + + (new_group_revs, sorted_groups) } 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 44344fa0a1..1cba496b51 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller.rs @@ -7,7 +7,6 @@ use flowy_error::FlowyResult; use flowy_grid_data_model::revision::{ FieldRevision, GroupConfigurationContent, RowChangeset, RowRevision, TypeOptionDataDeserializer, }; -use indexmap::IndexMap; use std::marker::PhantomData; use std::sync::Arc; @@ -36,7 +35,7 @@ pub trait GroupGenerator { pub trait GroupControllerSharedOperation: Send + Sync { // The field that is used for grouping the rows fn field_id(&self) -> &str; - fn groups(&self) -> &[Group]; + fn groups(&self) -> Vec; fn fill_groups(&mut self, row_revs: &[Arc], field_rev: &FieldRevision) -> FlowyResult>; fn did_update_row( &mut self, @@ -65,7 +64,6 @@ pub trait GroupControllerSharedOperation: Send + Sync { /// P: the parser that impl [CellBytesParser] for the CellBytes pub struct GenericGroupController { pub field_id: String, - pub groups_map: IndexMap, pub type_option: Option, pub configuration: GenericGroupConfiguration, /// default_group is used to store the rows that don't belong to any groups. @@ -87,7 +85,7 @@ where let field_type_rev = field_rev.ty; 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).await?; + let _ = configuration.merge_groups(groups).await?; let default_group = Group::new( DEFAULT_GROUP_ID.to_owned(), field_rev.id.clone(), @@ -97,7 +95,6 @@ where Ok(Self { field_id: field_rev.id.clone(), - groups_map: groups.into_iter().map(|group| (group.id.clone(), group)).collect(), default_group, type_option, configuration, @@ -117,8 +114,8 @@ where &self.field_id } - fn groups(&self) -> &[Group] { - todo!() + fn groups(&self) -> Vec { + self.configuration.clone_groups() } fn fill_groups(&mut self, row_revs: &[Arc], field_rev: &FieldRevision) -> FlowyResult> { @@ -127,7 +124,7 @@ where let mut group_rows: Vec = vec![]; let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), field_rev); let cell_data = cell_bytes.parser::

()?; - for group in self.groups_map.values() { + for group in self.configuration.groups() { if self.can_group(&group.content, &cell_data) { group_rows.push(GroupRow { row: row_rev.into(), @@ -140,7 +137,7 @@ where self.default_group.add_row(row_rev.into()); } else { for group_row in group_rows { - if let Some(group) = self.groups_map.get_mut(&group_row.group_id) { + if let Some(group) = self.configuration.get_mut_group(&group_row.group_id) { group.add_row(group_row.row); } } @@ -151,7 +148,7 @@ where } let default_group = self.default_group.clone(); - let mut groups: Vec = self.groups_map.values().cloned().collect(); + let mut groups: Vec = self.configuration.clone_groups(); if !default_group.number_of_row() == 0 { groups.push(default_group); } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs index e2e7f13ef9..47e7dfbce9 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs @@ -27,7 +27,7 @@ impl GroupAction for MultiSelectGroupController { fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec { let mut changesets = vec![]; - self.groups_map.iter_mut().for_each(|(_, group): (_, &mut Group)| { + self.configuration.with_mut_groups(|group| { add_row(group, &mut changesets, cell_data, row_rev); }); changesets @@ -39,7 +39,7 @@ impl GroupAction for MultiSelectGroupController { cell_data: &Self::CellDataType, ) -> Vec { let mut changesets = vec![]; - self.groups_map.iter_mut().for_each(|(_, group): (_, &mut Group)| { + self.configuration.with_mut_groups(|group| { remove_row(group, &mut changesets, cell_data, row_rev); }); changesets @@ -54,7 +54,7 @@ impl GroupAction for MultiSelectGroupController { to_row_id: &str, ) -> Vec { let mut group_changeset = vec![]; - self.groups_map.iter_mut().for_each(|(_, group): (_, &mut Group)| { + self.configuration.with_mut_groups(|group| { move_row( group, &mut group_changeset, @@ -71,7 +71,7 @@ impl GroupAction for MultiSelectGroupController { impl GroupController for MultiSelectGroupController { fn will_create_row(&mut self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str) { - let group: Option<&Group> = self.groups_map.get(group_id); + let group: Option<&Group> = self.configuration.get_group(group_id); match group { None => tracing::warn!("Can not find the group: {}", group_id), Some(group) => { diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs index 44920a0f1f..2eb1e4a60f 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs @@ -27,7 +27,7 @@ impl GroupAction for SingleSelectGroupController { fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec { let mut changesets = vec![]; - self.groups_map.iter_mut().for_each(|(_, group): (_, &mut Group)| { + self.configuration.with_mut_groups(|group| { add_row(group, &mut changesets, cell_data, row_rev); }); changesets @@ -39,7 +39,7 @@ impl GroupAction for SingleSelectGroupController { cell_data: &Self::CellDataType, ) -> Vec { let mut changesets = vec![]; - self.groups_map.iter_mut().for_each(|(_, group): (_, &mut Group)| { + self.configuration.with_mut_groups(|group| { remove_row(group, &mut changesets, cell_data, row_rev); }); changesets @@ -54,7 +54,7 @@ impl GroupAction for SingleSelectGroupController { to_row_id: &str, ) -> Vec { let mut group_changeset = vec![]; - self.groups_map.iter_mut().for_each(|(_, group): (_, &mut Group)| { + self.configuration.with_mut_groups(|group| { move_row( group, &mut group_changeset, @@ -71,7 +71,7 @@ impl GroupAction for SingleSelectGroupController { impl GroupController for SingleSelectGroupController { fn will_create_row(&mut self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str) { - let group: Option<&mut Group> = self.groups_map.get_mut(group_id); + let group: Option<&mut Group> = self.configuration.get_mut_group(group_id); match group { None => {} Some(group) => { diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs index 7cd4e387b9..a1cdd597af 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs @@ -36,12 +36,11 @@ impl GroupService { } pub(crate) async fn groups(&self) -> Vec { - // if let Some(group_controller) = self.group_controller.as_ref() { - // group_controller.read().await.groups() - // } else { - // vec![] - // } - todo!() + if let Some(group_controller) = self.group_controller.as_ref() { + group_controller.read().await.groups() + } else { + vec![] + } } pub(crate) async fn load_groups( diff --git a/frontend/rust-lib/flowy-grid/tests/grid/filter_test/script.rs b/frontend/rust-lib/flowy-grid/tests/grid/filter_test/script.rs index 948528589f..a1f0b8a0c6 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/filter_test/script.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/filter_test/script.rs @@ -9,10 +9,6 @@ use flowy_grid_data_model::revision::{FieldRevision, FieldTypeRevision}; use crate::grid::grid_editor::GridEditorTest; pub enum FilterScript { - #[allow(dead_code)] - UpdateGridSetting { - params: GridSettingChangesetParams, - }, InsertGridTableFilter { payload: CreateGridFilterPayloadPB, }, @@ -49,27 +45,18 @@ impl GridFilterTest { pub async fn run_script(&mut self, script: FilterScript) { match script { - FilterScript::UpdateGridSetting { params } => { - let _ = self.editor.update_grid_setting(params).await.unwrap(); - } + FilterScript::InsertGridTableFilter { payload } => { let params: CreateFilterParams = payload.try_into().unwrap(); - let layout_type = GridLayout::Table; - let params = GridSettingChangesetBuilder::new(&self.grid_id, &layout_type) - .insert_filter(params) - .build(); - let _ = self.editor.update_grid_setting(params).await.unwrap(); + let _ = self.editor.update_filter(params).await.unwrap(); } FilterScript::AssertTableFilterCount { count } => { let filters = self.editor.get_grid_filter().await.unwrap(); assert_eq!(count as usize, filters.len()); } FilterScript::DeleteGridTableFilter { filter_id, field_rev} => { - let layout_type = GridLayout::Table; - let params = GridSettingChangesetBuilder::new(&self.grid_id, &layout_type) - .delete_filter(DeleteFilterParams { field_id: field_rev.id, filter_id, field_type_rev: field_rev.ty }) - .build(); - let _ = self.editor.update_grid_setting(params).await.unwrap(); + let params = DeleteFilterParams { field_id: field_rev.id, filter_id, field_type_rev: field_rev.ty }; + let _ = self.editor.delete_filter(params).await.unwrap(); } FilterScript::AssertGridSetting { expected_setting } => { let setting = self.editor.get_grid_setting().await.unwrap(); 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 1900e89bff..118d417c31 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 @@ -1,5 +1,5 @@ use crate::grid::grid_editor::GridEditorTest; -use flowy_grid::entities::{CreateRowParams, FieldType, GridLayout, GroupPB, MoveRowParams, RowPB}; +use flowy_grid::entities::{CreateRowParams, FieldType, GridLayout, GroupPB, MoveGroupParams, MoveRowParams, RowPB}; use flowy_grid::services::cell::insert_select_option_cell; use flowy_grid_data_model::revision::RowChangeset; @@ -32,6 +32,10 @@ pub enum GroupScript { row_index: usize, to_group_index: usize, }, + MoveGroup { + from_group_index: usize, + to_group_index: usize, + }, } pub struct GridGroupTest { @@ -84,7 +88,6 @@ impl GridGroupTest { // let group = self.group_at_index(group_index).await; let compare_row = group.rows.get(row_index).unwrap().clone(); - assert_eq!(row.id, compare_row.id); } GroupScript::CreateRow { group_index } => { @@ -125,6 +128,20 @@ impl GridGroupTest { row_changeset.cell_by_field_id.insert(field_id, cell_rev); self.editor.update_row(row_changeset).await.unwrap(); } + GroupScript::MoveGroup { + from_group_index, + to_group_index, + } => { + let from_group = self.group_at_index(from_group_index).await; + let to_group = self.group_at_index(to_group_index).await; + let params = MoveGroupParams { + view_id: self.editor.grid_id.clone(), + from_group_id: from_group.group_id, + to_group_id: to_group.group_id, + }; + self.editor.move_group(params).await.unwrap(); + // + } } } diff --git a/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs index 9d7da79e5a..b8c9567cb4 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs @@ -6,19 +6,19 @@ use serde_repr::*; pub trait GroupConfigurationContent: Sized { fn from_configuration_content(s: &str) -> Result; + fn to_configuration_content(&self) -> Result; + fn get_groups(&self) -> &[GroupRecordRevision] { &[] } - fn mut_group(&mut self, _group_id: &str, _f: F) + fn set_groups(&mut self, _new_groups: Vec) {} + + fn with_mut_group(&mut self, _group_id: &str, _f: F) where F: FnOnce(&mut GroupRecordRevision), { } - - fn set_groups(&mut self, _new_groups: Vec) {} - - fn to_configuration_content(&self) -> Result; } #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] @@ -112,11 +112,19 @@ impl GroupConfigurationContent for SelectOptionGroupConfigurationRevision { serde_json::from_str(s) } + fn to_configuration_content(&self) -> Result { + serde_json::to_string(self) + } + fn get_groups(&self) -> &[GroupRecordRevision] { &self.groups } - fn mut_group(&mut self, group_id: &str, f: F) + fn set_groups(&mut self, new_groups: Vec) { + self.groups = new_groups; + } + + fn with_mut_group(&mut self, group_id: &str, f: F) where F: FnOnce(&mut GroupRecordRevision), { @@ -125,14 +133,6 @@ impl GroupConfigurationContent for SelectOptionGroupConfigurationRevision { Some(group) => f(group), } } - - fn set_groups(&mut self, new_groups: Vec) { - self.groups = new_groups; - } - - fn to_configuration_content(&self) -> Result { - serde_json::to_string(self) - } } #[derive(Clone, Default, Serialize, Deserialize)] @@ -142,6 +142,7 @@ pub struct GroupRecordRevision { #[serde(default = "DEFAULT_GROUP_RECORD_VISIBILITY")] pub visible: bool, } + const DEFAULT_GROUP_RECORD_VISIBILITY: fn() -> bool = || true; impl GroupRecordRevision { diff --git a/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs b/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs index fc5fa4bf67..e63c5d2088 100644 --- a/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs +++ b/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs @@ -52,20 +52,6 @@ impl GridViewRevisionPad { self.groups.get_all_objects(field_revs) } - pub fn insert_group_configuration( - &mut self, - field_id: &str, - field_type: &FieldTypeRevision, - group_rev: GroupConfigurationRevision, - ) -> CollaborateResult> { - self.modify(|view| { - // only one group can be set - view.groups.remove_all(); - view.groups.insert_object(field_id, field_type, group_rev); - Ok(Some(())) - }) - } - pub fn get_mut_group( &mut self, field_id: &str, From 93f5b5d754b4c621ce4236ab83323c9dde18ac4c Mon Sep 17 00:00:00 2001 From: appflowy Date: Sun, 21 Aug 2022 22:47:24 +0800 Subject: [PATCH 10/86] fix: save group rev without apply change --- frontend/rust-lib/flowy-error/src/errors.rs | 1 + .../group_entities/group_changeset.rs | 11 +- .../src/services/grid_view_editor.rs | 106 +++++++++++++----- .../src/services/grid_view_manager.rs | 10 +- .../src/services/group/configuration.rs | 67 ++++++++--- .../src/services/group/controller.rs | 11 ++ .../multi_select_controller.rs | 5 +- .../src/services/group/group_service.rs | 21 +++- .../tests/grid/group_test/script.rs | 15 ++- .../flowy-grid/tests/grid/group_test/test.rs | 50 ++++++--- frontend/rust-lib/flowy-sdk/src/lib.rs | 2 +- shared-lib/flowy-error-code/src/code.rs | 3 + .../src/revision/grid_setting_rev.rs | 2 +- .../src/revision/grid_view.rs | 105 +++++------------ .../src/revision/group_rev.rs | 19 ++++ .../src/client_grid/view_revision_pad.rs | 23 +++- 16 files changed, 302 insertions(+), 149 deletions(-) diff --git a/frontend/rust-lib/flowy-error/src/errors.rs b/frontend/rust-lib/flowy-error/src/errors.rs index e19ebb15b2..bc453054aa 100644 --- a/frontend/rust-lib/flowy-error/src/errors.rs +++ b/frontend/rust-lib/flowy-error/src/errors.rs @@ -66,6 +66,7 @@ impl FlowyError { static_flowy_error!(user_not_exist, ErrorCode::UserNotExist); static_flowy_error!(text_too_long, ErrorCode::TextTooLong); static_flowy_error!(invalid_data, ErrorCode::InvalidData); + static_flowy_error!(out_of_bounds, ErrorCode::OutOfBounds); } impl std::convert::From for FlowyError { diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs index dbee60dca9..a3ebee9cb7 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs @@ -109,10 +109,19 @@ pub struct GroupViewChangesetPB { pub view_id: String, #[pb(index = 2)] - pub inserted_groups: Vec, + pub inserted_groups: Vec, #[pb(index = 3)] pub deleted_groups: Vec, } impl GroupViewChangesetPB {} + +#[derive(Debug, Default, ProtoBuf)] +pub struct InsertedGroupPB { + #[pb(index = 1)] + pub group: GroupPB, + + #[pb(index = 2)] + pub index: i32, +} diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs index ff58585584..617c9a30d5 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs @@ -1,14 +1,12 @@ use crate::dart_notification::{send_dart_notification, GridNotification}; use crate::entities::{ CreateFilterParams, CreateRowParams, DeleteFilterParams, GridFilterConfiguration, GridLayout, GridLayoutPB, - GridSettingChangesetParams, GridSettingPB, GroupPB, GroupRowsChangesetPB, GroupViewChangesetPB, InsertedRowPB, + GridSettingPB, GroupPB, GroupRowsChangesetPB, GroupViewChangesetPB, InsertedGroupPB, InsertedRowPB, MoveGroupParams, RepeatedGridConfigurationFilterPB, RepeatedGridGroupConfigurationPB, RowPB, }; use crate::services::grid_editor_task::GridServiceTaskScheduler; use crate::services::grid_view_manager::{GridViewFieldDelegate, GridViewRowDelegate}; -use crate::services::group::{ - default_group_configuration, GroupConfigurationReader, GroupConfigurationWriter, GroupService, -}; +use crate::services::group::{GroupConfigurationReader, GroupConfigurationWriter, GroupService}; use flowy_error::{FlowyError, FlowyResult}; use flowy_grid_data_model::revision::{ gen_grid_filter_id, FieldRevision, FieldTypeRevision, FilterConfigurationRevision, GroupConfigurationRevision, @@ -19,6 +17,7 @@ use flowy_sync::client_grid::{GridViewRevisionChangeset, GridViewRevisionPad}; use flowy_sync::entities::revision::Revision; use lib_infra::future::{wrap_future, AFFuture, FutureResult}; use std::collections::HashMap; + use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use tokio::sync::RwLock; @@ -37,6 +36,7 @@ pub struct GridViewRevisionEditor { } impl GridViewRevisionEditor { + #[tracing::instrument(level = "trace", skip_all, err)] pub(crate) async fn new( user_id: &str, token: &str, @@ -109,7 +109,7 @@ impl GridViewRevisionEditor { // Send the group notification if the current view has groups; if let Some(changesets) = self .group_service - .write() + .read() .await .did_delete_row(row_rev, |field_id| self.field_delegate.get_field_rev(&field_id)) .await @@ -123,7 +123,7 @@ impl GridViewRevisionEditor { pub(crate) async fn did_update_row(&self, row_rev: &RowRevision) { if let Some(changesets) = self .group_service - .write() + .read() .await .did_update_row(row_rev, |field_id| self.field_delegate.get_field_rev(&field_id)) .await @@ -142,7 +142,7 @@ impl GridViewRevisionEditor { ) { if let Some(changesets) = self .group_service - .write() + .read() .await .did_move_row(row_rev, row_changeset, upper_row_id, |field_id| { self.field_delegate.get_field_rev(&field_id) @@ -156,11 +156,13 @@ impl GridViewRevisionEditor { } } + #[tracing::instrument(level = "trace", skip(self))] pub(crate) async fn load_groups(&self) -> FlowyResult> { let groups = if !self.did_load_group.load(Ordering::SeqCst) { self.did_load_group.store(true, Ordering::SeqCst); let field_revs = self.field_delegate.get_field_revs().await; let row_revs = self.row_delegate.gv_row_revs().await; + match self .group_service .write() @@ -174,11 +176,37 @@ impl GridViewRevisionEditor { } else { self.group_service.read().await.groups().await }; + + tracing::trace!("Number of groups: {}", groups.len()); Ok(groups.into_iter().map(GroupPB::from).collect()) } pub(crate) async fn move_group(&self, params: MoveGroupParams) -> FlowyResult<()> { - todo!() + let _ = self + .group_service + .read() + .await + .move_group(¶ms.from_group_id, ¶ms.to_group_id) + .await?; + + match self.group_service.read().await.get_group(¶ms.from_group_id).await { + None => {} + Some((index, group)) => { + let inserted_group = InsertedGroupPB { + group: GroupPB::from(group), + index: index as i32, + }; + + let changeset = GroupViewChangesetPB { + view_id: "".to_string(), + inserted_groups: vec![inserted_group], + deleted_groups: vec![params.from_group_id.clone()], + }; + + self.notify_did_update_view(changeset).await; + } + } + Ok(()) } pub(crate) async fn get_setting(&self) -> GridSettingPB { @@ -291,17 +319,19 @@ impl RevisionObjectBuilder for GridViewRevisionPadBuilder { struct GroupConfigurationReaderImpl(Arc>); impl GroupConfigurationReader for GroupConfigurationReaderImpl { - fn get_group_configuration(&self, field_rev: Arc) -> AFFuture> { + fn get_group_configuration( + &self, + field_rev: Arc, + ) -> AFFuture>> { let view_pad = self.0.clone(); wrap_future(async move { - let view_pad = view_pad.read().await; - let configurations = view_pad.get_groups(&field_rev.id, &field_rev.ty); - match configurations { - None => { - let default_configuration = default_group_configuration(&field_rev); - Arc::new(default_configuration) - } - Some(configuration) => configuration, + let mut groups = view_pad.read().await.groups.get_objects(&field_rev.id, &field_rev.ty)?; + + if groups.is_empty() { + None + } else { + debug_assert_eq!(groups.len(), 1); + Some(groups.pop().unwrap()) } }) } @@ -328,17 +358,25 @@ impl GroupConfigurationWriter for GroupConfigurationWriterImpl { let field_id = field_id.to_owned(); wrap_future(async move { - match view_pad.write().await.get_mut_group( - &field_id, - &field_type, - &configuration_id, - |group_configuration| { - group_configuration.content = content; - }, - )? { - None => Ok(()), - Some(changeset) => apply_change(&user_id, rev_manager, changeset).await, + let is_contained = view_pad.read().await.contains_group(&field_id, &field_type); + let changeset = if is_contained { + view_pad.write().await.with_mut_group( + &field_id, + &field_type, + &configuration_id, + |group_configuration| { + group_configuration.content = content; + }, + )? + } else { + let group_rev = GroupConfigurationRevision::new(field_id.clone(), field_type, content)?; + view_pad.write().await.insert_group(&field_id, &field_type, group_rev)? + }; + + if let Some(changeset) = changeset { + let _ = apply_change(&user_id, rev_manager, changeset).await?; } + Ok(()) }) } } @@ -371,3 +409,17 @@ pub fn make_grid_setting(view_pad: &GridViewRevisionPad, field_revs: &[Arc FlowyResult<()> { let view_editor = self.get_default_view_editor().await?; - let _s = view_editor.move_group(params).await?; + let _ = view_editor.move_group(params).await?; Ok(()) } @@ -175,12 +175,11 @@ async fn make_view_editor( row_delegate: Arc, scheduler: Arc, ) -> FlowyResult { - tracing::trace!("Open view:{} editor", view_id); - let rev_manager = make_grid_view_rev_manager(user, view_id).await?; let user_id = user.user_id()?; let token = user.token()?; let view_id = view_id.to_owned(); + GridViewRevisionEditor::new( &user_id, &token, @@ -194,7 +193,6 @@ async fn make_view_editor( } pub async fn make_grid_view_rev_manager(user: &Arc, view_id: &str) -> FlowyResult { - tracing::trace!("Open view:{} editor", view_id); let user_id = user.user_id()?; let pool = user.db_pool()?; 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 9c1562769c..f4a25d7304 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs @@ -1,5 +1,5 @@ -use crate::services::group::Group; -use flowy_error::FlowyResult; +use crate::services::group::{default_group_configuration, Group}; +use flowy_error::{FlowyError, FlowyResult}; use flowy_grid_data_model::revision::{ FieldRevision, FieldTypeRevision, GroupConfigurationContent, GroupConfigurationRevision, GroupRecordRevision, }; @@ -9,7 +9,10 @@ use lib_infra::future::AFFuture; use std::sync::Arc; pub trait GroupConfigurationReader: Send + Sync + 'static { - fn get_group_configuration(&self, field_rev: Arc) -> AFFuture>; + fn get_group_configuration( + &self, + field_rev: Arc, + ) -> AFFuture>>; } pub trait GroupConfigurationWriter: Send + Sync + 'static { @@ -39,7 +42,22 @@ where reader: Arc, writer: Arc, ) -> FlowyResult { - let configuration_rev = reader.get_group_configuration(field_rev.clone()).await; + let configuration_rev = match reader.get_group_configuration(field_rev.clone()).await { + None => { + let default_group_configuration = default_group_configuration(&field_rev); + writer + .save_group_configuration( + &field_rev.id, + field_rev.ty, + &default_group_configuration.id, + default_group_configuration.content.clone(), + ) + .await?; + Arc::new(default_group_configuration) + } + Some(configuration) => configuration, + }; + let configuration_id = configuration_rev.id.clone(); let configuration = C::from_configuration_content(&configuration_rev.content)?; Ok(Self { @@ -63,6 +81,8 @@ where let (group_revs, groups) = merge_groups(self.configuration.get_groups(), groups); self.configuration.set_groups(group_revs); let _ = self.save_configuration().await?; + + tracing::trace!("merge new groups: {}", groups.len()); groups.into_iter().for_each(|group| { self.groups_map.insert(group.id.clone(), group); }); @@ -97,8 +117,25 @@ where self.groups_map.get_mut(group_id) } - pub(crate) fn get_group(&mut self, group_id: &str) -> Option<&Group> { - self.groups_map.get(group_id) + pub(crate) fn move_group(&mut self, from_group_id: &str, to_group_id: &str) -> FlowyResult<()> { + let from_group_index = self.groups_map.get_index_of(from_group_id); + let to_group_index = self.groups_map.get_index_of(to_group_id); + match (from_group_index, to_group_index) { + (Some(from_index), Some(to_index)) => { + self.groups_map.swap_indices(from_index, to_index); + self.configuration.swap_group(from_group_id, to_group_id); + Ok(()) + } + _ => Err(FlowyError::out_of_bounds()), + } + } + + // Returns the index and group specified by the group_id + pub(crate) fn get_group(&self, group_id: &str) -> Option<(usize, &Group)> { + match (self.groups_map.get_index_of(group_id), self.groups_map.get(group_id)) { + (Some(index), Some(group)) => Some((index, group)), + _ => None, + } } pub async fn save_configuration(&self) -> FlowyResult<()> { @@ -111,17 +148,17 @@ where } } -impl GroupConfigurationReader for Arc -where - T: GroupConfigurationReader, -{ - fn get_group_configuration(&self, field_rev: Arc) -> AFFuture> { - (**self).get_group_configuration(field_rev) - } -} +// impl GroupConfigurationReader for Arc +// where +// T: GroupConfigurationReader, +// { +// fn get_group_configuration(&self, field_rev: Arc) -> AFFuture> { +// (**self).get_group_configuration(field_rev) +// } +// } fn merge_groups(old_group_revs: &[GroupRecordRevision], groups: Vec) -> (Vec, Vec) { - // tracing::trace!("Merge group: old: {}, new: {}", old_group.len(), groups.len()); + tracing::trace!("Merge group: old: {}, new: {}", old_group_revs.len(), groups.len()); if old_group_revs.is_empty() { let new_groups = groups .iter() 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 1cba496b51..af4d676b02 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller.rs @@ -36,7 +36,9 @@ pub trait GroupControllerSharedOperation: Send + Sync { // The field that is used for grouping the rows fn field_id(&self) -> &str; fn groups(&self) -> Vec; + fn get_group(&self, group_id: &str) -> Option<(usize, Group)>; fn fill_groups(&mut self, row_revs: &[Arc], field_rev: &FieldRevision) -> FlowyResult>; + fn move_group(&mut self, from_group_id: &str, to_group_id: &str) -> FlowyResult<()>; fn did_update_row( &mut self, row_rev: &RowRevision, @@ -118,6 +120,11 @@ where self.configuration.clone_groups() } + fn get_group(&self, group_id: &str) -> Option<(usize, Group)> { + let group = self.configuration.get_group(group_id)?; + Some((group.0, group.1.clone())) + } + fn fill_groups(&mut self, row_revs: &[Arc], field_rev: &FieldRevision) -> FlowyResult> { for row_rev in row_revs { if let Some(cell_rev) = row_rev.cells.get(&self.field_id) { @@ -156,6 +163,10 @@ where Ok(groups) } + fn move_group(&mut self, from_group_id: &str, to_group_id: &str) -> FlowyResult<()> { + self.configuration.move_group(from_group_id, to_group_id) + } + fn did_update_row( &mut self, row_rev: &RowRevision, diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs index 47e7dfbce9..7a208a884f 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs @@ -71,10 +71,9 @@ impl GroupAction for MultiSelectGroupController { impl GroupController for MultiSelectGroupController { fn will_create_row(&mut self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str) { - let group: Option<&Group> = self.configuration.get_group(group_id); - match group { + match self.configuration.get_group(group_id) { None => tracing::warn!("Can not find the group: {}", group_id), - Some(group) => { + Some((_, group)) => { let cell_rev = insert_select_option_cell(group.id.clone(), field_rev); row_rev.cells.insert(field_rev.id.clone(), cell_rev); } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs index a1cdd597af..7050c2c087 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs @@ -43,6 +43,14 @@ impl GroupService { } } + pub(crate) async fn get_group(&self, group_id: &str) -> Option<(usize, Group)> { + if let Some(group_controller) = self.group_controller.as_ref() { + group_controller.read().await.get_group(group_id) + } else { + None + } + } + pub(crate) async fn load_groups( &mut self, field_revs: &[Arc], @@ -164,9 +172,20 @@ impl GroupService { } } + #[tracing::instrument(level = "trace", skip_all)] + pub(crate) async fn move_group(&self, from_group_id: &str, to_group_id: &str) -> FlowyResult<()> { + match self.group_controller.as_ref() { + None => Ok(()), + Some(group_controller) => { + let _ = group_controller.write().await.move_group(from_group_id, to_group_id)?; + Ok(()) + } + } + } + #[tracing::instrument(level = "trace", skip_all)] async fn make_group_controller( - &mut self, + &self, field_type: &FieldType, field_rev: &Arc, ) -> FlowyResult>>> { 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 118d417c31..d284e69c76 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 @@ -4,11 +4,15 @@ use flowy_grid::services::cell::insert_select_option_cell; use flowy_grid_data_model::revision::RowChangeset; pub enum GroupScript { - AssertGroup { + AssertGroupRowCount { group_index: usize, row_count: usize, }, AssertGroupCount(usize), + AssertGroup { + group_index: usize, + expected_group: GroupPB, + }, AssertRow { group_index: usize, row_index: usize, @@ -56,7 +60,7 @@ impl GridGroupTest { pub async fn run_script(&mut self, script: GroupScript) { match script { - GroupScript::AssertGroup { group_index, row_count } => { + GroupScript::AssertGroupRowCount { group_index, row_count } => { assert_eq!(row_count, self.group_at_index(group_index).await.rows.len()); } GroupScript::AssertGroupCount(count) => { @@ -142,6 +146,13 @@ impl GridGroupTest { self.editor.move_group(params).await.unwrap(); // } + GroupScript::AssertGroup { + group_index, + expected_group: group_pb, + } => { + let group = self.group_at_index(group_index).await; + assert_eq!(group.group_id, group_pb.group_id); + } } } 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 798f478439..6092e97a5e 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,15 +6,15 @@ async fn board_init_test() { let mut test = GridGroupTest::new().await; let scripts = vec![ AssertGroupCount(3), - AssertGroup { + AssertGroupRowCount { group_index: 0, row_count: 2, }, - AssertGroup { + AssertGroupRowCount { group_index: 1, row_count: 2, }, - AssertGroup { + AssertGroupRowCount { group_index: 2, row_count: 1, }, @@ -34,7 +34,7 @@ async fn board_move_row_test() { to_group_index: 0, to_row_index: 1, }, - AssertGroup { + AssertGroupRowCount { group_index: 0, row_count: 2, }, @@ -58,11 +58,11 @@ async fn board_move_row_to_other_group_test() { to_group_index: 1, to_row_index: 1, }, - AssertGroup { + AssertGroupRowCount { group_index: 0, row_count: 1, }, - AssertGroup { + AssertGroupRowCount { group_index: 1, row_count: 3, }, @@ -106,13 +106,13 @@ async fn board_create_row_test() { let mut test = GridGroupTest::new().await; let scripts = vec![ CreateRow { group_index: 0 }, - AssertGroup { + AssertGroupRowCount { group_index: 0, row_count: 3, }, CreateRow { group_index: 1 }, CreateRow { group_index: 1 }, - AssertGroup { + AssertGroupRowCount { group_index: 1, row_count: 4, }, @@ -128,7 +128,7 @@ async fn board_delete_row_test() { group_index: 0, row_index: 0, }, - AssertGroup { + AssertGroupRowCount { group_index: 0, row_count: 1, }, @@ -148,7 +148,7 @@ async fn board_delete_all_row_test() { group_index: 0, row_index: 0, }, - AssertGroup { + AssertGroupRowCount { group_index: 0, row_count: 0, }, @@ -166,11 +166,11 @@ async fn board_update_row_test() { row_index: 0, to_group_index: 1, }, - AssertGroup { + AssertGroupRowCount { group_index: 0, row_count: 1, }, - AssertGroup { + AssertGroupRowCount { group_index: 1, row_count: 3, }, @@ -188,14 +188,36 @@ async fn board_reorder_group_test() { row_index: 0, to_group_index: 1, }, - AssertGroup { + AssertGroupRowCount { group_index: 0, row_count: 1, }, - AssertGroup { + AssertGroupRowCount { group_index: 1, row_count: 3, }, ]; test.run_scripts(scripts).await; } + +#[tokio::test] +async fn board_move_group_test() { + let mut test = GridGroupTest::new().await; + let group_0 = test.group_at_index(0).await; + let group_1 = test.group_at_index(1).await; + let scripts = vec![ + MoveGroup { + from_group_index: 0, + to_group_index: 1, + }, + AssertGroup { + group_index: 0, + expected_group: group_1, + }, + AssertGroup { + group_index: 1, + expected_group: group_0, + }, + ]; + test.run_scripts(scripts).await; +} diff --git a/frontend/rust-lib/flowy-sdk/src/lib.rs b/frontend/rust-lib/flowy-sdk/src/lib.rs index ce5920e532..e0bd601987 100644 --- a/frontend/rust-lib/flowy-sdk/src/lib.rs +++ b/frontend/rust-lib/flowy-sdk/src/lib.rs @@ -74,7 +74,7 @@ fn crate_log_filter(level: String) -> String { filters.push(format!("lib_ot={}", level)); filters.push(format!("lib_ws={}", level)); filters.push(format!("lib_infra={}", level)); - // filters.push(format!("flowy_sync={}", level)); + filters.push(format!("flowy_sync={}", level)); // filters.push(format!("flowy_revision={}", level)); // filters.push(format!("lib_dispatch={}", level)); diff --git a/shared-lib/flowy-error-code/src/code.rs b/shared-lib/flowy-error-code/src/code.rs index 63f7ac7749..c5e89a99f3 100644 --- a/shared-lib/flowy-error-code/src/code.rs +++ b/shared-lib/flowy-error-code/src/code.rs @@ -125,6 +125,9 @@ pub enum ErrorCode { #[display(fmt = "Invalid data")] InvalidData = 1000, + + #[display(fmt = "Out of bounds")] + OutOfBounds = 10001, } impl ErrorCode { diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs index 01ca80a380..829ebe4c07 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs @@ -49,7 +49,7 @@ where .get_mut(field_id) .and_then(|object_rev_map| object_rev_map.get_mut(field_type)); if value.is_none() { - tracing::warn!("Can't find the {:?} with", std::any::type_name::()); + tracing::warn!("[Configuration] Can't find the {:?} with", std::any::type_name::()); } value } diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_view.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_view.rs index 9d4df3067e..74c3c72511 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/grid_view.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/grid_view.rs @@ -1,11 +1,7 @@ -use crate::revision::{ - FieldRevision, FieldTypeRevision, FilterConfiguration, FilterConfigurationRevision, FilterConfigurationsByFieldId, - GroupConfiguration, GroupConfigurationRevision, GroupConfigurationsByFieldId, -}; +use crate::revision::{FilterConfiguration, GroupConfiguration}; use nanoid::nanoid; use serde::{Deserialize, Serialize}; use serde_repr::*; -use std::sync::Arc; #[allow(dead_code)] pub fn gen_grid_view_id() -> String { @@ -40,15 +36,15 @@ pub struct GridViewRevision { pub layout: LayoutRevision, + #[serde(default)] pub filters: FilterConfiguration, #[serde(default)] pub groups: GroupConfiguration, - - // For the moment, we just use the order returned from the GridRevision - #[allow(dead_code)] - #[serde(skip, rename = "row")] - pub row_orders: Vec, + // // For the moment, we just use the order returned from the GridRevision + // #[allow(dead_code)] + // #[serde(skip, rename = "rows")] + // pub row_orders: Vec, } impl GridViewRevision { @@ -59,78 +55,33 @@ impl GridViewRevision { layout: Default::default(), filters: Default::default(), groups: Default::default(), - row_orders: vec![], + // row_orders: vec![], } } - - pub fn get_all_groups(&self, field_revs: &[Arc]) -> Option { - self.groups.get_all_objects(field_revs) - } - - pub fn get_groups( - &self, - field_id: &str, - field_type_rev: &FieldTypeRevision, - ) -> Option> { - let mut groups = self.groups.get_objects(field_id, field_type_rev)?; - if groups.is_empty() { - debug_assert_eq!(groups.len(), 1); - Some(groups.pop().unwrap()) - } else { - None - } - } - - pub fn get_mut_groups( - &mut self, - field_id: &str, - field_type: &FieldTypeRevision, - ) -> Option<&mut Vec>> { - self.groups.get_mut_objects(field_id, field_type) - } - - pub fn insert_group( - &mut self, - field_id: &str, - field_type: &FieldTypeRevision, - group_rev: GroupConfigurationRevision, - ) { - // only one group can be set - self.groups.remove_all(); - self.groups.insert_object(field_id, field_type, group_rev); - } - - pub fn get_all_filters(&self, field_revs: &[Arc]) -> Option { - self.filters.get_all_objects(field_revs) - } - - pub fn get_filters( - &self, - field_id: &str, - field_type_rev: &FieldTypeRevision, - ) -> Option>> { - self.filters.get_objects(field_id, field_type_rev) - } - - pub fn get_mut_filters( - &mut self, - field_id: &str, - field_type: &FieldTypeRevision, - ) -> Option<&mut Vec>> { - self.filters.get_mut_objects(field_id, field_type) - } - - pub fn insert_filter( - &mut self, - field_id: &str, - field_type: &FieldTypeRevision, - filter_rev: FilterConfigurationRevision, - ) { - self.filters.insert_object(field_id, field_type, filter_rev); - } } #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct RowOrderRevision { pub row_id: String, } + +#[cfg(test)] +mod tests { + use crate::revision::GridViewRevision; + + #[test] + fn grid_view_revision_serde_test() { + let grid_view_revision = GridViewRevision { + view_id: "1".to_string(), + grid_id: "1".to_string(), + layout: Default::default(), + filters: Default::default(), + groups: Default::default(), + }; + let s = serde_json::to_string(&grid_view_revision).unwrap(); + assert_eq!( + s, + r#"{"view_id":"1","grid_id":"1","layout":0,"filters":[],"groups":[]}"# + ); + } +} diff --git a/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs index b8c9567cb4..caaa1b43a9 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs @@ -12,8 +12,23 @@ pub trait GroupConfigurationContent: Sized { &[] } + fn mut_groups(&mut self) -> &mut Vec { + todo!() + } + fn set_groups(&mut self, _new_groups: Vec) {} + fn swap_group(&mut self, from_group_id: &str, to_group_id: &str) { + let from_index = self + .get_groups() + .iter() + .position(|group| group.group_id == from_group_id); + let to_index = self.get_groups().iter().position(|group| group.group_id == to_group_id); + if let (Some(from), Some(to)) = (from_index, to_index) { + self.mut_groups().swap(from, to); + } + } + fn with_mut_group(&mut self, _group_id: &str, _f: F) where F: FnOnce(&mut GroupRecordRevision), @@ -120,6 +135,10 @@ impl GroupConfigurationContent for SelectOptionGroupConfigurationRevision { &self.groups } + fn mut_groups(&mut self) -> &mut Vec { + &mut self.groups + } + fn set_groups(&mut self, new_groups: Vec) { self.groups = new_groups; } diff --git a/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs b/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs index e63c5d2088..57bd783d75 100644 --- a/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs +++ b/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs @@ -52,7 +52,26 @@ impl GridViewRevisionPad { self.groups.get_all_objects(field_revs) } - pub fn get_mut_group( + #[tracing::instrument(level = "trace", skip_all, err)] + pub fn insert_group( + &mut self, + field_id: &str, + field_type: &FieldTypeRevision, + group_rev: GroupConfigurationRevision, + ) -> CollaborateResult> { + self.modify(|view| { + view.groups.insert_object(field_id, field_type, group_rev); + Ok(Some(())) + }) + } + + #[tracing::instrument(level = "trace", skip_all)] + pub fn contains_group(&self, field_id: &str, field_type: &FieldTypeRevision) -> bool { + self.view.groups.get_objects(field_id, field_type).is_some() + } + + #[tracing::instrument(level = "trace", skip_all, err)] + pub fn with_mut_group( &mut self, field_id: &str, field_type: &FieldTypeRevision, @@ -147,6 +166,8 @@ impl GridViewRevisionPad { None => Ok(None), Some(delta) => { self.delta = self.delta.compose(&delta)?; + tracing::info!("GridView: {:?}", delta); + let md5 = md5(&self.delta.json_bytes()); Ok(Some(GridViewRevisionChangeset { delta, md5 })) } From 54dab3b1af277a70acd539dd562647deda8b1f37 Mon Sep 17 00:00:00 2001 From: yodatak Date: Sun, 21 Aug 2022 22:39:43 +0200 Subject: [PATCH 11/86] Update fr-FR.json --- frontend/app_flowy/assets/translations/fr-FR.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/app_flowy/assets/translations/fr-FR.json b/frontend/app_flowy/assets/translations/fr-FR.json index df4b795fa5..ffb96ad863 100644 --- a/frontend/app_flowy/assets/translations/fr-FR.json +++ b/frontend/app_flowy/assets/translations/fr-FR.json @@ -157,11 +157,11 @@ "textFieldName": "Texte", "checkboxFieldName": "Case à cocher", "dateFieldName": "Date", - "numberFieldName": "Nombres", + "numberFieldName": "Nombre", "singleSelectFieldName": "Sélectionner", "multiSelectFieldName": "Multisélection", "urlFieldName": "URL", - "numberFormat": " Format des nombres", + "numberFormat": " Format du nombre", "dateFormat": " Format de la date", "includeTime": " Inclure l'heure", "dateFormatFriendly": "Mois Jour, Année", From ffc6f141fad8de46630630297e7ba765a559a2e8 Mon Sep 17 00:00:00 2001 From: appflowy Date: Mon, 22 Aug 2022 08:53:02 +0800 Subject: [PATCH 12/86] chore: fix GroupConfigurationRevision deserialize error --- .../entities/group_entities/configuration.rs | 4 - frontend/rust-lib/flowy-grid/src/manager.rs | 1 - .../src/services/grid_view_editor.rs | 22 +--- .../src/services/group/configuration.rs | 124 ++++++++++++------ .../src/services/group/controller.rs | 6 +- .../src/services/group/group_service.rs | 8 +- .../src/revision/grid_setting_rev.rs | 16 +-- .../src/revision/group_rev.rs | 74 ++--------- 8 files changed, 116 insertions(+), 139 deletions(-) diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/configuration.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/configuration.rs index 5797da3517..cb5503727b 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/configuration.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/configuration.rs @@ -17,16 +17,12 @@ pub struct TextGroupConfigurationPB { pub struct SelectOptionGroupConfigurationPB { #[pb(index = 1)] hide_empty: bool, - - #[pb(index = 2)] - groups: Vec, } impl std::convert::From for SelectOptionGroupConfigurationPB { fn from(rev: SelectOptionGroupConfigurationRevision) -> Self { Self { hide_empty: rev.hide_empty, - groups: rev.groups.into_iter().map(GroupRecordPB::from).collect(), } } } diff --git a/frontend/rust-lib/flowy-grid/src/manager.rs b/frontend/rust-lib/flowy-grid/src/manager.rs index 9e4556b793..12b94ff8db 100644 --- a/frontend/rust-lib/flowy-grid/src/manager.rs +++ b/frontend/rust-lib/flowy-grid/src/manager.rs @@ -131,7 +131,6 @@ impl GridManager { async fn get_or_create_grid_editor(&self, grid_id: &str) -> FlowyResult> { match self.grid_editors.get(grid_id) { None => { - tracing::trace!("Create grid editor with id: {}", grid_id); let db_pool = self.grid_user.db_pool()?; let editor = self.make_grid_rev_editor(grid_id, db_pool).await?; diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs index 617c9a30d5..fb8706b175 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs @@ -348,30 +348,18 @@ impl GroupConfigurationWriter for GroupConfigurationWriterImpl { &self, field_id: &str, field_type: FieldTypeRevision, - configuration_id: &str, - content: String, + group_configuration: GroupConfigurationRevision, ) -> AFFuture> { let user_id = self.user_id.clone(); - let configuration_id = configuration_id.to_owned(); let rev_manager = self.rev_manager.clone(); let view_pad = self.view_pad.clone(); let field_id = field_id.to_owned(); wrap_future(async move { - let is_contained = view_pad.read().await.contains_group(&field_id, &field_type); - let changeset = if is_contained { - view_pad.write().await.with_mut_group( - &field_id, - &field_type, - &configuration_id, - |group_configuration| { - group_configuration.content = content; - }, - )? - } else { - let group_rev = GroupConfigurationRevision::new(field_id.clone(), field_type, content)?; - view_pad.write().await.insert_group(&field_id, &field_type, group_rev)? - }; + let changeset = view_pad + .write() + .await + .insert_group(&field_id, &field_type, group_configuration)?; if let Some(changeset) = changeset { let _ = apply_change(&user_id, rev_manager, 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 f4a25d7304..36d49a10e1 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs @@ -1,8 +1,9 @@ use crate::services::group::{default_group_configuration, Group}; use flowy_error::{FlowyError, FlowyResult}; use flowy_grid_data_model::revision::{ - FieldRevision, FieldTypeRevision, GroupConfigurationContent, GroupConfigurationRevision, GroupRecordRevision, + FieldRevision, FieldTypeRevision, GroupConfigurationContentSerde, GroupConfigurationRevision, GroupRecordRevision, }; +use std::marker::PhantomData; use indexmap::IndexMap; use lib_infra::future::AFFuture; @@ -20,14 +21,13 @@ pub trait GroupConfigurationWriter: Send + Sync + 'static { &self, field_id: &str, field_type: FieldTypeRevision, - configuration_id: &str, - content: String, + group_configuration: GroupConfigurationRevision, ) -> AFFuture>; } pub struct GenericGroupConfiguration { - pub configuration: C, - configuration_id: String, + pub configuration: Arc, + configuration_content: PhantomData, field_rev: Arc, groups_map: IndexMap, writer: Arc, @@ -35,37 +35,32 @@ pub struct GenericGroupConfiguration { impl GenericGroupConfiguration where - C: GroupConfigurationContent, + C: GroupConfigurationContentSerde, { + #[tracing::instrument(level = "trace", skip_all, err)] pub async fn new( field_rev: Arc, reader: Arc, writer: Arc, ) -> FlowyResult { - let configuration_rev = match reader.get_group_configuration(field_rev.clone()).await { + let configuration = match reader.get_group_configuration(field_rev.clone()).await { None => { let default_group_configuration = default_group_configuration(&field_rev); writer - .save_group_configuration( - &field_rev.id, - field_rev.ty, - &default_group_configuration.id, - default_group_configuration.content.clone(), - ) + .save_group_configuration(&field_rev.id, field_rev.ty, default_group_configuration.clone()) .await?; Arc::new(default_group_configuration) } Some(configuration) => configuration, }; - let configuration_id = configuration_rev.id.clone(); - let configuration = C::from_configuration_content(&configuration_rev.content)?; + // let configuration = C::from_configuration_content(&configuration_rev.content)?; Ok(Self { - configuration_id, field_rev, groups_map: IndexMap::new(), writer, configuration, + configuration_content: PhantomData, }) } @@ -78,11 +73,12 @@ where } pub(crate) async fn merge_groups(&mut self, groups: Vec) -> FlowyResult<()> { - let (group_revs, groups) = merge_groups(self.configuration.get_groups(), groups); - self.configuration.set_groups(group_revs); - let _ = self.save_configuration().await?; + let (group_revs, groups) = merge_groups(&self.configuration.groups, groups); + self.mut_configuration(move |configuration| { + configuration.groups = group_revs; + true + })?; - tracing::trace!("merge new groups: {}", groups.len()); groups.into_iter().for_each(|group| { self.groups_map.insert(group.id.clone(), group); }); @@ -91,19 +87,17 @@ where #[allow(dead_code)] pub(crate) async fn hide_group(&mut self, group_id: &str) -> FlowyResult<()> { - self.configuration.with_mut_group(group_id, |group_rev| { + self.mut_configuration_group(group_id, |group_rev| { group_rev.visible = false; - }); - let _ = self.save_configuration().await?; + })?; Ok(()) } #[allow(dead_code)] pub(crate) async fn show_group(&mut self, group_id: &str) -> FlowyResult<()> { - self.configuration.with_mut_group(group_id, |group_rev| { + self.mut_configuration_group(group_id, |group_rev| { group_rev.visible = true; - }); - let _ = self.save_configuration().await?; + })?; Ok(()) } @@ -123,7 +117,21 @@ where match (from_group_index, to_group_index) { (Some(from_index), Some(to_index)) => { self.groups_map.swap_indices(from_index, to_index); - self.configuration.swap_group(from_group_id, to_group_id); + + self.mut_configuration(|configuration| { + let from_index = configuration + .groups + .iter() + .position(|group| group.group_id == from_group_id); + let to_index = configuration + .groups + .iter() + .position(|group| group.group_id == to_group_id); + if let (Some(from), Some(to)) = (from_index, to_index) { + configuration.groups.swap(from, to); + } + true + })?; Ok(()) } _ => Err(FlowyError::out_of_bounds()), @@ -138,25 +146,55 @@ where } } - pub async fn save_configuration(&self) -> FlowyResult<()> { - let content = self.configuration.to_configuration_content()?; - let _ = self - .writer - .save_group_configuration(&self.field_rev.id, self.field_rev.ty, &self.configuration_id, content) - .await?; + pub fn save_configuration(&self) -> FlowyResult<()> { + let configuration = (&*self.configuration).clone(); + let writer = self.writer.clone(); + let field_id = self.field_rev.id.clone(); + let field_type = self.field_rev.ty.clone(); + tokio::spawn(async move { + match writer + .save_group_configuration(&field_id, field_type, configuration) + .await + { + Ok(_) => {} + Err(e) => { + tracing::error!("Save group configuration failed: {}", e); + } + } + }); + + Ok(()) + } + + fn mut_configuration_group( + &mut self, + group_id: &str, + mut_groups_fn: impl Fn(&mut GroupRecordRevision), + ) -> FlowyResult<()> { + self.mut_configuration(|configuration| { + match configuration.groups.iter_mut().find(|group| group.group_id == group_id) { + None => false, + Some(group_rev) => { + mut_groups_fn(group_rev); + true + } + } + }) + } + + fn mut_configuration( + &mut self, + mut_configuration_fn: impl FnOnce(&mut GroupConfigurationRevision) -> bool, + ) -> FlowyResult<()> { + let configuration = Arc::make_mut(&mut self.configuration); + let is_changed = mut_configuration_fn(configuration); + if is_changed { + let _ = self.save_configuration()?; + } Ok(()) } } -// impl GroupConfigurationReader for Arc -// where -// T: GroupConfigurationReader, -// { -// fn get_group_configuration(&self, field_rev: Arc) -> AFFuture> { -// (**self).get_group_configuration(field_rev) -// } -// } - fn merge_groups(old_group_revs: &[GroupRecordRevision], groups: Vec) -> (Vec, Vec) { tracing::trace!("Merge group: old: {}, new: {}", old_group_revs.len(), groups.len()); if old_group_revs.is_empty() { @@ -172,6 +210,7 @@ fn merge_groups(old_group_revs: &[GroupRecordRevision], groups: Vec) -> ( group_map.insert(group.id.clone(), group); }); + // Inert let mut sorted_groups: Vec = vec![]; for group_rev in old_group_revs { if let Some(group) = group_map.remove(&group_rev.group_id) { @@ -184,5 +223,6 @@ fn merge_groups(old_group_revs: &[GroupRecordRevision], groups: Vec) -> ( .map(|group| GroupRecordRevision::new(group.id.clone())) .collect::>(); + tracing::trace!("group revs: {}, groups: {}", new_group_revs.len(), sorted_groups.len()); (new_group_revs, sorted_groups) } 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 af4d676b02..13b4ccce63 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller.rs @@ -5,7 +5,7 @@ use crate::services::group::configuration::GenericGroupConfiguration; use crate::services::group::entities::Group; use flowy_error::FlowyResult; use flowy_grid_data_model::revision::{ - FieldRevision, GroupConfigurationContent, RowChangeset, RowRevision, TypeOptionDataDeserializer, + FieldRevision, GroupConfigurationContentSerde, RowChangeset, RowRevision, TypeOptionDataDeserializer, }; use std::marker::PhantomData; @@ -76,7 +76,7 @@ pub struct GenericGroupController { impl GenericGroupController where - C: GroupConfigurationContent, + C: GroupConfigurationContentSerde, T: TypeOptionDataDeserializer, G: GroupGenerator, TypeOptionType = T>, { @@ -109,7 +109,7 @@ where impl GroupControllerSharedOperation for GenericGroupController where P: CellBytesParser, - C: GroupConfigurationContent, + C: GroupConfigurationContentSerde, Self: GroupAction, { fn field_id(&self) -> &str { diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs index 7050c2c087..63a3b4b67b 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs @@ -71,12 +71,14 @@ impl GroupService { vec![] } }; - drop(write_guard); } Some(groups) } - Err(_) => Some(vec![]), + Err(err) => { + tracing::error!("Load group failed: {}", err); + Some(vec![]) + } } } @@ -183,7 +185,7 @@ impl GroupService { } } - #[tracing::instrument(level = "trace", skip_all)] + #[tracing::instrument(level = "trace", skip(self, field_rev), err)] async fn make_group_controller( &self, field_type: &FieldType, diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs index 829ebe4c07..61a8a386d5 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs @@ -25,11 +25,11 @@ pub type FilterConfigurationsByFieldId = HashMap; pub type GroupConfigurationsByFieldId = HashMap>>; -#[derive(Debug, Clone, Serialize, Deserialize, Default, Eq, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(transparent)] pub struct Configuration where - T: Debug + Clone + Default + Eq + PartialEq + serde::Serialize + serde::de::DeserializeOwned + 'static, + T: Debug + Clone + Default + serde::Serialize + serde::de::DeserializeOwned + 'static, { /// Key: field_id /// Value: this value contains key/value. @@ -41,7 +41,7 @@ where impl Configuration where - T: Debug + Clone + Default + Eq + PartialEq + serde::Serialize + serde::de::DeserializeOwned + 'static, + T: Debug + Clone + Default + serde::Serialize + serde::de::DeserializeOwned + 'static, { pub fn get_mut_objects(&mut self, field_id: &str, field_type: &FieldTypeRevision) -> Option<&mut Vec>> { let value = self @@ -93,11 +93,11 @@ where } } -#[derive(Debug, Clone, Serialize, Deserialize, Default, Eq, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(transparent)] pub struct ObjectIndexMap where - T: Debug + Clone + Default + Eq + PartialEq + serde::Serialize + serde::de::DeserializeOwned + 'static, + T: Debug + Clone + Default + serde::Serialize + serde::de::DeserializeOwned + 'static, { #[serde(with = "indexmap::serde_seq")] pub object_by_field_type: IndexMap>>, @@ -105,7 +105,7 @@ where impl ObjectIndexMap where - T: Debug + Clone + Default + Eq + PartialEq + serde::Serialize + serde::de::DeserializeOwned + 'static, + T: Debug + Clone + Default + serde::Serialize + serde::de::DeserializeOwned + 'static, { pub fn new() -> Self { ObjectIndexMap::default() @@ -114,7 +114,7 @@ where impl std::ops::Deref for ObjectIndexMap where - T: Debug + Clone + Default + Eq + PartialEq + serde::Serialize + serde::de::DeserializeOwned + 'static, + T: Debug + Clone + Default + serde::Serialize + serde::de::DeserializeOwned + 'static, { type Target = IndexMap>>; @@ -125,7 +125,7 @@ where impl std::ops::DerefMut for ObjectIndexMap where - T: Debug + Clone + Default + Eq + PartialEq + serde::Serialize + serde::de::DeserializeOwned + 'static, + T: Debug + Clone + Default + serde::Serialize + serde::de::DeserializeOwned + 'static, { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.object_by_field_type diff --git a/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs index caaa1b43a9..204bdf273f 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs @@ -3,57 +3,32 @@ use serde::{Deserialize, Serialize}; use serde_json::Error; use serde_repr::*; -pub trait GroupConfigurationContent: Sized { +pub trait GroupConfigurationContentSerde: Sized + Send + Sync { fn from_configuration_content(s: &str) -> Result; fn to_configuration_content(&self) -> Result; - - fn get_groups(&self) -> &[GroupRecordRevision] { - &[] - } - - fn mut_groups(&mut self) -> &mut Vec { - todo!() - } - - fn set_groups(&mut self, _new_groups: Vec) {} - - fn swap_group(&mut self, from_group_id: &str, to_group_id: &str) { - let from_index = self - .get_groups() - .iter() - .position(|group| group.group_id == from_group_id); - let to_index = self.get_groups().iter().position(|group| group.group_id == to_group_id); - if let (Some(from), Some(to)) = (from_index, to_index) { - self.mut_groups().swap(from, to); - } - } - - fn with_mut_group(&mut self, _group_id: &str, _f: F) - where - F: FnOnce(&mut GroupRecordRevision), - { - } } -#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct GroupConfigurationRevision { pub id: String, pub field_id: String, pub field_type_rev: FieldTypeRevision, + pub groups: Vec, pub content: String, } impl GroupConfigurationRevision { pub fn new(field_id: String, field_type: FieldTypeRevision, content: T) -> Result where - T: serde::Serialize, + T: GroupConfigurationContentSerde, { - let content = serde_json::to_string(&content)?; + let content = content.to_configuration_content()?; Ok(Self { id: gen_grid_group_id(), field_id, field_type_rev: field_type, + groups: vec![], content, }) } @@ -64,7 +39,7 @@ pub struct TextGroupConfigurationRevision { pub hide_empty: bool, } -impl GroupConfigurationContent for TextGroupConfigurationRevision { +impl GroupConfigurationContentSerde for TextGroupConfigurationRevision { fn from_configuration_content(s: &str) -> Result { serde_json::from_str(s) } @@ -78,7 +53,7 @@ pub struct NumberGroupConfigurationRevision { pub hide_empty: bool, } -impl GroupConfigurationContent for NumberGroupConfigurationRevision { +impl GroupConfigurationContentSerde for NumberGroupConfigurationRevision { fn from_configuration_content(s: &str) -> Result { serde_json::from_str(s) } @@ -92,7 +67,7 @@ pub struct UrlGroupConfigurationRevision { pub hide_empty: bool, } -impl GroupConfigurationContent for UrlGroupConfigurationRevision { +impl GroupConfigurationContentSerde for UrlGroupConfigurationRevision { fn from_configuration_content(s: &str) -> Result { serde_json::from_str(s) } @@ -106,7 +81,7 @@ pub struct CheckboxGroupConfigurationRevision { pub hide_empty: bool, } -impl GroupConfigurationContent for CheckboxGroupConfigurationRevision { +impl GroupConfigurationContentSerde for CheckboxGroupConfigurationRevision { fn from_configuration_content(s: &str) -> Result { serde_json::from_str(s) } @@ -119,10 +94,9 @@ impl GroupConfigurationContent for CheckboxGroupConfigurationRevision { #[derive(Default, Serialize, Deserialize)] pub struct SelectOptionGroupConfigurationRevision { pub hide_empty: bool, - pub groups: Vec, } -impl GroupConfigurationContent for SelectOptionGroupConfigurationRevision { +impl GroupConfigurationContentSerde for SelectOptionGroupConfigurationRevision { fn from_configuration_content(s: &str) -> Result { serde_json::from_str(s) } @@ -130,31 +104,9 @@ impl GroupConfigurationContent for SelectOptionGroupConfigurationRevision { fn to_configuration_content(&self) -> Result { serde_json::to_string(self) } - - fn get_groups(&self) -> &[GroupRecordRevision] { - &self.groups - } - - fn mut_groups(&mut self) -> &mut Vec { - &mut self.groups - } - - fn set_groups(&mut self, new_groups: Vec) { - self.groups = new_groups; - } - - fn with_mut_group(&mut self, group_id: &str, f: F) - where - F: FnOnce(&mut GroupRecordRevision), - { - match self.groups.iter_mut().find(|group| group.group_id == group_id) { - None => {} - Some(group) => f(group), - } - } } -#[derive(Clone, Default, Serialize, Deserialize)] +#[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct GroupRecordRevision { pub group_id: String, @@ -179,7 +131,7 @@ pub struct DateGroupConfigurationRevision { pub condition: DateCondition, } -impl GroupConfigurationContent for DateGroupConfigurationRevision { +impl GroupConfigurationContentSerde for DateGroupConfigurationRevision { fn from_configuration_content(s: &str) -> Result { serde_json::from_str(s) } From 7ae25f49fd3b7c040d3a5cb1e555a561f933b725 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 19 Aug 2022 23:22:59 +0800 Subject: [PATCH 13/86] chore: improve cursor display style --- .../src/render/rich_text/flowy_rich_text.dart | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart index fb4dbdc4a1..89df8a54b1 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart @@ -65,18 +65,35 @@ class _FlowyRichTextState extends State with Selectable { @override Rect? getCursorRectInPosition(Position position) { final textPosition = TextPosition(offset: position.offset); - final cursorOffset = - _renderParagraph.getOffsetForCaret(textPosition, Rect.zero); - final cursorHeight = widget.cursorHeight ?? - _renderParagraph.getFullHeightForCaret(textPosition) ?? - _placeholderRenderParagraph.getFullHeightForCaret(textPosition) ?? - 16.0; // default height + var cursorHeight = _renderParagraph.getFullHeightForCaret(textPosition); + var cursorOffset = + _renderParagraph.getOffsetForCaret(textPosition, Rect.zero); + if (cursorHeight == null) { + cursorHeight = + _placeholderRenderParagraph.getFullHeightForCaret(textPosition); + cursorOffset = _placeholderRenderParagraph.getOffsetForCaret( + textPosition, Rect.zero); + } + if (cursorHeight != null) { + // workaround: Calling the `getFullHeightForCaret` function will return + // the full height of rich text component instead of the plain text + // if we set the line height. + // So need to divide by the line height to get the expected value. + // + // And the default height of plain text is too short. Add a magic height + // to expand it. + const magicHeight = 3.0; + cursorOffset = cursorOffset.translate( + 0, (cursorHeight - cursorHeight / _lineHeight) / 2.0); + cursorHeight /= _lineHeight; + cursorHeight += magicHeight; + } final rect = Rect.fromLTWH( cursorOffset.dx - (widget.cursorWidth / 2), cursorOffset.dy, widget.cursorWidth, - cursorHeight, + widget.cursorHeight ?? cursorHeight ?? 16.0, ); return rect; } From f943aeacd7dd97162c7b6365ed744e3eef5b6c34 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 19 Aug 2022 23:22:59 +0800 Subject: [PATCH 14/86] chore: improve cursor display style --- .../src/render/rich_text/flowy_rich_text.dart | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart index fb4dbdc4a1..89df8a54b1 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart @@ -65,18 +65,35 @@ class _FlowyRichTextState extends State with Selectable { @override Rect? getCursorRectInPosition(Position position) { final textPosition = TextPosition(offset: position.offset); - final cursorOffset = - _renderParagraph.getOffsetForCaret(textPosition, Rect.zero); - final cursorHeight = widget.cursorHeight ?? - _renderParagraph.getFullHeightForCaret(textPosition) ?? - _placeholderRenderParagraph.getFullHeightForCaret(textPosition) ?? - 16.0; // default height + var cursorHeight = _renderParagraph.getFullHeightForCaret(textPosition); + var cursorOffset = + _renderParagraph.getOffsetForCaret(textPosition, Rect.zero); + if (cursorHeight == null) { + cursorHeight = + _placeholderRenderParagraph.getFullHeightForCaret(textPosition); + cursorOffset = _placeholderRenderParagraph.getOffsetForCaret( + textPosition, Rect.zero); + } + if (cursorHeight != null) { + // workaround: Calling the `getFullHeightForCaret` function will return + // the full height of rich text component instead of the plain text + // if we set the line height. + // So need to divide by the line height to get the expected value. + // + // And the default height of plain text is too short. Add a magic height + // to expand it. + const magicHeight = 3.0; + cursorOffset = cursorOffset.translate( + 0, (cursorHeight - cursorHeight / _lineHeight) / 2.0); + cursorHeight /= _lineHeight; + cursorHeight += magicHeight; + } final rect = Rect.fromLTWH( cursorOffset.dx - (widget.cursorWidth / 2), cursorOffset.dy, widget.cursorWidth, - cursorHeight, + widget.cursorHeight ?? cursorHeight ?? 16.0, ); return rect; } From 1f90f3027430f6f4f5e3a07ebef69ff419362d76 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 22 Aug 2022 17:34:16 +0800 Subject: [PATCH 15/86] feat: implement link menu --- .../appflowy_editor/assets/images/delete.svg | 6 + .../appflowy_editor/assets/images/link.svg | 4 + .../assets/images/toolbar/link.svg | 4 +- .../src/extensions/text_node_extensions.dart | 25 ++ .../lib/src/render/link_menu/link_menu.dart | 137 +++++++++++ .../src/render/selection/toolbar_widget.dart | 217 ------------------ .../lib/src/render/toolbar/toolbar_item.dart | 192 ++++++++++++++++ .../render/toolbar/toolbar_item_widget.dart | 35 +++ .../src/render/toolbar/toolbar_widget.dart | 82 +++++++ .../lib/src/service/input_service.dart | 21 +- .../lib/src/service/toolbar_service.dart | 5 +- 11 files changed, 498 insertions(+), 230 deletions(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/assets/images/delete.svg create mode 100644 frontend/app_flowy/packages/appflowy_editor/assets/images/link.svg create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart delete mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection/toolbar_widget.dart create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item_widget.dart create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/assets/images/delete.svg b/frontend/app_flowy/packages/appflowy_editor/assets/images/delete.svg new file mode 100644 index 0000000000..b7f242542d --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/assets/images/delete.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/app_flowy/packages/appflowy_editor/assets/images/link.svg b/frontend/app_flowy/packages/appflowy_editor/assets/images/link.svg new file mode 100644 index 0000000000..5fbcc8d787 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/assets/images/link.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/app_flowy/packages/appflowy_editor/assets/images/toolbar/link.svg b/frontend/app_flowy/packages/appflowy_editor/assets/images/toolbar/link.svg index 612e8377b6..279e7ac471 100644 --- a/frontend/app_flowy/packages/appflowy_editor/assets/images/toolbar/link.svg +++ b/frontend/app_flowy/packages/appflowy_editor/assets/images/toolbar/link.svg @@ -1,4 +1,4 @@ - - + + diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart index 1d7c68ab80..8416485a5a 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart @@ -6,6 +6,31 @@ import 'package:appflowy_editor/src/document/text_delta.dart'; import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; extension TextNodeExtension on TextNode { + dynamic getAttributeInSelection(Selection selection, String styleKey) { + final ops = delta.whereType(); + final startOffset = + selection.isBackward ? selection.start.offset : selection.end.offset; + final endOffset = + selection.isBackward ? selection.end.offset : selection.start.offset; + var start = 0; + for (final op in ops) { + if (start >= endOffset) { + break; + } + final length = op.length; + if (start < endOffset && start + length > startOffset) { + if (op.attributes?.containsKey(styleKey) == true) { + return op.attributes![styleKey]; + } + } + start += length; + } + return null; + } + + bool allSatisfyLinkInSelection(Selection selection) => + allSatisfyInSelection(StyleKey.href, null, selection); + bool allSatisfyBoldInSelection(Selection selection) => allSatisfyInSelection(StyleKey.bold, true, selection); diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart new file mode 100644 index 0000000000..3ebf87f4fa --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart @@ -0,0 +1,137 @@ +import 'package:appflowy_editor/src/infra/flowy_svg.dart'; +import 'package:flutter/material.dart'; + +class LinkMenu extends StatefulWidget { + const LinkMenu({ + Key? key, + this.linkText, + required this.onSubmitted, + required this.onCopyLink, + required this.onRemoveLink, + }) : super(key: key); + + final String? linkText; + final void Function(String text) onSubmitted; + final VoidCallback onCopyLink; + final VoidCallback onRemoveLink; + + @override + State createState() => _LinkMenuState(); +} + +class _LinkMenuState extends State { + final _textEditingController = TextEditingController(); + + @override + void initState() { + super.initState(); + + _textEditingController.text = widget.linkText ?? ''; + } + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 350, + height: 200, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + blurRadius: 5, + spreadRadius: 1, + color: Colors.black.withOpacity(0.1), + ), + ], + borderRadius: BorderRadius.circular(6.0), + ), + child: SizedBox( + width: 350, + height: 200, + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeader(), + const SizedBox(height: 16.0), + _buildInput(), + const SizedBox(height: 16.0), + _buildIconButton( + iconName: 'link', + text: 'Copy link', + onPressed: widget.onCopyLink, + ), + _buildIconButton( + iconName: 'delete', + text: 'Remove link', + onPressed: widget.onRemoveLink, + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildHeader() { + return const Text( + 'Add your link', + style: TextStyle( + color: Colors.grey, + fontWeight: FontWeight.bold, + ), + ); + } + + Widget _buildInput() { + return TextField( + autofocus: true, + style: const TextStyle(fontSize: 14.0), + textAlign: TextAlign.left, + controller: _textEditingController, + onSubmitted: widget.onSubmitted, + decoration: const InputDecoration( + hintText: 'URL', + hintStyle: TextStyle(fontSize: 14.0), + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + borderSide: BorderSide(color: Color(0xFFBDBDBD)), + ), + contentPadding: EdgeInsets.all(16.0), + isDense: true, + ), + ); + } + + Widget _buildIconButton({ + required String iconName, + required String text, + required VoidCallback onPressed, + }) { + return TextButton.icon( + icon: FlowySvg( + name: iconName, + width: 20.0, + height: 20.0, + ), + style: TextButton.styleFrom( + minimumSize: const Size.fromHeight(40), + padding: EdgeInsets.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + alignment: Alignment.centerLeft, + ), + label: Text( + text, + textAlign: TextAlign.left, + style: const TextStyle( + color: Colors.black, + fontSize: 14.0, + ), + ), + onPressed: onPressed, + ); + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection/toolbar_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection/toolbar_widget.dart deleted file mode 100644 index 4c2b621795..0000000000 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection/toolbar_widget.dart +++ /dev/null @@ -1,217 +0,0 @@ -import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; -import 'package:flutter/material.dart'; - -import 'package:appflowy_editor/src/editor_state.dart'; -import 'package:appflowy_editor/src/infra/flowy_svg.dart'; -import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart'; - -typedef ToolbarEventHandler = void Function(EditorState editorState); - -typedef ToolbarEventHandlers = Map; - -ToolbarEventHandlers defaultToolbarEventHandlers = { - 'bold': (editorState) => formatBold(editorState), - 'italic': (editorState) => formatItalic(editorState), - 'strikethrough': (editorState) => formatStrikethrough(editorState), - 'underline': (editorState) => formatUnderline(editorState), - 'quote': (editorState) => formatQuote(editorState), - 'bulleted_list': (editorState) => formatBulletedList(editorState), - 'highlight': (editorState) => formatHighlight(editorState), - 'Text': (editorState) => formatText(editorState), - 'h1': (editorState) => formatHeading(editorState, StyleKey.h1), - 'h2': (editorState) => formatHeading(editorState, StyleKey.h2), - 'h3': (editorState) => formatHeading(editorState, StyleKey.h3), -}; - -List defaultListToolbarEventNames = [ - 'Text', - 'H1', - 'H2', - 'H3', -]; - -mixin ToolbarMixin on State { - void hide(); -} - -class ToolbarWidget extends StatefulWidget { - const ToolbarWidget({ - Key? key, - required this.editorState, - required this.layerLink, - required this.offset, - required this.handlers, - }) : super(key: key); - - final EditorState editorState; - final LayerLink layerLink; - final Offset offset; - final ToolbarEventHandlers handlers; - - @override - State createState() => _ToolbarWidgetState(); -} - -class _ToolbarWidgetState extends State with ToolbarMixin { - // final GlobalKey _listToolbarKey = GlobalKey(); - - final toolbarHeight = 32.0; - final topPadding = 5.0; - - final listToolbarWidth = 60.0; - final listToolbarHeight = 120.0; - - final cornerRadius = 8.0; - - OverlayEntry? _listToolbarOverlay; - - @override - Widget build(BuildContext context) { - return Positioned( - top: widget.offset.dx, - left: widget.offset.dy, - child: CompositedTransformFollower( - link: widget.layerLink, - showWhenUnlinked: true, - offset: widget.offset, - child: _buildToolbar(context), - ), - ); - } - - @override - void hide() { - _listToolbarOverlay?.remove(); - _listToolbarOverlay = null; - } - - Widget _buildToolbar(BuildContext context) { - return Material( - borderRadius: BorderRadius.circular(cornerRadius), - color: const Color(0xFF333333), - child: SizedBox( - height: toolbarHeight, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // _listToolbar(context), - _centerToolbarIcon('h1', tooltipMessage: 'Heading 1'), - _centerToolbarIcon('h2', tooltipMessage: 'Heading 2'), - _centerToolbarIcon('h3', tooltipMessage: 'Heading 3'), - _centerToolbarIcon('divider', width: 2), - _centerToolbarIcon('bold', tooltipMessage: 'Bold'), - _centerToolbarIcon('italic', tooltipMessage: 'Italic'), - _centerToolbarIcon('strikethrough', - tooltipMessage: 'Strikethrough'), - _centerToolbarIcon('underline', tooltipMessage: 'Underline'), - _centerToolbarIcon('divider', width: 2), - _centerToolbarIcon('quote', tooltipMessage: 'Quote'), - // _centerToolbarIcon('number_list'), - _centerToolbarIcon('bulleted_list', - tooltipMessage: 'Bulleted List'), - _centerToolbarIcon('divider', width: 2), - _centerToolbarIcon('highlight', tooltipMessage: 'Highlight'), - ], - ), - ), - ); - } - - // Widget _listToolbar(BuildContext context) { - // return _centerToolbarIcon( - // 'quote', - // key: _listToolbarKey, - // width: listToolbarWidth, - // onTap: () => _onTapListToolbar(context), - // ); - // } - - Widget _centerToolbarIcon(String name, - {Key? key, String? tooltipMessage, double? width, VoidCallback? onTap}) { - return Tooltip( - key: key, - preferBelow: false, - message: tooltipMessage ?? '', - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: onTap ?? () => _onTap(name), - child: SizedBox.fromSize( - size: - Size(toolbarHeight - (width != null ? 20 : 0), toolbarHeight), - child: Center( - child: FlowySvg( - width: width ?? 20, - name: 'toolbar/$name', - ), - ), - ), - ), - )); - } - - // void _onTapListToolbar(BuildContext context) { - // // TODO: implement more detailed UI. - // final items = defaultListToolbarEventNames; - // final renderBox = - // _listToolbarKey.currentContext?.findRenderObject() as RenderBox; - // final offset = renderBox - // .localToGlobal(Offset.zero) - // .translate(0, toolbarHeight - cornerRadius); - // final rect = offset & Size(listToolbarWidth, listToolbarHeight); - - // _listToolbarOverlay?.remove(); - // _listToolbarOverlay = OverlayEntry(builder: (context) { - // return Positioned.fromRect( - // rect: rect, - // child: Material( - // borderRadius: BorderRadius.only( - // bottomLeft: Radius.circular(cornerRadius), - // bottomRight: Radius.circular(cornerRadius), - // ), - // color: const Color(0xFF333333), - // child: SingleChildScrollView( - // child: ListView.builder( - // itemExtent: toolbarHeight, - // padding: const EdgeInsets.only(bottom: 10.0), - // shrinkWrap: true, - // itemCount: items.length, - // itemBuilder: ((context, index) { - // return ListTile( - // contentPadding: const EdgeInsets.only( - // left: 3.0, - // right: 3.0, - // ), - // minVerticalPadding: 0.0, - // title: FittedBox( - // fit: BoxFit.scaleDown, - // child: Text( - // items[index], - // textAlign: TextAlign.center, - // style: const TextStyle( - // color: Colors.white, - // ), - // ), - // ), - // onTap: () { - // _onTap(items[index]); - // }, - // ); - // }), - // ), - // ), - // ), - // ); - // }); - // // TODO: disable scrolling. - // Overlay.of(context)?.insert(_listToolbarOverlay!); - // } - - void _onTap(String eventName) { - if (defaultToolbarEventHandlers.containsKey(eventName)) { - defaultToolbarEventHandlers[eventName]!(widget.editorState); - return; - } - assert(false, 'Could not find the event handler for $eventName'); - } -} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart new file mode 100644 index 0000000000..49413311d2 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart @@ -0,0 +1,192 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/infra/flowy_svg.dart'; +import 'package:appflowy_editor/src/render/link_menu/link_menu.dart'; +import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; +import 'package:appflowy_editor/src/extensions/text_node_extensions.dart'; +import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart'; +import 'package:flutter/material.dart'; +import 'package:rich_clipboard/rich_clipboard.dart'; + +typedef ToolbarEventHandler = void Function( + EditorState editorState, BuildContext context); +typedef ToolbarShowValidator = bool Function(EditorState editorState); + +class ToolbarItem { + ToolbarItem({ + required this.icon, + this.tooltipsMessage = '', + required this.validator, + required this.handler, + }); + + final Widget icon; + final String tooltipsMessage; + final ToolbarShowValidator validator; + final ToolbarEventHandler handler; + + factory ToolbarItem.divider() { + return ToolbarItem( + icon: const FlowySvg(name: 'toolbar/divider'), + validator: (editorState) => true, + handler: (editorState, context) {}, + ); + } +} + +List defaultToolbarItems = [ + ToolbarItem( + tooltipsMessage: 'Heading 1', + icon: const FlowySvg(name: 'toolbar/h1'), + validator: _onlyShowInSingleTextSelection, + handler: (editorState, context) => formatHeading(editorState, StyleKey.h1), + ), + ToolbarItem( + tooltipsMessage: 'Heading 2', + icon: const FlowySvg(name: 'toolbar/h2'), + validator: _onlyShowInSingleTextSelection, + handler: (editorState, context) => formatHeading(editorState, StyleKey.h2), + ), + ToolbarItem( + tooltipsMessage: 'Heading 3', + icon: const FlowySvg(name: 'toolbar/h3'), + validator: _onlyShowInSingleTextSelection, + handler: (editorState, context) => formatHeading(editorState, StyleKey.h3), + ), + ToolbarItem.divider(), + ToolbarItem( + tooltipsMessage: 'Bold', + icon: const FlowySvg(name: 'toolbar/bold'), + validator: _showInTextSelection, + handler: (editorState, context) => formatBold(editorState), + ), + ToolbarItem( + tooltipsMessage: 'Italic', + icon: const FlowySvg(name: 'toolbar/italic'), + validator: _showInTextSelection, + handler: (editorState, context) => formatItalic(editorState), + ), + ToolbarItem( + tooltipsMessage: 'Underline', + icon: const FlowySvg(name: 'toolbar/underline'), + validator: _showInTextSelection, + handler: (editorState, context) => formatUnderline(editorState), + ), + ToolbarItem( + tooltipsMessage: 'Strikethrough', + icon: const FlowySvg(name: 'toolbar/strikethrough'), + validator: _showInTextSelection, + handler: (editorState, context) => formatStrikethrough(editorState), + ), + ToolbarItem.divider(), + ToolbarItem( + tooltipsMessage: 'Quote', + icon: const FlowySvg(name: 'toolbar/quote'), + validator: _onlyShowInSingleTextSelection, + handler: (editorState, context) => formatQuote(editorState), + ), + ToolbarItem( + tooltipsMessage: 'Bulleted list', + icon: const FlowySvg(name: 'toolbar/bulleted_list'), + validator: _onlyShowInSingleTextSelection, + handler: (editorState, context) => formatBulletedList(editorState), + ), + ToolbarItem.divider(), + ToolbarItem( + tooltipsMessage: 'Link', + icon: const FlowySvg(name: 'toolbar/link'), + validator: _onlyShowInSingleTextSelection, + handler: (editorState, context) => _showLinkMenu(editorState, context), + ), + ToolbarItem( + tooltipsMessage: 'Highlight', + icon: const FlowySvg(name: 'toolbar/highlight'), + validator: _showInTextSelection, + handler: (editorState, context) => formatHighlight(editorState), + ), +]; + +ToolbarShowValidator _onlyShowInSingleTextSelection = (editorState) { + final nodes = editorState.service.selectionService.currentSelectedNodes; + return (nodes.length == 1 && nodes.first is TextNode); +}; + +ToolbarShowValidator _showInTextSelection = (editorState) { + final nodes = editorState.service.selectionService.currentSelectedNodes + .whereType(); + return nodes.isNotEmpty; +}; + +OverlayEntry? _linkMenuOverlay; +EditorState? _editorState; +void _showLinkMenu(EditorState editorState, BuildContext context) { + _editorState = editorState; + + final rects = editorState.service.selectionService.selectionRects; + var maxBottom = 0.0; + late Rect matchRect; + for (final rect in rects) { + if (rect.bottom > maxBottom) { + maxBottom = rect.bottom; + matchRect = rect; + } + } + + _dismissLinkMenu(); + + // Since the link menu will only show in single text selection, + // We get the text node directly instead of judging details again. + final selection = + editorState.service.selectionService.currentSelection.value!; + final index = + selection.isBackward ? selection.start.offset : selection.end.offset; + final length = (selection.start.offset - selection.end.offset).abs(); + final node = editorState.service.selectionService.currentSelectedNodes.first + as TextNode; + final linkText = node.getAttributeInSelection(selection, StyleKey.href); + _linkMenuOverlay = OverlayEntry(builder: (context) { + return Positioned( + top: matchRect.bottom, + left: matchRect.left, + child: Material( + child: LinkMenu( + linkText: linkText, + onSubmitted: (text) { + TransactionBuilder(editorState) + ..formatText(node, index, length, { + StyleKey.href: text, + }) + ..commit(); + _dismissLinkMenu(); + }, + onCopyLink: () { + RichClipboard.setData(RichClipboardData(text: linkText)); + _dismissLinkMenu(); + }, + onRemoveLink: () { + TransactionBuilder(editorState) + ..formatText(node, index, length, { + StyleKey.href: null, + }) + ..commit(); + _dismissLinkMenu(); + }, + ), + ), + ); + }); + Overlay.of(context)?.insert(_linkMenuOverlay!); + + editorState.service.scrollService?.disable(); + editorState.service.selectionService.currentSelection + .addListener(_dismissLinkMenu); +} + +void _dismissLinkMenu() { + _linkMenuOverlay?.remove(); + _linkMenuOverlay = null; + + _editorState?.service.scrollService?.enable(); + _editorState?.service.selectionService.currentSelection + .removeListener(_dismissLinkMenu); + _editorState = null; +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item_widget.dart new file mode 100644 index 0000000000..ce89eef126 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item_widget.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; + +import 'toolbar_item.dart'; + +class ToolbarItemWidget extends StatelessWidget { + const ToolbarItemWidget({ + Key? key, + required this.item, + required this.onPressed, + }) : super(key: key); + + final ToolbarItem item; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 28, + height: 28, + child: Tooltip( + preferBelow: false, + message: item.tooltipsMessage, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: IconButton( + padding: EdgeInsets.zero, + icon: item.icon, + iconSize: 28, + onPressed: onPressed, + ), + ), + ), + ); + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart new file mode 100644 index 0000000000..167f8e79ba --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart @@ -0,0 +1,82 @@ +import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart'; +import 'package:appflowy_editor/src/render/toolbar/toolbar_item_widget.dart'; +import 'package:flutter/material.dart'; + +import 'package:appflowy_editor/src/editor_state.dart'; + +mixin ToolbarMixin on State { + void hide(); +} + +class ToolbarWidget extends StatefulWidget { + const ToolbarWidget({ + Key? key, + required this.editorState, + required this.layerLink, + required this.offset, + required this.items, + }) : super(key: key); + + final EditorState editorState; + final LayerLink layerLink; + final Offset offset; + final List items; + + @override + State createState() => _ToolbarWidgetState(); +} + +class _ToolbarWidgetState extends State with ToolbarMixin { + OverlayEntry? _listToolbarOverlay; + + @override + Widget build(BuildContext context) { + return Positioned( + top: widget.offset.dx, + left: widget.offset.dy, + child: CompositedTransformFollower( + link: widget.layerLink, + showWhenUnlinked: true, + offset: widget.offset, + child: _buildToolbar(context), + ), + ); + } + + @override + void hide() { + _listToolbarOverlay?.remove(); + _listToolbarOverlay = null; + } + + Widget _buildToolbar(BuildContext context) { + final items = widget.items.where( + (item) => item.validator(widget.editorState), + ); + return Material( + borderRadius: BorderRadius.circular(8.0), + color: const Color(0xFF333333), + child: Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: SizedBox( + height: 32.0, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: items + .map( + (item) => Center( + child: ToolbarItemWidget( + item: item, + onPressed: () { + item.handler(widget.editorState, context); + }, + ), + ), + ) + .toList(growable: false), + ), + ), + ), + ); + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart index 9aae2b5fcb..a92fae1b95 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart @@ -87,15 +87,18 @@ class _AppFlowyInputState extends State @override void attach(TextEditingValue textEditingValue) { - _textInputConnection ??= TextInput.attach( - this, - const TextInputConfiguration( - // TODO: customize - enableDeltaModel: true, - inputType: TextInputType.multiline, - textCapitalization: TextCapitalization.sentences, - ), - ); + if (_textInputConnection == null || + _textInputConnection!.attached == false) { + _textInputConnection = TextInput.attach( + this, + const TextInputConfiguration( + // TODO: customize + enableDeltaModel: true, + inputType: TextInputType.multiline, + textCapitalization: TextCapitalization.sentences, + ), + ); + } _textInputConnection! ..setEditingState(textEditingValue) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart index bf380290f9..fe4a2beace 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart @@ -1,7 +1,8 @@ +import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart'; import 'package:flutter/material.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor/src/render/selection/toolbar_widget.dart'; +import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart'; import 'package:appflowy_editor/src/extensions/object_extensions.dart'; abstract class FlowyToolbarService { @@ -41,7 +42,7 @@ class _FlowyToolbarState extends State editorState: widget.editorState, layerLink: layerLink, offset: offset.translate(0, -37.0), - handlers: const {}, + items: defaultToolbarItems, ), ); Overlay.of(context)?.insert(_toolbarOverlay!); From 074c497d576c2184ad23e31dc52746623d2523f3 Mon Sep 17 00:00:00 2001 From: appflowy Date: Mon, 22 Aug 2022 16:16:15 +0800 Subject: [PATCH 16/86] fix: fix some bugs --- frontend/.vscode/launch.json | 2 +- .../plugins/board/application/board_bloc.dart | 32 +++-- .../board/application/group_controller.dart | 17 +++ .../app_flowy/lib/plugins/board/board.dart | 2 +- .../grid/application/row/row_service.dart | 30 +++++ .../example/lib/multi_board_list_example.dart | 2 +- .../board_column/board_column_data.dart | 3 +- .../lib/src/widgets/board_data.dart | 15 ++- .../flowy-grid/src/entities/grid_entities.rs | 43 ++++++ .../rust-lib/flowy-grid/src/event_handler.rs | 11 ++ frontend/rust-lib/flowy-grid/src/event_map.rs | 4 + .../flowy-grid/src/services/grid_editor.rs | 39 ++++-- .../src/services/grid_view_editor.rs | 19 ++- .../src/services/grid_view_manager.rs | 11 +- .../flowy-grid/src/services/group/action.rs | 12 +- .../src/services/group/configuration.rs | 25 ++-- .../src/services/group/controller.rs | 31 ++--- .../controller_impls/checkbox_controller.rs | 13 +- .../multi_select_controller.rs | 25 +--- .../single_select_controller.rs | 25 +--- .../select_option_controller/util.rs | 78 ++++++----- .../src/services/group/group_service.rs | 123 ++++++++--------- .../tests/grid/group_test/script.rs | 14 +- .../flowy-grid/tests/grid/group_test/test.rs | 125 +++++++++++++++--- .../src/revision/grid_setting_rev.rs | 5 +- .../src/revision/group_rev.rs | 24 ++++ .../src/client_grid/view_revision_pad.rs | 8 +- 27 files changed, 486 insertions(+), 252 deletions(-) diff --git a/frontend/.vscode/launch.json b/frontend/.vscode/launch.json index e70b9ffb97..0efc79b00e 100644 --- a/frontend/.vscode/launch.json +++ b/frontend/.vscode/launch.json @@ -29,7 +29,7 @@ "program": "./lib/main.dart", "type": "dart", "env": { - "RUST_LOG": "trace" + "RUST_LOG": "debug" }, "cwd": "${workspaceRoot}/app_flowy" }, diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index ac72b6e24e..f84c2a2bd1 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -23,7 +23,7 @@ class BoardBloc extends Bloc { final BoardDataController _dataController; late final AFBoardDataController afBoardDataController; final MoveRowFFIService _rowService; - Map groupControllers = {}; + LinkedHashMap groupControllers = LinkedHashMap.new(); GridFieldCache get fieldCache => _dataController.fieldCache; String get gridId => _dataController.gridId; @@ -34,9 +34,13 @@ class BoardBloc extends Bloc { super(BoardState.initial(view.id)) { afBoardDataController = AFBoardDataController( onMoveColumn: ( + fromColumnId, fromIndex, + toColumnId, toIndex, - ) {}, + ) { + _moveGroup(fromColumnId, toColumnId); + }, onMoveColumnItem: ( columnId, fromIndex, @@ -44,7 +48,7 @@ class BoardBloc extends Bloc { ) { final fromRow = groupControllers[columnId]?.rowAtIndex(fromIndex); final toRow = groupControllers[columnId]?.rowAtIndex(toIndex); - _moveRow(fromRow, toRow); + _moveRow(fromRow, columnId, toRow); }, onMoveColumnItemToColumn: ( fromColumnId, @@ -54,7 +58,7 @@ class BoardBloc extends Bloc { ) { final fromRow = groupControllers[fromColumnId]?.rowAtIndex(fromIndex); final toRow = groupControllers[toColumnId]?.rowAtIndex(toIndex); - _moveRow(fromRow, toRow); + _moveRow(fromRow, toColumnId, toRow); }, ); @@ -95,12 +99,13 @@ class BoardBloc extends Bloc { ); } - void _moveRow(RowPB? fromRow, RowPB? toRow) { - if (fromRow != null && toRow != null) { + void _moveRow(RowPB? fromRow, String columnId, RowPB? toRow) { + if (fromRow != null) { _rowService - .moveRow( + .moveGroupRow( fromRowId: fromRow.id, - toRowId: toRow.id, + toGroupId: columnId, + toRowId: toRow?.id, ) .then((result) { result.fold((l) => null, (r) => add(BoardEvent.didReceiveError(r))); @@ -108,6 +113,17 @@ class BoardBloc extends Bloc { } } + void _moveGroup(String fromColumnId, String toColumnId) { + _rowService + .moveGroup( + fromGroupId: fromColumnId, + toGroupId: toColumnId, + ) + .then((result) { + result.fold((l) => null, (r) => add(BoardEvent.didReceiveError(r))); + }); + } + @override Future close() async { await _dataController.dispose(); diff --git a/frontend/app_flowy/lib/plugins/board/application/group_controller.dart b/frontend/app_flowy/lib/plugins/board/application/group_controller.dart index 3f545dae3b..6fd68b1df8 100644 --- a/frontend/app_flowy/lib/plugins/board/application/group_controller.dart +++ b/frontend/app_flowy/lib/plugins/board/application/group_controller.dart @@ -37,6 +37,14 @@ class GroupController { (GroupRowsChangesetPB changeset) { for (final insertedRow in changeset.insertedRows) { final index = insertedRow.hasIndex() ? insertedRow.index : null; + + if (insertedRow.hasIndex() && + group.rows.length > insertedRow.index) { + group.rows.insert(insertedRow.index, insertedRow.row); + } else { + group.rows.add(insertedRow.row); + } + delegate.insertRow( group.groupId, insertedRow.row, @@ -45,10 +53,19 @@ class GroupController { } for (final deletedRow in changeset.deletedRows) { + group.rows.removeWhere((rowPB) => rowPB.id == deletedRow); delegate.removeRow(group.groupId, deletedRow); } for (final updatedRow in changeset.updatedRows) { + final index = group.rows.indexWhere( + (rowPB) => rowPB.id == updatedRow.id, + ); + + if (index != -1) { + group.rows[index] = updatedRow; + } + delegate.updateRow(group.groupId, updatedRow); } }, diff --git a/frontend/app_flowy/lib/plugins/board/board.dart b/frontend/app_flowy/lib/plugins/board/board.dart index 213cc8bc3c..c55d7f2e17 100644 --- a/frontend/app_flowy/lib/plugins/board/board.dart +++ b/frontend/app_flowy/lib/plugins/board/board.dart @@ -31,7 +31,7 @@ class BoardPluginBuilder implements PluginBuilder { class BoardPluginConfig implements PluginConfig { @override - bool get creatable => true; + bool get creatable => false; } class BoardPlugin extends Plugin { diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart index a18c0c8e75..2612f5975c 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart @@ -3,6 +3,7 @@ import 'package:flowy_sdk/dispatch/dispatch.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/group_changeset.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/row_entities.pb.dart'; class RowFFIService { @@ -68,4 +69,33 @@ class MoveRowFFIService { return GridEventMoveRow(payload).send(); } + + Future> moveGroupRow({ + required String fromRowId, + required String toGroupId, + required String? toRowId, + }) { + var payload = MoveGroupRowPayloadPB.create() + ..viewId = gridId + ..fromRowId = fromRowId + ..toGroupId = toGroupId; + + if (toRowId != null) { + payload.toRowId = toRowId; + } + + return GridEventMoveGroupRow(payload).send(); + } + + Future> moveGroup({ + required String fromGroupId, + required String toGroupId, + }) { + final payload = MoveGroupPayloadPB.create() + ..viewId = gridId + ..fromGroupId = fromGroupId + ..toGroupId = toGroupId; + + return GridEventMoveGroup(payload).send(); + } } diff --git a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart index 83f75d2a0e..01aba725a5 100644 --- a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart +++ b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart @@ -10,7 +10,7 @@ class MultiBoardListExample extends StatefulWidget { class _MultiBoardListExampleState extends State { final AFBoardDataController boardDataController = AFBoardDataController( - onMoveColumn: (fromIndex, toIndex) { + onMoveColumn: (fromColumnId, fromIndex, toColumnId, toIndex) { debugPrint('Move column from $fromIndex to $toIndex'); }, onMoveColumnItem: (columnId, fromIndex, toIndex) { diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart index 6e184761c5..f26bd16c50 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart @@ -145,7 +145,8 @@ class AFBoardColumnData extends ReoderFlexItem with EquatableMixin { }) : _items = items; /// Returns the readonly List - UnmodifiableListView get items => UnmodifiableListView(_items); + UnmodifiableListView get items => + UnmodifiableListView([..._items]); @override List get props => [id, ..._items]; diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart index 6208dbd0f0..e8d5471939 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart @@ -8,7 +8,12 @@ import 'reorder_flex/reorder_flex.dart'; import 'package:flutter/material.dart'; import 'reorder_phantom/phantom_controller.dart'; -typedef OnMoveColumn = void Function(int fromIndex, int toIndex); +typedef OnMoveColumn = void Function( + String fromColumnId, + int fromIndex, + String toColumnId, + int toIndex, +); typedef OnMoveColumnItem = void Function( String columnId, @@ -98,9 +103,11 @@ class AFBoardDataController extends ChangeNotifier } void moveColumn(int fromIndex, int toIndex, {bool notify = true}) { - final columnData = _columnDatas.removeAt(fromIndex); - _columnDatas.insert(toIndex, columnData); - onMoveColumn?.call(fromIndex, toIndex); + final toColumnData = _columnDatas[toIndex]; + final fromColumnData = _columnDatas.removeAt(fromIndex); + + _columnDatas.insert(toIndex, fromColumnData); + onMoveColumn?.call(fromColumnData.id, fromIndex, toColumnData.id, toIndex); if (notify) notifyListeners(); } diff --git a/frontend/rust-lib/flowy-grid/src/entities/grid_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/grid_entities.rs index be8cfdeae1..c012376f55 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/grid_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/grid_entities.rs @@ -123,3 +123,46 @@ impl TryInto for MoveRowPayloadPB { }) } } +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct MoveGroupRowPayloadPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub from_row_id: String, + + #[pb(index = 3)] + pub to_group_id: String, + + #[pb(index = 4, one_of)] + pub to_row_id: Option, +} + +pub struct MoveGroupRowParams { + pub view_id: String, + pub from_row_id: String, + pub to_group_id: String, + pub to_row_id: Option, +} + +impl TryInto for MoveGroupRowPayloadPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::GridViewIdIsEmpty)?; + let from_row_id = NotEmptyStr::parse(self.from_row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?; + let to_group_id = NotEmptyStr::parse(self.to_group_id).map_err(|_| ErrorCode::GroupIdIsEmpty)?; + + let to_row_id = match self.to_row_id { + None => None, + Some(to_row_id) => Some(NotEmptyStr::parse(to_row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?.0), + }; + + Ok(MoveGroupRowParams { + view_id: view_id.0, + from_row_id: from_row_id.0, + to_group_id: to_group_id.0, + to_row_id, + }) + } +} diff --git a/frontend/rust-lib/flowy-grid/src/event_handler.rs b/frontend/rust-lib/flowy-grid/src/event_handler.rs index 149f7017a3..a9060aa87e 100644 --- a/frontend/rust-lib/flowy-grid/src/event_handler.rs +++ b/frontend/rust-lib/flowy-grid/src/event_handler.rs @@ -436,3 +436,14 @@ pub(crate) async fn move_group_handler( let _ = editor.move_group(params).await?; Ok(()) } + +#[tracing::instrument(level = "debug", skip(data, manager), err)] +pub(crate) async fn move_group_row_handler( + data: Data, + manager: AppData>, +) -> FlowyResult<()> { + let params: MoveGroupRowParams = data.into_inner().try_into()?; + let editor = manager.get_grid_editor(params.view_id.as_ref())?; + let _ = editor.move_group_row(params).await?; + Ok(()) +} diff --git a/frontend/rust-lib/flowy-grid/src/event_map.rs b/frontend/rust-lib/flowy-grid/src/event_map.rs index 844ae7f5c3..a78bcb5ed3 100644 --- a/frontend/rust-lib/flowy-grid/src/event_map.rs +++ b/frontend/rust-lib/flowy-grid/src/event_map.rs @@ -42,6 +42,7 @@ pub fn create(grid_manager: Arc) -> Module { // Group .event(GridEvent::CreateBoardCard, create_board_card_handler) .event(GridEvent::MoveGroup, move_group_handler) + .event(GridEvent::MoveGroupRow, move_group_row_handler) .event(GridEvent::GetGroup, get_groups_handler); module @@ -221,4 +222,7 @@ pub enum GridEvent { #[event(input = "MoveGroupPayloadPB")] MoveGroup = 111, + + #[event(input = "MoveGroupRowPayloadPB")] + MoveGroupRow = 112, } diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index 93c48b0880..485bf6930c 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -560,16 +560,6 @@ impl GridRevisionEditor { .block_manager .move_row(row_rev.clone(), from_index, to_index) .await?; - - if let Some(row_changeset) = self.view_manager.move_row(row_rev, to_row_id.clone()).await { - tracing::trace!("Receive row changeset after moving the row"); - match self.block_manager.update_row(row_changeset).await { - Ok(_) => {} - Err(e) => { - tracing::error!("Apply row changeset error:{:?}", e); - } - } - } } (_, None) => tracing::warn!("Can not find the from row id: {}", from_row_id), (None, _) => tracing::warn!("Can not find the to row id: {}", to_row_id), @@ -579,6 +569,35 @@ impl GridRevisionEditor { Ok(()) } + pub async fn move_group_row(&self, params: MoveGroupRowParams) -> FlowyResult<()> { + let MoveGroupRowParams { + view_id: _, + from_row_id, + to_group_id, + to_row_id, + } = params; + + match self.block_manager.get_row_rev(&from_row_id).await? { + None => tracing::warn!("Move row failed, can not find the row:{}", from_row_id), + Some(row_rev) => { + if let Some(row_changeset) = self + .view_manager + .move_group_row(row_rev, to_group_id, to_row_id.clone()) + .await + { + match self.block_manager.update_row(row_changeset).await { + Ok(_) => {} + Err(e) => { + tracing::error!("Apply row changeset error:{:?}", e); + } + } + } + } + } + + Ok(()) + } + pub async fn move_field(&self, params: MoveFieldParams) -> FlowyResult<()> { let MoveFieldParams { grid_id: _, diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs index fb8706b175..4e23d9be19 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs @@ -80,7 +80,7 @@ impl GridViewRevisionEditor { None => {} Some(group_id) => { self.group_service - .read() + .write() .await .will_create_row(row_rev, group_id, |field_id| { self.field_delegate.get_field_rev(&field_id) @@ -109,7 +109,7 @@ impl GridViewRevisionEditor { // Send the group notification if the current view has groups; if let Some(changesets) = self .group_service - .read() + .write() .await .did_delete_row(row_rev, |field_id| self.field_delegate.get_field_rev(&field_id)) .await @@ -123,7 +123,7 @@ impl GridViewRevisionEditor { pub(crate) async fn did_update_row(&self, row_rev: &RowRevision) { if let Some(changesets) = self .group_service - .read() + .write() .await .did_update_row(row_rev, |field_id| self.field_delegate.get_field_rev(&field_id)) .await @@ -134,23 +134,23 @@ impl GridViewRevisionEditor { } } - pub(crate) async fn did_move_row( + pub(crate) async fn move_group_row( &self, row_rev: &RowRevision, row_changeset: &mut RowChangeset, - upper_row_id: &str, + to_group_id: &str, + to_row_id: Option, ) { if let Some(changesets) = self .group_service - .read() + .write() .await - .did_move_row(row_rev, row_changeset, upper_row_id, |field_id| { + .move_group_row(row_rev, row_changeset, to_group_id, to_row_id, |field_id| { self.field_delegate.get_field_rev(&field_id) }) .await { for changeset in changesets { - tracing::trace!("Group: {} changeset: {}", changeset.group_id, changeset); self.notify_did_update_group_rows(changeset).await; } } @@ -184,7 +184,7 @@ impl GridViewRevisionEditor { pub(crate) async fn move_group(&self, params: MoveGroupParams) -> FlowyResult<()> { let _ = self .group_service - .read() + .write() .await .move_group(¶ms.from_group_id, ¶ms.to_group_id) .await?; @@ -326,7 +326,6 @@ impl GroupConfigurationReader for GroupConfigurationReaderImpl { let view_pad = self.0.clone(); wrap_future(async move { let mut groups = view_pad.read().await.groups.get_objects(&field_rev.id, &field_rev.ty)?; - if groups.is_empty() { None } else { 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 a1265d0e25..b0e578f804 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 @@ -129,10 +129,17 @@ impl GridViewManager { /// It may generate a RowChangeset when the Row was moved from one group to another. /// The return value, [RowChangeset], contains the changes made by the groups. /// - pub(crate) async fn move_row(&self, row_rev: Arc, to_row_id: String) -> Option { + pub(crate) async fn move_group_row( + &self, + row_rev: Arc, + to_group_id: String, + to_row_id: Option, + ) -> Option { let mut row_changeset = RowChangeset::new(row_rev.id.clone()); for view_editor in self.view_editors.iter() { - view_editor.did_move_row(&row_rev, &mut row_changeset, &to_row_id).await; + view_editor + .move_group_row(&row_rev, &mut row_changeset, &to_group_id, to_row_id.clone()) + .await; } if row_changeset.has_changed() { diff --git a/frontend/rust-lib/flowy-grid/src/services/group/action.rs b/frontend/rust-lib/flowy-grid/src/services/group/action.rs index a495fe0a1f..29dc51cc37 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/action.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/action.rs @@ -1,6 +1,7 @@ use crate::entities::GroupRowsChangesetPB; -use flowy_grid_data_model::revision::{FieldRevision, RowChangeset, RowRevision}; +use crate::services::group::controller::MoveGroupRowContext; +use flowy_grid_data_model::revision::RowRevision; pub trait GroupAction: Send + Sync { type CellDataType; @@ -12,12 +13,5 @@ pub trait GroupAction: Send + Sync { cell_data: &Self::CellDataType, ) -> Vec; - fn move_row_if_match( - &mut self, - field_rev: &FieldRevision, - row_rev: &RowRevision, - row_changeset: &mut RowChangeset, - cell_data: &Self::CellDataType, - to_row_id: &str, - ) -> Vec; + fn move_row(&mut self, cell_data: &Self::CellDataType, context: MoveGroupRowContext) -> Vec; } 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 36d49a10e1..a462278d2b 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs @@ -101,9 +101,9 @@ where Ok(()) } - pub(crate) fn with_mut_groups(&mut self, mut mut_groups_fn: impl FnMut(&mut Group)) { + pub(crate) fn with_mut_groups(&mut self, mut each: impl FnMut(&mut Group)) { self.groups_map.iter_mut().for_each(|(_, group)| { - mut_groups_fn(group); + each(group); }) } @@ -111,22 +111,16 @@ where self.groups_map.get_mut(group_id) } - pub(crate) fn move_group(&mut self, from_group_id: &str, to_group_id: &str) -> FlowyResult<()> { - let from_group_index = self.groups_map.get_index_of(from_group_id); - let to_group_index = self.groups_map.get_index_of(to_group_id); - match (from_group_index, to_group_index) { + 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); + match (from_index, to_index) { (Some(from_index), Some(to_index)) => { self.groups_map.swap_indices(from_index, to_index); self.mut_configuration(|configuration| { - let from_index = configuration - .groups - .iter() - .position(|group| group.group_id == from_group_id); - let to_index = configuration - .groups - .iter() - .position(|group| group.group_id == to_group_id); + let from_index = configuration.groups.iter().position(|group| group.group_id == from_id); + let to_index = configuration.groups.iter().position(|group| group.group_id == to_id); if let (Some(from), Some(to)) = (from_index, to_index) { configuration.groups.swap(from, to); } @@ -150,7 +144,7 @@ where let configuration = (&*self.configuration).clone(); let writer = self.writer.clone(); let field_id = self.field_rev.id.clone(); - let field_type = self.field_rev.ty.clone(); + let field_type = self.field_rev.ty; tokio::spawn(async move { match writer .save_group_configuration(&field_id, field_type, configuration) @@ -196,7 +190,6 @@ where } fn merge_groups(old_group_revs: &[GroupRecordRevision], groups: Vec) -> (Vec, Vec) { - tracing::trace!("Merge group: old: {}, new: {}", old_group_revs.len(), groups.len()); if old_group_revs.is_empty() { let new_groups = groups .iter() 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 13b4ccce63..1a5ee23694 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller.rs @@ -31,6 +31,14 @@ pub trait GroupGenerator { ) -> Vec; } +pub struct MoveGroupRowContext<'a> { + pub row_rev: &'a RowRevision, + pub row_changeset: &'a mut RowChangeset, + pub field_rev: &'a FieldRevision, + pub to_group_id: &'a str, + pub to_row_id: Option, +} + // Defines the shared actions each group controller can perform. pub trait GroupControllerSharedOperation: Send + Sync { // The field that is used for grouping the rows @@ -51,13 +59,7 @@ pub trait GroupControllerSharedOperation: Send + Sync { field_rev: &FieldRevision, ) -> FlowyResult>; - fn did_move_row( - &mut self, - row_rev: &RowRevision, - row_changeset: &mut RowChangeset, - field_rev: &FieldRevision, - to_row_id: &str, - ) -> FlowyResult>; + fn move_group_row(&mut self, context: MoveGroupRowContext) -> FlowyResult>; } /// C: represents the group configuration that impl [GroupConfigurationSerde] @@ -195,18 +197,11 @@ where } } - fn did_move_row( - &mut self, - row_rev: &RowRevision, - row_changeset: &mut RowChangeset, - field_rev: &FieldRevision, - to_row_id: &str, - ) -> FlowyResult> { - 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); + fn move_group_row(&mut self, context: MoveGroupRowContext) -> FlowyResult> { + if let Some(cell_rev) = context.row_rev.cells.get(&self.field_id) { + let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), context.field_rev); let cell_data = cell_bytes.parser::

()?; - tracing::trace!("Move row:{} to row:{}", row_rev.id, to_row_id); - Ok(self.move_row_if_match(field_rev, row_rev, row_changeset, &cell_data, to_row_id)) + Ok(self.move_row(&cell_data, context)) } else { Ok(vec![]) } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs index 59a53ae72a..ffcbf117fe 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs @@ -2,10 +2,12 @@ use crate::entities::GroupRowsChangesetPB; use crate::services::field::{CheckboxCellData, CheckboxCellDataParser, CheckboxTypeOptionPB, CHECK, UNCHECK}; use crate::services::group::action::GroupAction; use crate::services::group::configuration::GenericGroupConfiguration; -use crate::services::group::controller::{GenericGroupController, GroupController, GroupGenerator}; +use crate::services::group::controller::{ + GenericGroupController, GroupController, GroupGenerator, MoveGroupRowContext, +}; use crate::services::group::entities::Group; -use flowy_grid_data_model::revision::{CheckboxGroupConfigurationRevision, FieldRevision, RowChangeset, RowRevision}; +use flowy_grid_data_model::revision::{CheckboxGroupConfigurationRevision, FieldRevision, RowRevision}; pub type CheckboxGroupController = GenericGroupController< CheckboxGroupConfigurationRevision, @@ -38,13 +40,10 @@ impl GroupAction for CheckboxGroupController { todo!() } - fn move_row_if_match( + fn move_row( &mut self, - _field_rev: &FieldRevision, - _row_rev: &RowRevision, - _row_changeset: &mut RowChangeset, _cell_data: &Self::CellDataType, - _to_row_id: &str, + _context: MoveGroupRowContext, ) -> Vec { todo!() } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs index 7a208a884f..cce2698158 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs @@ -3,12 +3,12 @@ use crate::services::cell::insert_select_option_cell; use crate::services::field::{MultiSelectTypeOptionPB, SelectOptionCellDataPB, SelectOptionCellDataParser}; use crate::services::group::action::GroupAction; -use crate::services::group::controller::{GenericGroupController, GroupController, GroupGenerator}; +use crate::services::group::controller::{ + GenericGroupController, GroupController, GroupGenerator, MoveGroupRowContext, +}; use crate::services::group::controller_impls::select_option_controller::util::*; use crate::services::group::entities::Group; -use flowy_grid_data_model::revision::{ - FieldRevision, RowChangeset, RowRevision, SelectOptionGroupConfigurationRevision, -}; +use flowy_grid_data_model::revision::{FieldRevision, RowRevision, SelectOptionGroupConfigurationRevision}; // MultiSelect pub type MultiSelectGroupController = GenericGroupController< @@ -45,25 +45,14 @@ impl GroupAction for MultiSelectGroupController { changesets } - fn move_row_if_match( + fn move_row( &mut self, - field_rev: &FieldRevision, - row_rev: &RowRevision, - row_changeset: &mut RowChangeset, cell_data: &Self::CellDataType, - to_row_id: &str, + mut context: MoveGroupRowContext, ) -> Vec { let mut group_changeset = vec![]; self.configuration.with_mut_groups(|group| { - move_row( - group, - &mut group_changeset, - field_rev, - row_rev, - row_changeset, - cell_data, - to_row_id, - ); + move_select_option_row(group, &mut group_changeset, cell_data, &mut context); }); group_changeset } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs index 2eb1e4a60f..d48cdd8ee7 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs @@ -3,13 +3,13 @@ use crate::services::cell::insert_select_option_cell; use crate::services::field::{SelectOptionCellDataPB, SelectOptionCellDataParser, SingleSelectTypeOptionPB}; use crate::services::group::action::GroupAction; -use crate::services::group::controller::{GenericGroupController, GroupController, GroupGenerator}; +use crate::services::group::controller::{ + GenericGroupController, GroupController, GroupGenerator, MoveGroupRowContext, +}; use crate::services::group::controller_impls::select_option_controller::util::*; use crate::services::group::entities::Group; -use flowy_grid_data_model::revision::{ - FieldRevision, RowChangeset, RowRevision, SelectOptionGroupConfigurationRevision, -}; +use flowy_grid_data_model::revision::{FieldRevision, RowRevision, SelectOptionGroupConfigurationRevision}; // SingleSelect pub type SingleSelectGroupController = GenericGroupController< @@ -45,25 +45,14 @@ impl GroupAction for SingleSelectGroupController { changesets } - fn move_row_if_match( + fn move_row( &mut self, - field_rev: &FieldRevision, - row_rev: &RowRevision, - row_changeset: &mut RowChangeset, cell_data: &Self::CellDataType, - to_row_id: &str, + mut context: MoveGroupRowContext, ) -> Vec { let mut group_changeset = vec![]; self.configuration.with_mut_groups(|group| { - move_row( - group, - &mut group_changeset, - field_rev, - row_rev, - row_changeset, - cell_data, - to_row_id, - ); + move_select_option_row(group, &mut group_changeset, cell_data, &mut context); }); group_changeset } 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 6675f26aa4..bdca688531 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 @@ -4,9 +4,8 @@ use crate::services::field::SelectOptionCellDataPB; use crate::services::group::configuration::GenericGroupConfiguration; use crate::services::group::Group; -use flowy_grid_data_model::revision::{ - FieldRevision, RowChangeset, RowRevision, SelectOptionGroupConfigurationRevision, -}; +use crate::services::group::controller::MoveGroupRowContext; +use flowy_grid_data_model::revision::{RowRevision, SelectOptionGroupConfigurationRevision}; pub type SelectOptionGroupConfiguration = GenericGroupConfiguration; @@ -47,45 +46,60 @@ pub fn remove_row( }); } -pub fn move_row( +pub fn move_select_option_row( group: &mut Group, group_changeset: &mut Vec, - field_rev: &FieldRevision, - row_rev: &RowRevision, - row_changeset: &mut RowChangeset, - cell_data: &SelectOptionCellDataPB, - to_row_id: &str, + _cell_data: &SelectOptionCellDataPB, + context: &mut MoveGroupRowContext, ) { - cell_data.select_options.iter().for_each(|option| { - // Remove the row in which group contains the row - let is_group_contains = group.contains_row(&row_rev.id); - let to_index = group.index_of_row(to_row_id); + let MoveGroupRowContext { + row_rev, + row_changeset, + field_rev, + to_group_id, + to_row_id, + } = context; - if option.id == group.id && is_group_contains { - group_changeset.push(GroupRowsChangesetPB::delete(group.id.clone(), vec![row_rev.id.clone()])); - group.remove_row(&row_rev.id); - } + let from_index = group.index_of_row(&row_rev.id); + let to_index = match to_row_id { + None => None, + Some(to_row_id) => group.index_of_row(to_row_id), + }; - // Find the inserted group - if let Some(to_index) = to_index { - let row_pb = RowPB::from(row_rev); - let inserted_row = InsertedRowPB { - row: row_pb.clone(), - index: Some(to_index as i32), - }; - group_changeset.push(GroupRowsChangesetPB::insert(group.id.clone(), vec![inserted_row])); - if group.number_of_row() == to_index { + // Remove the row in which group contains it + if from_index.is_some() { + group_changeset.push(GroupRowsChangesetPB::delete(group.id.clone(), vec![row_rev.id.clone()])); + tracing::debug!("Group:{} remove row:{}", group.id, row_rev.id); + group.remove_row(&row_rev.id); + } + + if group.id == *to_group_id { + let row_pb = RowPB::from(*row_rev); + let mut inserted_row = InsertedRowPB::new(row_pb.clone()); + match to_index { + None => { + group_changeset.push(GroupRowsChangesetPB::insert(group.id.clone(), vec![inserted_row])); + tracing::debug!("Group:{} append row:{}", group.id, row_rev.id); group.add_row(row_pb); - } else { - group.insert_row(to_index, row_pb); + } + Some(to_index) => { + if to_index < group.number_of_row() { + tracing::debug!("Group:{} insert row:{} at {} ", group.id, row_rev.id, to_index); + inserted_row.index = Some(to_index as i32); + group.insert_row(to_index, row_pb); + } else { + tracing::debug!("Group:{} append row:{}", group.id, row_rev.id); + group.add_row(row_pb); + } + group_changeset.push(GroupRowsChangesetPB::insert(group.id.clone(), vec![inserted_row])); } } - // If the inserted row comes from other group, it needs to update the corresponding cell content. - if to_index.is_some() && option.id != group.id { - // Update the corresponding row's cell content. + // Update the corresponding row's cell content. + if from_index.is_none() { + tracing::debug!("Mark row:{} belong to group:{}", row_rev.id, group.id); let cell_rev = insert_select_option_cell(group.id.clone(), field_rev); row_changeset.cell_by_field_id.insert(field_rev.id.clone(), cell_rev); } - }); + } } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs index 63a3b4b67b..6afec9dd82 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs @@ -1,5 +1,6 @@ +use crate::entities::{FieldType, GroupRowsChangesetPB}; use crate::services::group::configuration::GroupConfigurationReader; -use crate::services::group::controller::GroupController; +use crate::services::group::controller::{GroupController, MoveGroupRowContext}; use crate::services::group::{ CheckboxGroupConfiguration, CheckboxGroupController, Group, GroupConfigurationWriter, MultiSelectGroupController, SelectOptionGroupConfiguration, SingleSelectGroupController, @@ -10,16 +11,13 @@ use flowy_grid_data_model::revision::{ NumberGroupConfigurationRevision, RowChangeset, RowRevision, SelectOptionGroupConfigurationRevision, TextGroupConfigurationRevision, UrlGroupConfigurationRevision, }; - -use crate::entities::{FieldType, GroupRowsChangesetPB}; use std::future::Future; use std::sync::Arc; -use tokio::sync::RwLock; pub(crate) struct GroupService { configuration_reader: Arc, configuration_writer: Arc, - group_controller: Option>>, + group_controller: Option>, } impl GroupService { @@ -36,19 +34,16 @@ impl GroupService { } pub(crate) async fn groups(&self) -> Vec { - if let Some(group_controller) = self.group_controller.as_ref() { - group_controller.read().await.groups() - } else { - vec![] - } + self.group_controller + .as_ref() + .and_then(|group_controller| Some(group_controller.groups())) + .unwrap_or(vec![]) } pub(crate) async fn get_group(&self, group_id: &str) -> Option<(usize, Group)> { - if let Some(group_controller) = self.group_controller.as_ref() { - group_controller.read().await.get_group(group_id) - } else { - None - } + self.group_controller + .as_ref() + .and_then(|group_controller| group_controller.get_group(group_id)) } pub(crate) async fn load_groups( @@ -58,51 +53,37 @@ impl GroupService { ) -> Option> { let field_rev = find_group_field(field_revs)?; let field_type: FieldType = field_rev.ty.into(); - match self.make_group_controller(&field_type, &field_rev).await { - Ok(group_controller) => { - self.group_controller = group_controller; - let mut groups = vec![]; - if let Some(group_action_handler) = self.group_controller.as_ref() { - let mut write_guard = group_action_handler.write().await; - groups = match write_guard.fill_groups(&row_revs, &field_rev) { - Ok(groups) => groups, - Err(e) => { - tracing::error!("Fill groups failed:{:?}", e); - vec![] - } - }; - drop(write_guard); - } - Some(groups) + + let mut group_controller = self.make_group_controller(&field_type, &field_rev).await.ok()??; + let groups = match group_controller.fill_groups(&row_revs, &field_rev) { + Ok(groups) => groups, + Err(e) => { + tracing::error!("Fill groups failed:{:?}", e); + vec![] } - Err(err) => { - tracing::error!("Load group failed: {}", err); - Some(vec![]) - } - } + }; + self.group_controller = Some(group_controller); + Some(groups) } - pub(crate) async fn will_create_row(&self, row_rev: &mut RowRevision, group_id: &str, get_field_fn: F) + pub(crate) async fn will_create_row(&mut self, row_rev: &mut RowRevision, group_id: &str, get_field_fn: F) where F: FnOnce(String) -> O, O: Future>> + Send + Sync + 'static, { - if let Some(group_controller) = self.group_controller.as_ref() { - let field_id = group_controller.read().await.field_id().to_owned(); + if let Some(group_controller) = self.group_controller.as_mut() { + let field_id = group_controller.field_id().to_owned(); match get_field_fn(field_id).await { None => {} Some(field_rev) => { - group_controller - .write() - .await - .will_create_row(row_rev, &field_rev, group_id); + group_controller.will_create_row(row_rev, &field_rev, group_id); } } } } pub(crate) async fn did_delete_row( - &self, + &mut self, row_rev: &RowRevision, get_field_fn: F, ) -> Option> @@ -110,11 +91,11 @@ impl GroupService { F: FnOnce(String) -> O, O: Future>> + Send + Sync + 'static, { - let group_controller = self.group_controller.as_ref()?; - let field_id = group_controller.read().await.field_id().to_owned(); + let group_controller = self.group_controller.as_mut()?; + let field_id = group_controller.field_id().to_owned(); let field_rev = get_field_fn(field_id).await?; - match group_controller.write().await.did_delete_row(row_rev, &field_rev) { + match group_controller.did_delete_row(row_rev, &field_rev) { Ok(changesets) => Some(changesets), Err(e) => { tracing::error!("Delete group data failed, {:?}", e); @@ -123,26 +104,30 @@ impl GroupService { } } - pub(crate) async fn did_move_row( - &self, + pub(crate) async fn move_group_row( + &mut self, row_rev: &RowRevision, row_changeset: &mut RowChangeset, - upper_row_id: &str, + to_group_id: &str, + to_row_id: Option, get_field_fn: F, ) -> Option> where F: FnOnce(String) -> O, O: Future>> + Send + Sync + 'static, { - let group_controller = self.group_controller.as_ref()?; - let field_id = group_controller.read().await.field_id().to_owned(); + let group_controller = self.group_controller.as_mut()?; + let field_id = group_controller.field_id().to_owned(); let field_rev = get_field_fn(field_id).await?; + let move_row_context = MoveGroupRowContext { + row_rev, + row_changeset, + field_rev: field_rev.as_ref(), + to_group_id, + to_row_id, + }; - match group_controller - .write() - .await - .did_move_row(row_rev, row_changeset, &field_rev, upper_row_id) - { + match group_controller.move_group_row(move_row_context) { Ok(changesets) => Some(changesets), Err(e) => { tracing::error!("Move group data failed, {:?}", e); @@ -153,7 +138,7 @@ impl GroupService { #[tracing::instrument(level = "trace", skip_all)] pub(crate) async fn did_update_row( - &self, + &mut self, row_rev: &RowRevision, get_field_fn: F, ) -> Option> @@ -161,11 +146,11 @@ impl GroupService { F: FnOnce(String) -> O, O: Future>> + Send + Sync + 'static, { - let group_controller = self.group_controller.as_ref()?; - let field_id = group_controller.read().await.field_id().to_owned(); + let group_controller = self.group_controller.as_mut()?; + let field_id = group_controller.field_id().to_owned(); let field_rev = get_field_fn(field_id).await?; - match group_controller.write().await.did_update_row(row_rev, &field_rev) { + match group_controller.did_update_row(row_rev, &field_rev) { Ok(changeset) => Some(changeset), Err(e) => { tracing::error!("Update group data failed, {:?}", e); @@ -175,11 +160,11 @@ impl GroupService { } #[tracing::instrument(level = "trace", skip_all)] - pub(crate) async fn move_group(&self, from_group_id: &str, to_group_id: &str) -> FlowyResult<()> { - match self.group_controller.as_ref() { + pub(crate) async fn move_group(&mut self, from_group_id: &str, to_group_id: &str) -> FlowyResult<()> { + match self.group_controller.as_mut() { None => Ok(()), Some(group_controller) => { - let _ = group_controller.write().await.move_group(from_group_id, to_group_id)?; + let _ = group_controller.move_group(from_group_id, to_group_id)?; Ok(()) } } @@ -190,8 +175,8 @@ impl GroupService { &self, field_type: &FieldType, field_rev: &Arc, - ) -> FlowyResult>>> { - let mut group_controller: Option>> = None; + ) -> FlowyResult>> { + let mut group_controller: Option> = None; match field_type { FieldType::RichText => { // let generator = GroupGenerator::::from_configuration(configuration); @@ -210,7 +195,7 @@ impl GroupService { ) .await?; let controller = SingleSelectGroupController::new(field_rev, configuration).await?; - group_controller = Some(Arc::new(RwLock::new(controller))); + group_controller = Some(Box::new(controller)); } FieldType::MultiSelect => { let configuration = SelectOptionGroupConfiguration::new( @@ -220,7 +205,7 @@ impl GroupService { ) .await?; let controller = MultiSelectGroupController::new(field_rev, configuration).await?; - group_controller = Some(Arc::new(RwLock::new(controller))); + group_controller = Some(Box::new(controller)); } FieldType::Checkbox => { let configuration = CheckboxGroupConfiguration::new( @@ -230,7 +215,7 @@ impl GroupService { ) .await?; let controller = CheckboxGroupController::new(field_rev, configuration).await?; - group_controller = Some(Arc::new(RwLock::new(controller))) + group_controller = Some(Box::new(controller)); } FieldType::URL => { // let generator = GroupGenerator::::from_configuration(configuration); 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 d284e69c76..94ef4dff1b 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 @@ -1,5 +1,7 @@ use crate::grid::grid_editor::GridEditorTest; -use flowy_grid::entities::{CreateRowParams, FieldType, GridLayout, GroupPB, MoveGroupParams, MoveRowParams, RowPB}; +use flowy_grid::entities::{ + CreateRowParams, FieldType, GridLayout, GroupPB, MoveGroupParams, MoveGroupRowParams, RowPB, +}; use flowy_grid::services::cell::insert_select_option_cell; use flowy_grid_data_model::revision::RowChangeset; @@ -75,14 +77,16 @@ impl GridGroupTest { } => { let groups: Vec = self.editor.load_groups().await.unwrap().items; let from_row = groups.get(from_group_index).unwrap().rows.get(from_row_index).unwrap(); - let to_row = groups.get(to_group_index).unwrap().rows.get(to_row_index).unwrap(); - let params = MoveRowParams { + let to_group = groups.get(to_group_index).unwrap(); + let to_row = to_group.rows.get(to_row_index).unwrap(); + let params = MoveGroupRowParams { view_id: self.inner.grid_id.clone(), from_row_id: from_row.id.clone(), - to_row_id: to_row.id.clone(), + to_group_id: to_group.group_id.clone(), + to_row_id: Some(to_row.id.clone()), }; - self.editor.move_row(params).await.unwrap(); + self.editor.move_group_row(params).await.unwrap(); } GroupScript::AssertRow { group_index, 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 6092e97a5e..bebf499773 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 @@ -2,7 +2,7 @@ use crate::grid::group_test::script::GridGroupTest; use crate::grid::group_test::script::GroupScript::*; #[tokio::test] -async fn board_init_test() { +async fn group_init_test() { let mut test = GridGroupTest::new().await; let scripts = vec![ AssertGroupCount(3), @@ -23,7 +23,7 @@ async fn board_init_test() { } #[tokio::test] -async fn board_move_row_test() { +async fn group_move_row_test() { let mut test = GridGroupTest::new().await; let group = test.group_at_index(0).await; let scripts = vec![ @@ -48,7 +48,7 @@ async fn board_move_row_test() { } #[tokio::test] -async fn board_move_row_to_other_group_test() { +async fn group_move_row_to_other_group_test() { let mut test = GridGroupTest::new().await; let group = test.group_at_index(0).await; let scripts = vec![ @@ -76,7 +76,7 @@ async fn board_move_row_to_other_group_test() { } #[tokio::test] -async fn board_move_row_to_other_group_and_reorder_test() { +async fn group_move_two_row_to_other_group_test() { let mut test = GridGroupTest::new().await; let group = test.group_at_index(0).await; let scripts = vec![ @@ -86,15 +86,41 @@ async fn board_move_row_to_other_group_and_reorder_test() { to_group_index: 1, to_row_index: 1, }, - MoveRow { - from_group_index: 1, - from_row_index: 1, - to_group_index: 1, - to_row_index: 2, + AssertGroupRowCount { + group_index: 0, + row_count: 1, + }, + AssertGroupRowCount { + group_index: 1, + row_count: 3, }, AssertRow { group_index: 1, - row_index: 2, + row_index: 1, + row: group.rows.get(0).unwrap().clone(), + }, + ]; + test.run_scripts(scripts).await; + + let group = test.group_at_index(0).await; + let scripts = vec![ + MoveRow { + from_group_index: 0, + from_row_index: 0, + to_group_index: 1, + to_row_index: 1, + }, + AssertGroupRowCount { + group_index: 0, + row_count: 0, + }, + AssertGroupRowCount { + group_index: 1, + row_count: 4, + }, + AssertRow { + group_index: 1, + row_index: 1, row: group.rows.get(0).unwrap().clone(), }, ]; @@ -102,7 +128,74 @@ async fn board_move_row_to_other_group_and_reorder_test() { } #[tokio::test] -async fn board_create_row_test() { +async fn group_move_row_to_other_group_and_reorder_from_up_to_down_test() { + let mut test = GridGroupTest::new().await; + let group_0 = test.group_at_index(0).await; + let group_1 = test.group_at_index(1).await; + let scripts = vec![ + MoveRow { + from_group_index: 0, + from_row_index: 0, + to_group_index: 1, + to_row_index: 1, + }, + AssertRow { + group_index: 1, + row_index: 1, + row: group_0.rows.get(0).unwrap().clone(), + }, + ]; + test.run_scripts(scripts).await; + + let scripts = vec![ + MoveRow { + from_group_index: 1, + from_row_index: 0, + to_group_index: 1, + to_row_index: 2, + }, + AssertRow { + group_index: 1, + row_index: 2, + row: group_1.rows.get(0).unwrap().clone(), + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn group_move_row_to_other_group_and_reorder_from_bottom_to_up_test() { + let mut test = GridGroupTest::new().await; + let scripts = vec![MoveRow { + from_group_index: 0, + from_row_index: 0, + to_group_index: 1, + to_row_index: 1, + }]; + test.run_scripts(scripts).await; + + let group = test.group_at_index(1).await; + let scripts = vec![ + AssertGroupRowCount { + group_index: 1, + row_count: 3, + }, + MoveRow { + from_group_index: 1, + from_row_index: 2, + to_group_index: 1, + to_row_index: 0, + }, + AssertRow { + group_index: 1, + row_index: 0, + row: group.rows.get(2).unwrap().clone(), + }, + ]; + test.run_scripts(scripts).await; +} +#[tokio::test] +async fn group_create_row_test() { let mut test = GridGroupTest::new().await; let scripts = vec![ CreateRow { group_index: 0 }, @@ -121,7 +214,7 @@ async fn board_create_row_test() { } #[tokio::test] -async fn board_delete_row_test() { +async fn group_delete_row_test() { let mut test = GridGroupTest::new().await; let scripts = vec![ DeleteRow { @@ -137,7 +230,7 @@ async fn board_delete_row_test() { } #[tokio::test] -async fn board_delete_all_row_test() { +async fn group_delete_all_row_test() { let mut test = GridGroupTest::new().await; let scripts = vec![ DeleteRow { @@ -157,7 +250,7 @@ async fn board_delete_all_row_test() { } #[tokio::test] -async fn board_update_row_test() { +async fn group_update_row_test() { let mut test = GridGroupTest::new().await; let scripts = vec![ // Update the row at 0 in group0 by setting the row's group field data @@ -179,7 +272,7 @@ async fn board_update_row_test() { } #[tokio::test] -async fn board_reorder_group_test() { +async fn group_reorder_group_test() { let mut test = GridGroupTest::new().await; let scripts = vec![ // Update the row at 0 in group0 by setting the row's group field data @@ -201,7 +294,7 @@ async fn board_reorder_group_test() { } #[tokio::test] -async fn board_move_group_test() { +async fn group_move_group_test() { let mut test = GridGroupTest::new().await; let group_0 = test.group_at_index(0).await; let group_1 = test.group_at_index(1).await; diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs index 61a8a386d5..1f52ae93e2 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs @@ -76,7 +76,8 @@ where Some(objects_by_field_id) } - pub fn insert_object(&mut self, field_id: &str, field_type: &FieldTypeRevision, object: T) { + /// add object to the end of the list + pub fn add_object(&mut self, field_id: &str, field_type: &FieldTypeRevision, object: T) { let object_rev_map = self .inner .entry(field_id.to_string()) @@ -88,7 +89,7 @@ where .push(Arc::new(object)) } - pub fn remove_all(&mut self) { + pub fn clear(&mut self) { self.inner.clear() } } diff --git a/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs index 204bdf273f..97d45295fe 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs @@ -155,3 +155,27 @@ impl std::default::Default for DateCondition { DateCondition::Relative } } + +#[cfg(test)] +mod tests { + use crate::revision::{GroupConfigurationRevision, SelectOptionGroupConfigurationRevision}; + + #[test] + fn group_configuration_serde_test() { + let content = SelectOptionGroupConfigurationRevision { hide_empty: false }; + let rev = GroupConfigurationRevision::new("1".to_owned(), 2, content).unwrap(); + let json = serde_json::to_string(&rev).unwrap(); + + let rev: GroupConfigurationRevision = serde_json::from_str(&json).unwrap(); + let _content: SelectOptionGroupConfigurationRevision = serde_json::from_str(&rev.content).unwrap(); + } + + #[test] + fn group_configuration_serde_test2() { + let content = SelectOptionGroupConfigurationRevision { hide_empty: false }; + let content_json = serde_json::to_string(&content).unwrap(); + let rev = GroupConfigurationRevision::new("1".to_owned(), 2, content).unwrap(); + + assert_eq!(rev.content, content_json); + } +} diff --git a/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs b/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs index 57bd783d75..ddc9c6e05a 100644 --- a/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs +++ b/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs @@ -60,7 +60,9 @@ impl GridViewRevisionPad { group_rev: GroupConfigurationRevision, ) -> CollaborateResult> { self.modify(|view| { - view.groups.insert_object(field_id, field_type, group_rev); + // Only save one group + view.groups.clear(); + view.groups.add_object(field_id, field_type, group_rev); Ok(Some(())) }) } @@ -127,7 +129,7 @@ impl GridViewRevisionPad { filter_rev: FilterConfigurationRevision, ) -> CollaborateResult> { self.modify(|view| { - view.filters.insert_object(field_id, field_type, filter_rev); + view.filters.add_object(field_id, field_type, filter_rev); Ok(Some(())) }) } @@ -166,8 +168,6 @@ impl GridViewRevisionPad { None => Ok(None), Some(delta) => { self.delta = self.delta.compose(&delta)?; - tracing::info!("GridView: {:?}", delta); - let md5 = md5(&self.delta.json_bytes()); Ok(Some(GridViewRevisionChangeset { delta, md5 })) } From 886c1f00e541d6c1b11b646845d161ee98543d69 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 22 Aug 2022 17:51:47 +0800 Subject: [PATCH 17/86] feat: implement command + K to trigger link menu --- .../lib/src/render/link_menu/link_menu.dart | 17 ++++++++----- .../lib/src/render/toolbar/toolbar_item.dart | 24 ++++++++++++++----- ...pdate_text_style_by_command_x_handler.dart | 6 +++++ .../lib/src/service/toolbar_service.dart | 14 +++++++++++ 4 files changed, 49 insertions(+), 12 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart index 3ebf87f4fa..679bea09f7 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart @@ -21,12 +21,21 @@ class LinkMenu extends StatefulWidget { class _LinkMenuState extends State { final _textEditingController = TextEditingController(); + final _focusNode = FocusNode(); @override void initState() { super.initState(); _textEditingController.text = widget.linkText ?? ''; + _focusNode.requestFocus(); + } + + @override + void dispose() { + _focusNode.dispose(); + + super.dispose(); } @override @@ -88,7 +97,7 @@ class _LinkMenuState extends State { Widget _buildInput() { return TextField( - autofocus: true, + focusNode: _focusNode, style: const TextStyle(fontSize: 14.0), textAlign: TextAlign.left, controller: _textEditingController, @@ -112,11 +121,7 @@ class _LinkMenuState extends State { required VoidCallback onPressed, }) { return TextButton.icon( - icon: FlowySvg( - name: iconName, - width: 20.0, - height: 20.0, - ), + icon: FlowySvg(name: iconName), style: TextButton.styleFrom( minimumSize: const Size.fromHeight(40), padding: EdgeInsets.zero, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart index 49413311d2..a068722cb2 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart @@ -13,12 +13,14 @@ typedef ToolbarShowValidator = bool Function(EditorState editorState); class ToolbarItem { ToolbarItem({ + required this.id, required this.icon, this.tooltipsMessage = '', required this.validator, required this.handler, }); + final String id; final Widget icon; final String tooltipsMessage; final ToolbarShowValidator validator; @@ -26,6 +28,7 @@ class ToolbarItem { factory ToolbarItem.divider() { return ToolbarItem( + id: 'divider', icon: const FlowySvg(name: 'toolbar/divider'), validator: (editorState) => true, handler: (editorState, context) {}, @@ -35,18 +38,21 @@ class ToolbarItem { List defaultToolbarItems = [ ToolbarItem( + id: 'appflowy.toolbar.h1', tooltipsMessage: 'Heading 1', icon: const FlowySvg(name: 'toolbar/h1'), validator: _onlyShowInSingleTextSelection, handler: (editorState, context) => formatHeading(editorState, StyleKey.h1), ), ToolbarItem( + id: 'appflowy.toolbar.h2', tooltipsMessage: 'Heading 2', icon: const FlowySvg(name: 'toolbar/h2'), validator: _onlyShowInSingleTextSelection, handler: (editorState, context) => formatHeading(editorState, StyleKey.h2), ), ToolbarItem( + id: 'appflowy.toolbar.h3', tooltipsMessage: 'Heading 3', icon: const FlowySvg(name: 'toolbar/h3'), validator: _onlyShowInSingleTextSelection, @@ -54,24 +60,28 @@ List defaultToolbarItems = [ ), ToolbarItem.divider(), ToolbarItem( + id: 'appflowy.toolbar.bold', tooltipsMessage: 'Bold', icon: const FlowySvg(name: 'toolbar/bold'), validator: _showInTextSelection, handler: (editorState, context) => formatBold(editorState), ), ToolbarItem( + id: 'appflowy.toolbar.italic', tooltipsMessage: 'Italic', icon: const FlowySvg(name: 'toolbar/italic'), validator: _showInTextSelection, handler: (editorState, context) => formatItalic(editorState), ), ToolbarItem( + id: 'appflowy.toolbar.underline', tooltipsMessage: 'Underline', icon: const FlowySvg(name: 'toolbar/underline'), validator: _showInTextSelection, handler: (editorState, context) => formatUnderline(editorState), ), ToolbarItem( + id: 'appflowy.toolbar.strikethrough', tooltipsMessage: 'Strikethrough', icon: const FlowySvg(name: 'toolbar/strikethrough'), validator: _showInTextSelection, @@ -79,12 +89,14 @@ List defaultToolbarItems = [ ), ToolbarItem.divider(), ToolbarItem( + id: 'appflowy.toolbar.quote', tooltipsMessage: 'Quote', icon: const FlowySvg(name: 'toolbar/quote'), validator: _onlyShowInSingleTextSelection, handler: (editorState, context) => formatQuote(editorState), ), ToolbarItem( + id: 'appflowy.toolbar.bulleted_list', tooltipsMessage: 'Bulleted list', icon: const FlowySvg(name: 'toolbar/bulleted_list'), validator: _onlyShowInSingleTextSelection, @@ -92,12 +104,14 @@ List defaultToolbarItems = [ ), ToolbarItem.divider(), ToolbarItem( + id: 'appflowy.toolbar.link', tooltipsMessage: 'Link', icon: const FlowySvg(name: 'toolbar/link'), validator: _onlyShowInSingleTextSelection, handler: (editorState, context) => _showLinkMenu(editorState, context), ), ToolbarItem( + id: 'appflowy.toolbar.highlight', tooltipsMessage: 'Highlight', icon: const FlowySvg(name: 'toolbar/highlight'), validator: _showInTextSelection, @@ -152,9 +166,7 @@ void _showLinkMenu(EditorState editorState, BuildContext context) { linkText: linkText, onSubmitted: (text) { TransactionBuilder(editorState) - ..formatText(node, index, length, { - StyleKey.href: text, - }) + ..formatText(node, index, length, {StyleKey.href: text}) ..commit(); _dismissLinkMenu(); }, @@ -164,9 +176,7 @@ void _showLinkMenu(EditorState editorState, BuildContext context) { }, onRemoveLink: () { TransactionBuilder(editorState) - ..formatText(node, index, length, { - StyleKey.href: null, - }) + ..formatText(node, index, length, {StyleKey.href: null}) ..commit(); _dismissLinkMenu(); }, @@ -177,6 +187,7 @@ void _showLinkMenu(EditorState editorState, BuildContext context) { Overlay.of(context)?.insert(_linkMenuOverlay!); editorState.service.scrollService?.disable(); + editorState.service.keyboardService?.disable(); editorState.service.selectionService.currentSelection .addListener(_dismissLinkMenu); } @@ -186,6 +197,7 @@ void _dismissLinkMenu() { _linkMenuOverlay = null; _editorState?.service.scrollService?.enable(); + _editorState?.service.keyboardService?.enable(); _editorState?.service.selectionService.currentSelection .removeListener(_dismissLinkMenu); _editorState = null; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart index 0eb926525b..00b304f527 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart @@ -36,6 +36,12 @@ AppFlowyKeyEventHandler updateTextStyleByCommandXHandler = event.isShiftPressed) { formatHighlight(editorState); return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.keyK) { + if (editorState.service.toolbarService + ?.triggerHandler('appflowy.toolbar.link') == + true) { + return KeyEventResult.handled; + } } return KeyEventResult.ignored; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart index fe4a2beace..290fe4b4bb 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart @@ -11,6 +11,9 @@ abstract class FlowyToolbarService { /// Hide the toolbar widget. void hide(); + + /// Trigger the specified handler. + bool triggerHandler(String id); } class FlowyToolbar extends StatefulWidget { @@ -55,6 +58,17 @@ class _FlowyToolbarState extends State _toolbarOverlay = null; } + @override + bool triggerHandler(String id) { + final items = defaultToolbarItems.where((item) => item.id == id); + if (items.length != 1) { + assert(items.length == 1, 'The toolbar item\'s id must be unique'); + return false; + } + items.first.handler(widget.editorState, context); + return true; + } + @override Widget build(BuildContext context) { return Container( From 1cd9a77e007538d1056cf864bb8225bef0dbd022 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 22 Aug 2022 18:16:15 +0800 Subject: [PATCH 18/86] fix: could not open link without scheme --- .../lib/src/render/rich_text/rich_text_style.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart index 7bd68c45e7..6bc50c5115 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart @@ -277,7 +277,13 @@ class RichTextStyle { if (href != null) { return TapGestureRecognizer() ..onTap = () async { - await launchUrlString(href); + final uri = Uri.parse(href); + // url_launcher cannot open a link without scheme. + final newHref = + (uri.scheme.isNotEmpty ? href : 'http://$href').trim(); + if (await canLaunchUrlString(newHref)) { + await launchUrlString(newHref); + } }; } return null; From 6517adece7a92bba53d774ad532dd11c82be4cba Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 22 Aug 2022 18:19:21 +0800 Subject: [PATCH 19/86] fix: show remove link and copy link entry when there is not linkd text --- .../lib/src/render/link_menu/link_menu.dart | 55 +++++++++---------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart index 679bea09f7..3a519ed70f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart @@ -40,33 +40,30 @@ class _LinkMenuState extends State { @override Widget build(BuildContext context) { - return SizedBox( - width: 350, - height: 200, - child: Container( - decoration: BoxDecoration( - color: Colors.white, - boxShadow: [ - BoxShadow( - blurRadius: 5, - spreadRadius: 1, - color: Colors.black.withOpacity(0.1), - ), - ], - borderRadius: BorderRadius.circular(6.0), - ), - child: SizedBox( - width: 350, - height: 200, - child: Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildHeader(), - const SizedBox(height: 16.0), - _buildInput(), - const SizedBox(height: 16.0), + return Container( + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + blurRadius: 5, + spreadRadius: 1, + color: Colors.black.withOpacity(0.1), + ), + ], + borderRadius: BorderRadius.circular(6.0), + ), + child: SizedBox( + width: 350, + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeader(), + const SizedBox(height: 16.0), + _buildInput(), + const SizedBox(height: 16.0), + if (widget.linkText != null) ...[ _buildIconButton( iconName: 'link', text: 'Copy link', @@ -77,8 +74,8 @@ class _LinkMenuState extends State { text: 'Remove link', onPressed: widget.onRemoveLink, ), - ], - ), + ] + ], ), ), ), From 6ded9aafd90c053476ab971df4ebc5e944b840fe Mon Sep 17 00:00:00 2001 From: appflowy Date: Mon, 22 Aug 2022 18:23:06 +0800 Subject: [PATCH 20/86] fix: disable flowyoverly focusNode --- .../flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart index ad04dc25c2..25507b8f6d 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart @@ -338,7 +338,9 @@ class FlowyOverlayState extends State { Widget build(BuildContext context) { final overlays = _overlayList.map((item) { var widget = item.widget; - item.focusNode.requestFocus(); + + // requestFocus will cause the children weird focus behaviors. + // item.focusNode.requestFocus(); if (item.delegate?.asBarrier() ?? false) { widget = Container( color: style.barrierColor, From 7d404ff0dadfec5ba96302c8f853aea0951f11e7 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Tue, 16 Aug 2022 16:25:52 +0800 Subject: [PATCH 21/86] feat: document model in rust --- shared-lib/Cargo.lock | 7 +++ shared-lib/lib-ot/Cargo.toml | 1 + .../lib-ot/src/core/document/attributes.rs | 9 ++++ .../lib-ot/src/core/document/document.rs | 52 +++++++++++++++++++ shared-lib/lib-ot/src/core/document/mod.rs | 8 +++ shared-lib/lib-ot/src/core/document/node.rs | 15 ++++++ .../lib-ot/src/core/document/position.rs | 7 +++ shared-lib/lib-ot/src/core/mod.rs | 2 + shared-lib/lib-ot/tests/main.rs | 6 +++ 9 files changed, 107 insertions(+) create mode 100644 shared-lib/lib-ot/src/core/document/attributes.rs create mode 100644 shared-lib/lib-ot/src/core/document/document.rs create mode 100644 shared-lib/lib-ot/src/core/document/mod.rs create mode 100644 shared-lib/lib-ot/src/core/document/node.rs create mode 100644 shared-lib/lib-ot/src/core/document/position.rs diff --git a/shared-lib/Cargo.lock b/shared-lib/Cargo.lock index da8bc0ff1c..596f1fe694 100644 --- a/shared-lib/Cargo.lock +++ b/shared-lib/Cargo.lock @@ -741,6 +741,12 @@ dependencies = [ "serde", ] +[[package]] +name = "indextree" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b4b46b3311ebd8e5cd44f6b03b36e0f48a70552cf6b036afcebc5626794066" + [[package]] name = "instant" version = "0.1.12" @@ -809,6 +815,7 @@ dependencies = [ "bytes", "dashmap", "derive_more", + "indextree", "lazy_static", "log", "md5", diff --git a/shared-lib/lib-ot/Cargo.toml b/shared-lib/lib-ot/Cargo.toml index a1a577132e..d47968dcfe 100644 --- a/shared-lib/lib-ot/Cargo.toml +++ b/shared-lib/lib-ot/Cargo.toml @@ -24,6 +24,7 @@ lazy_static = "1.4.0" strum = "0.21" strum_macros = "0.21" bytes = "1.0" +indextree = "4.4.0" [features] diff --git a/shared-lib/lib-ot/src/core/document/attributes.rs b/shared-lib/lib-ot/src/core/document/attributes.rs new file mode 100644 index 0000000000..ae9d1fbffb --- /dev/null +++ b/shared-lib/lib-ot/src/core/document/attributes.rs @@ -0,0 +1,9 @@ +use std::collections::HashMap; + +pub struct NodeAttributes(HashMap>); + +impl NodeAttributes { + pub fn new() -> NodeAttributes { + NodeAttributes(HashMap::new()) + } +} diff --git a/shared-lib/lib-ot/src/core/document/document.rs b/shared-lib/lib-ot/src/core/document/document.rs new file mode 100644 index 0000000000..fa8e2aea6a --- /dev/null +++ b/shared-lib/lib-ot/src/core/document/document.rs @@ -0,0 +1,52 @@ +use crate::core::document::position::Position; +use crate::core::NodeData; +use indextree::{Arena, NodeId}; + +pub struct DocumentTree { + arena: Arena, + root: NodeId, +} + +impl DocumentTree { + pub fn new() -> DocumentTree { + let mut arena = Arena::new(); + let root = arena.new_node(NodeData::new("root".into())); + DocumentTree { + arena: Arena::new(), + root, + } + } + + pub fn node_at_path(&self, position: &Position) -> Option { + if position.is_empty() { + return None; + } + + let mut iterate_node = self.root; + + for id in &position.0 { + let child = self.child_at_index_of_path(iterate_node, id.clone()); + iterate_node = match child { + Some(node) => node, + None => return None, + }; + } + + Some(iterate_node) + } + + fn child_at_index_of_path(&self, at_node: NodeId, index: usize) -> Option { + let children = at_node.children(&self.arena); + + let mut counter = 0; + for child in children { + if counter == index { + return Some(child); + } + + counter += 1; + } + + None + } +} diff --git a/shared-lib/lib-ot/src/core/document/mod.rs b/shared-lib/lib-ot/src/core/document/mod.rs new file mode 100644 index 0000000000..efbf9f362c --- /dev/null +++ b/shared-lib/lib-ot/src/core/document/mod.rs @@ -0,0 +1,8 @@ +mod attributes; +mod document; +mod node; +mod position; + +pub use attributes::*; +pub use document::*; +pub use node::*; diff --git a/shared-lib/lib-ot/src/core/document/node.rs b/shared-lib/lib-ot/src/core/document/node.rs new file mode 100644 index 0000000000..7f0f172cd6 --- /dev/null +++ b/shared-lib/lib-ot/src/core/document/node.rs @@ -0,0 +1,15 @@ +use crate::core::NodeAttributes; + +pub struct NodeData { + pub node_type: String, + pub attributes: NodeAttributes, +} + +impl NodeData { + pub fn new(node_type: &str) -> NodeData { + NodeData { + node_type: node_type.into(), + attributes: NodeAttributes::new(), + } + } +} diff --git a/shared-lib/lib-ot/src/core/document/position.rs b/shared-lib/lib-ot/src/core/document/position.rs new file mode 100644 index 0000000000..6e1983b440 --- /dev/null +++ b/shared-lib/lib-ot/src/core/document/position.rs @@ -0,0 +1,7 @@ +pub struct Position(pub Vec); + +impl Position { + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} diff --git a/shared-lib/lib-ot/src/core/mod.rs b/shared-lib/lib-ot/src/core/mod.rs index 7c1ed3f2ef..262233c85a 100644 --- a/shared-lib/lib-ot/src/core/mod.rs +++ b/shared-lib/lib-ot/src/core/mod.rs @@ -1,9 +1,11 @@ mod delta; +mod document; mod interval; mod operation; mod ot_str; pub use delta::*; +pub use document::*; pub use interval::*; pub use operation::*; pub use ot_str::*; diff --git a/shared-lib/lib-ot/tests/main.rs b/shared-lib/lib-ot/tests/main.rs index 8b13789179..2c766017e4 100644 --- a/shared-lib/lib-ot/tests/main.rs +++ b/shared-lib/lib-ot/tests/main.rs @@ -1 +1,7 @@ +use lib_ot::core::DocumentTree; +#[test] +fn main() { + // Create a new arena + let _document = DocumentTree::new(); +} From 2466b3eebcd51d927e63e239961a282fe2fcfbcd Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 17 Aug 2022 16:20:56 +0800 Subject: [PATCH 22/86] feat: add operations of the document --- .../lib-ot/src/core/document/attributes.rs | 1 + .../src/core/document/document_operation.rs | 57 +++++++++++++++++++ shared-lib/lib-ot/src/core/document/mod.rs | 2 + .../lib-ot/src/core/document/position.rs | 1 + 4 files changed, 61 insertions(+) create mode 100644 shared-lib/lib-ot/src/core/document/document_operation.rs diff --git a/shared-lib/lib-ot/src/core/document/attributes.rs b/shared-lib/lib-ot/src/core/document/attributes.rs index ae9d1fbffb..1b898ccbb3 100644 --- a/shared-lib/lib-ot/src/core/document/attributes.rs +++ b/shared-lib/lib-ot/src/core/document/attributes.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; +#[derive(Clone)] pub struct NodeAttributes(HashMap>); impl NodeAttributes { diff --git a/shared-lib/lib-ot/src/core/document/document_operation.rs b/shared-lib/lib-ot/src/core/document/document_operation.rs new file mode 100644 index 0000000000..b1e797a146 --- /dev/null +++ b/shared-lib/lib-ot/src/core/document/document_operation.rs @@ -0,0 +1,57 @@ +use crate::core::document::position::Position; +use crate::core::{NodeAttributes, TextDelta}; +use indextree::NodeId; + +pub enum DocumentOperation { + Insert(InsertOperation), + Update(UpdateOperation), + Delete(DeleteOperation), + TextEdit(TextEditOperation), +} + +impl DocumentOperation { + pub fn invert(&self) -> DocumentOperation { + match self { + DocumentOperation::Insert(insert_operation) => DocumentOperation::Delete(DeleteOperation { + path: insert_operation.path.clone(), + nodes: insert_operation.nodes.clone(), + }), + DocumentOperation::Update(update_operation) => DocumentOperation::Update(UpdateOperation { + path: update_operation.path.clone(), + attributes: update_operation.old_attributes.clone(), + old_attributes: update_operation.attributes.clone(), + }), + DocumentOperation::Delete(delete_operation) => DocumentOperation::Insert(InsertOperation { + path: delete_operation.path.clone(), + nodes: delete_operation.nodes.clone(), + }), + DocumentOperation::TextEdit(text_edit_operation) => DocumentOperation::TextEdit(TextEditOperation { + path: text_edit_operation.path.clone(), + delta: text_edit_operation.inverted.clone(), + inverted: text_edit_operation.delta.clone(), + }), + } + } +} + +pub struct InsertOperation { + path: Position, + nodes: Vec, +} + +pub struct UpdateOperation { + path: Position, + attributes: NodeAttributes, + old_attributes: NodeAttributes, +} + +pub struct DeleteOperation { + path: Position, + nodes: Vec, +} + +pub struct TextEditOperation { + path: Position, + delta: TextDelta, + inverted: TextDelta, +} diff --git a/shared-lib/lib-ot/src/core/document/mod.rs b/shared-lib/lib-ot/src/core/document/mod.rs index efbf9f362c..81008fa50e 100644 --- a/shared-lib/lib-ot/src/core/document/mod.rs +++ b/shared-lib/lib-ot/src/core/document/mod.rs @@ -1,8 +1,10 @@ mod attributes; mod document; +mod document_operation; mod node; mod position; pub use attributes::*; pub use document::*; +pub use document_operation::*; pub use node::*; diff --git a/shared-lib/lib-ot/src/core/document/position.rs b/shared-lib/lib-ot/src/core/document/position.rs index 6e1983b440..60a69ce8be 100644 --- a/shared-lib/lib-ot/src/core/document/position.rs +++ b/shared-lib/lib-ot/src/core/document/position.rs @@ -1,3 +1,4 @@ +#[derive(Clone)] pub struct Position(pub Vec); impl Position { From aa90613bf65a61691d64d8ba7b25c1d3c9e3a61c Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 17 Aug 2022 16:48:45 +0800 Subject: [PATCH 23/86] feat: transaction built der --- .../lib-ot/src/core/document/document.rs | 42 ++++++++++++++++++- shared-lib/lib-ot/src/core/document/mod.rs | 2 + .../lib-ot/src/core/document/transaction.rs | 25 +++++++++++ shared-lib/lib-ot/tests/main.rs | 9 +++- 4 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 shared-lib/lib-ot/src/core/document/transaction.rs diff --git a/shared-lib/lib-ot/src/core/document/document.rs b/shared-lib/lib-ot/src/core/document/document.rs index fa8e2aea6a..7acd357159 100644 --- a/shared-lib/lib-ot/src/core/document/document.rs +++ b/shared-lib/lib-ot/src/core/document/document.rs @@ -1,5 +1,5 @@ use crate::core::document::position::Position; -use crate::core::NodeData; +use crate::core::{NodeData, Transaction}; use indextree::{Arena, NodeId}; pub struct DocumentTree { @@ -35,6 +35,42 @@ impl DocumentTree { Some(iterate_node) } + pub fn path_of_node(&self, node_id: NodeId) -> Position { + let mut path: Vec = Vec::new(); + + let mut ancestors = node_id.ancestors(&self.arena); + let mut current_node = node_id; + let mut parent = ancestors.next(); + + while parent.is_some() { + let parent_node = parent.unwrap(); + let counter = self.index_of_node(parent_node, current_node); + path.push(counter); + current_node = parent_node; + parent = ancestors.next(); + } + + Position(path) + } + + fn index_of_node(&self, parent_node: NodeId, child_node: NodeId) -> usize { + let mut counter: usize = 0; + + let mut children_iterator = parent_node.children(&self.arena); + let mut node = children_iterator.next(); + + while node.is_some() { + if node.unwrap() == child_node { + return counter; + } + + node = children_iterator.next(); + counter += 1; + } + + counter + } + fn child_at_index_of_path(&self, at_node: NodeId, index: usize) -> Option { let children = at_node.children(&self.arena); @@ -49,4 +85,8 @@ impl DocumentTree { None } + + pub fn apply(&self, _transaction: Transaction) { + unimplemented!() + } } diff --git a/shared-lib/lib-ot/src/core/document/mod.rs b/shared-lib/lib-ot/src/core/document/mod.rs index 81008fa50e..968c78bc2b 100644 --- a/shared-lib/lib-ot/src/core/document/mod.rs +++ b/shared-lib/lib-ot/src/core/document/mod.rs @@ -3,8 +3,10 @@ mod document; mod document_operation; mod node; mod position; +mod transaction; pub use attributes::*; pub use document::*; pub use document_operation::*; pub use node::*; +pub use transaction::*; diff --git a/shared-lib/lib-ot/src/core/document/transaction.rs b/shared-lib/lib-ot/src/core/document/transaction.rs new file mode 100644 index 0000000000..7e48a4c544 --- /dev/null +++ b/shared-lib/lib-ot/src/core/document/transaction.rs @@ -0,0 +1,25 @@ +use crate::core::DocumentOperation; + +pub struct Transaction { + pub operations: Vec, +} + +pub struct TransactionBuilder { + operations: Vec, +} + +impl TransactionBuilder { + pub fn new() -> TransactionBuilder { + TransactionBuilder { operations: Vec::new() } + } + + pub fn push(&mut self, op: DocumentOperation) { + self.operations.push(op); + } + + pub fn finalize(self) -> Transaction { + Transaction { + operations: self.operations, + } + } +} diff --git a/shared-lib/lib-ot/tests/main.rs b/shared-lib/lib-ot/tests/main.rs index 2c766017e4..cf2a6a94f3 100644 --- a/shared-lib/lib-ot/tests/main.rs +++ b/shared-lib/lib-ot/tests/main.rs @@ -1,7 +1,14 @@ -use lib_ot::core::DocumentTree; +use lib_ot::core::{DocumentTree, TransactionBuilder}; #[test] fn main() { // Create a new arena let _document = DocumentTree::new(); } + +#[test] +fn test_documents() { + let document = DocumentTree::new(); + let tb = TransactionBuilder::new(); + document.apply(tb.finalize()); +} From bb7c7e4f41fe48c806fb2710cda3e7599adef460 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 17 Aug 2022 17:43:58 +0800 Subject: [PATCH 24/86] feat: apply operation --- .../lib-ot/src/core/document/document.rs | 51 ++++++++++++++++++- .../src/core/document/document_operation.rs | 20 ++++---- shared-lib/lib-ot/tests/main.rs | 2 +- 3 files changed, 60 insertions(+), 13 deletions(-) diff --git a/shared-lib/lib-ot/src/core/document/document.rs b/shared-lib/lib-ot/src/core/document/document.rs index 7acd357159..bc62491c82 100644 --- a/shared-lib/lib-ot/src/core/document/document.rs +++ b/shared-lib/lib-ot/src/core/document/document.rs @@ -1,5 +1,5 @@ use crate::core::document::position::Position; -use crate::core::{NodeData, Transaction}; +use crate::core::{DeleteOperation, DocumentOperation, InsertOperation, NodeData, TextEditOperation, Transaction, UpdateOperation}; use indextree::{Arena, NodeId}; pub struct DocumentTree { @@ -86,7 +86,54 @@ impl DocumentTree { None } - pub fn apply(&self, _transaction: Transaction) { + pub fn apply(&mut self, transaction: Transaction) { + for op in &transaction.operations { + self.apply_op(op); + } + } + + fn apply_op(&mut self, op: &DocumentOperation) { + match op { + DocumentOperation::Insert(op) => self.apply_insert(op), + DocumentOperation::Update(op) => self.apply_update(op), + DocumentOperation::Delete(op) => self.apply_delete(op), + DocumentOperation::TextEdit(op) => self.apply_text_edit(op), + } + } + + fn apply_insert(&mut self, op: &InsertOperation) { + let parent_path = &op.path.0[0..(op.path.0.len() - 1)]; + let last_index = op.path.0[op.path.0.len() - 1]; + let parent_node = self.node_at_path(&Position(parent_path.to_vec())); + if let Some(parent_node) = parent_node { + self.insert_child_at_index(parent_node, last_index, &op.nodes); + } + } + + fn insert_child_at_index(&mut self, parent: NodeId, index: usize, insert_children: &[NodeId]) { + if index == 0 && insert_children.len() == 0 { + for id in insert_children { + parent.append(*id, &mut self.arena); + } + return; + } + + let node_to_insert = self.child_at_index_of_path(parent, index).unwrap(); + + for id in insert_children { + node_to_insert.insert_before(*id, &mut self.arena); + } + } + + fn apply_update(&self, _op: &UpdateOperation) { + unimplemented!() + } + + fn apply_delete(&self, _op: &DeleteOperation) { + unimplemented!() + } + + fn apply_text_edit(&self, _op: &TextEditOperation) { unimplemented!() } } diff --git a/shared-lib/lib-ot/src/core/document/document_operation.rs b/shared-lib/lib-ot/src/core/document/document_operation.rs index b1e797a146..d5c60cc45d 100644 --- a/shared-lib/lib-ot/src/core/document/document_operation.rs +++ b/shared-lib/lib-ot/src/core/document/document_operation.rs @@ -35,23 +35,23 @@ impl DocumentOperation { } pub struct InsertOperation { - path: Position, - nodes: Vec, + pub path: Position, + pub nodes: Vec, } pub struct UpdateOperation { - path: Position, - attributes: NodeAttributes, - old_attributes: NodeAttributes, + pub path: Position, + pub attributes: NodeAttributes, + pub old_attributes: NodeAttributes, } pub struct DeleteOperation { - path: Position, - nodes: Vec, + pub path: Position, + pub nodes: Vec, } pub struct TextEditOperation { - path: Position, - delta: TextDelta, - inverted: TextDelta, + pub path: Position, + pub delta: TextDelta, + pub inverted: TextDelta, } diff --git a/shared-lib/lib-ot/tests/main.rs b/shared-lib/lib-ot/tests/main.rs index cf2a6a94f3..d554c12cfb 100644 --- a/shared-lib/lib-ot/tests/main.rs +++ b/shared-lib/lib-ot/tests/main.rs @@ -8,7 +8,7 @@ fn main() { #[test] fn test_documents() { - let document = DocumentTree::new(); + let mut document = DocumentTree::new(); let tb = TransactionBuilder::new(); document.apply(tb.finalize()); } From ba160c8026dec010f3ca1fb6c38a4e9dfe3728e7 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Thu, 18 Aug 2022 16:19:50 +0800 Subject: [PATCH 25/86] feat: compose attributes --- .../lib-ot/src/core/document/attributes.rs | 12 ++++++++++ .../lib-ot/src/core/document/document.rs | 17 +++++++++---- shared-lib/lib-ot/src/core/document/node.rs | 9 ++++--- .../lib-ot/src/core/document/transaction.rs | 24 +++++++++++++++---- shared-lib/lib-ot/tests/main.rs | 5 ++-- 5 files changed, 52 insertions(+), 15 deletions(-) diff --git a/shared-lib/lib-ot/src/core/document/attributes.rs b/shared-lib/lib-ot/src/core/document/attributes.rs index 1b898ccbb3..011c4cec43 100644 --- a/shared-lib/lib-ot/src/core/document/attributes.rs +++ b/shared-lib/lib-ot/src/core/document/attributes.rs @@ -7,4 +7,16 @@ impl NodeAttributes { pub fn new() -> NodeAttributes { NodeAttributes(HashMap::new()) } + + pub fn compose(a: &NodeAttributes, b: &NodeAttributes) -> NodeAttributes { + let mut new_map: HashMap> = b.0.clone(); + + for (key, value) in &a.0 { + if b.0.contains_key(key.as_str()) { + new_map.insert(key.into(), value.clone()); + } + } + + NodeAttributes(new_map) + } } diff --git a/shared-lib/lib-ot/src/core/document/document.rs b/shared-lib/lib-ot/src/core/document/document.rs index bc62491c82..4200d4e398 100644 --- a/shared-lib/lib-ot/src/core/document/document.rs +++ b/shared-lib/lib-ot/src/core/document/document.rs @@ -1,5 +1,5 @@ use crate::core::document::position::Position; -use crate::core::{DeleteOperation, DocumentOperation, InsertOperation, NodeData, TextEditOperation, Transaction, UpdateOperation}; +use crate::core::{DeleteOperation, DocumentOperation, InsertOperation, NodeAttributes, NodeData, TextEditOperation, Transaction, UpdateOperation}; use indextree::{Arena, NodeId}; pub struct DocumentTree { @@ -125,12 +125,19 @@ impl DocumentTree { } } - fn apply_update(&self, _op: &UpdateOperation) { - unimplemented!() + fn apply_update(&self, op: &UpdateOperation) { + let update_node = self.node_at_path(&op.path).unwrap(); + let node_data = self.arena.get(update_node).unwrap(); + let new_attributes = { + let old_attributes = node_data.get().attributes.borrow(); + NodeAttributes::compose(&old_attributes, &op.attributes) + }; + node_data.get().attributes.replace(new_attributes); } - fn apply_delete(&self, _op: &DeleteOperation) { - unimplemented!() + fn apply_delete(&mut self, op: &DeleteOperation) { + let update_node = self.node_at_path(&op.path).unwrap(); + update_node.remove_subtree(&mut self.arena); } fn apply_text_edit(&self, _op: &TextEditOperation) { diff --git a/shared-lib/lib-ot/src/core/document/node.rs b/shared-lib/lib-ot/src/core/document/node.rs index 7f0f172cd6..2fa9706ce3 100644 --- a/shared-lib/lib-ot/src/core/document/node.rs +++ b/shared-lib/lib-ot/src/core/document/node.rs @@ -1,15 +1,18 @@ -use crate::core::NodeAttributes; +use std::cell::RefCell; +use crate::core::{TextDelta, NodeAttributes}; pub struct NodeData { pub node_type: String, - pub attributes: NodeAttributes, + pub attributes: RefCell, + pub delta: RefCell>, } impl NodeData { pub fn new(node_type: &str) -> NodeData { NodeData { node_type: node_type.into(), - attributes: NodeAttributes::new(), + attributes: RefCell::new(NodeAttributes::new()), + delta: RefCell::new(None), } } } diff --git a/shared-lib/lib-ot/src/core/document/transaction.rs b/shared-lib/lib-ot/src/core/document/transaction.rs index 7e48a4c544..c75cb16bea 100644 --- a/shared-lib/lib-ot/src/core/document/transaction.rs +++ b/shared-lib/lib-ot/src/core/document/transaction.rs @@ -1,16 +1,30 @@ -use crate::core::DocumentOperation; +use crate::core::{DocumentOperation, DocumentTree}; pub struct Transaction { pub operations: Vec, } -pub struct TransactionBuilder { +impl Transaction { + + fn new(operations: Vec) -> Transaction { + Transaction { + operations, + } + } + +} + +pub struct TransactionBuilder<'a> { + document: &'a DocumentTree, operations: Vec, } -impl TransactionBuilder { - pub fn new() -> TransactionBuilder { - TransactionBuilder { operations: Vec::new() } +impl<'a> TransactionBuilder<'a> { + pub fn new(document: &'a DocumentTree) -> TransactionBuilder { + TransactionBuilder { + document, + operations: Vec::new() + } } pub fn push(&mut self, op: DocumentOperation) { diff --git a/shared-lib/lib-ot/tests/main.rs b/shared-lib/lib-ot/tests/main.rs index d554c12cfb..ecfdfb4da2 100644 --- a/shared-lib/lib-ot/tests/main.rs +++ b/shared-lib/lib-ot/tests/main.rs @@ -9,6 +9,7 @@ fn main() { #[test] fn test_documents() { let mut document = DocumentTree::new(); - let tb = TransactionBuilder::new(); - document.apply(tb.finalize()); + let tb = TransactionBuilder::new(&document); + let transaction = tb.finalize(); + document.apply(transaction); } From c207bf36794ffb95449b92de26687d6c7c83767a Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Thu, 18 Aug 2022 17:49:20 +0800 Subject: [PATCH 26/86] feat: apply transactions --- .../lib-ot/src/core/document/document.rs | 28 ++++++++----- .../src/core/document/document_operation.rs | 7 ++-- shared-lib/lib-ot/src/core/document/mod.rs | 1 + shared-lib/lib-ot/src/core/document/node.rs | 3 +- .../lib-ot/src/core/document/transaction.rs | 42 ++++++++++++++----- shared-lib/lib-ot/tests/main.rs | 5 ++- 6 files changed, 58 insertions(+), 28 deletions(-) diff --git a/shared-lib/lib-ot/src/core/document/document.rs b/shared-lib/lib-ot/src/core/document/document.rs index 4200d4e398..9d1d071f95 100644 --- a/shared-lib/lib-ot/src/core/document/document.rs +++ b/shared-lib/lib-ot/src/core/document/document.rs @@ -1,25 +1,25 @@ use crate::core::document::position::Position; -use crate::core::{DeleteOperation, DocumentOperation, InsertOperation, NodeAttributes, NodeData, TextEditOperation, Transaction, UpdateOperation}; +use crate::core::{ + DeleteOperation, DocumentOperation, InsertOperation, NodeAttributes, NodeData, TextEditOperation, Transaction, + UpdateOperation, +}; use indextree::{Arena, NodeId}; pub struct DocumentTree { - arena: Arena, - root: NodeId, + pub arena: Arena, + pub root: NodeId, } impl DocumentTree { pub fn new() -> DocumentTree { let mut arena = Arena::new(); let root = arena.new_node(NodeData::new("root".into())); - DocumentTree { - arena: Arena::new(), - root, - } + DocumentTree { arena, root } } pub fn node_at_path(&self, position: &Position) -> Option { if position.is_empty() { - return None; + return Some(self.root); } let mut iterate_node = self.root; @@ -93,7 +93,7 @@ impl DocumentTree { } fn apply_op(&mut self, op: &DocumentOperation) { - match op { + match op { DocumentOperation::Insert(op) => self.apply_insert(op), DocumentOperation::Update(op) => self.apply_update(op), DocumentOperation::Delete(op) => self.apply_delete(op), @@ -106,12 +106,18 @@ impl DocumentTree { let last_index = op.path.0[op.path.0.len() - 1]; let parent_node = self.node_at_path(&Position(parent_path.to_vec())); if let Some(parent_node) = parent_node { - self.insert_child_at_index(parent_node, last_index, &op.nodes); + let mut inserted_nodes = Vec::new(); + + for node in &op.nodes { + inserted_nodes.push(self.arena.new_node(node.clone())); + } + + self.insert_child_at_index(parent_node, last_index, &inserted_nodes); } } fn insert_child_at_index(&mut self, parent: NodeId, index: usize, insert_children: &[NodeId]) { - if index == 0 && insert_children.len() == 0 { + if index == 0 && parent.children(&self.arena).next().is_none() { for id in insert_children { parent.append(*id, &mut self.arena); } diff --git a/shared-lib/lib-ot/src/core/document/document_operation.rs b/shared-lib/lib-ot/src/core/document/document_operation.rs index d5c60cc45d..7b7b0c4a62 100644 --- a/shared-lib/lib-ot/src/core/document/document_operation.rs +++ b/shared-lib/lib-ot/src/core/document/document_operation.rs @@ -1,6 +1,5 @@ use crate::core::document::position::Position; -use crate::core::{NodeAttributes, TextDelta}; -use indextree::NodeId; +use crate::core::{NodeAttributes, NodeData, TextDelta}; pub enum DocumentOperation { Insert(InsertOperation), @@ -36,7 +35,7 @@ impl DocumentOperation { pub struct InsertOperation { pub path: Position, - pub nodes: Vec, + pub nodes: Vec, } pub struct UpdateOperation { @@ -47,7 +46,7 @@ pub struct UpdateOperation { pub struct DeleteOperation { pub path: Position, - pub nodes: Vec, + pub nodes: Vec, } pub struct TextEditOperation { diff --git a/shared-lib/lib-ot/src/core/document/mod.rs b/shared-lib/lib-ot/src/core/document/mod.rs index 968c78bc2b..b019cb0f71 100644 --- a/shared-lib/lib-ot/src/core/document/mod.rs +++ b/shared-lib/lib-ot/src/core/document/mod.rs @@ -9,4 +9,5 @@ pub use attributes::*; pub use document::*; pub use document_operation::*; pub use node::*; +pub use position::*; pub use transaction::*; diff --git a/shared-lib/lib-ot/src/core/document/node.rs b/shared-lib/lib-ot/src/core/document/node.rs index 2fa9706ce3..a3ec5e729a 100644 --- a/shared-lib/lib-ot/src/core/document/node.rs +++ b/shared-lib/lib-ot/src/core/document/node.rs @@ -1,6 +1,7 @@ +use crate::core::{NodeAttributes, TextDelta}; use std::cell::RefCell; -use crate::core::{TextDelta, NodeAttributes}; +#[derive(Clone)] pub struct NodeData { pub node_type: String, pub attributes: RefCell, diff --git a/shared-lib/lib-ot/src/core/document/transaction.rs b/shared-lib/lib-ot/src/core/document/transaction.rs index c75cb16bea..875b1bebdf 100644 --- a/shared-lib/lib-ot/src/core/document/transaction.rs +++ b/shared-lib/lib-ot/src/core/document/transaction.rs @@ -1,17 +1,14 @@ -use crate::core::{DocumentOperation, DocumentTree}; +use crate::core::document::position::Position; +use crate::core::{DeleteOperation, DocumentOperation, DocumentTree, InsertOperation, NodeData}; pub struct Transaction { pub operations: Vec, } impl Transaction { - fn new(operations: Vec) -> Transaction { - Transaction { - operations, - } + Transaction { operations } } - } pub struct TransactionBuilder<'a> { @@ -23,17 +20,42 @@ impl<'a> TransactionBuilder<'a> { pub fn new(document: &'a DocumentTree) -> TransactionBuilder { TransactionBuilder { document, - operations: Vec::new() + operations: Vec::new(), } } + pub fn insert_nodes(&mut self, path: &Position, nodes: &[NodeData]) { + self.push(DocumentOperation::Insert(InsertOperation { + path: path.clone(), + nodes: nodes.to_vec(), + })); + } + + pub fn delete_node(&mut self, path: &Position) { + self.delete_nodes(path, 1); + } + + pub fn delete_nodes(&mut self, path: &Position, length: usize) { + let mut node = self.document.node_at_path(path).unwrap(); + let mut deleted_nodes: Vec = Vec::new(); + + for _ in 0..length { + let data = self.document.arena.get(node).unwrap(); + deleted_nodes.push(data.get().clone()); + node = node.following_siblings(&self.document.arena).next().unwrap(); + } + + self.operations.push(DocumentOperation::Delete(DeleteOperation { + path: path.clone(), + nodes: deleted_nodes, + })) + } + pub fn push(&mut self, op: DocumentOperation) { self.operations.push(op); } pub fn finalize(self) -> Transaction { - Transaction { - operations: self.operations, - } + Transaction::new(self.operations) } } diff --git a/shared-lib/lib-ot/tests/main.rs b/shared-lib/lib-ot/tests/main.rs index ecfdfb4da2..8ed7c8a74e 100644 --- a/shared-lib/lib-ot/tests/main.rs +++ b/shared-lib/lib-ot/tests/main.rs @@ -1,4 +1,4 @@ -use lib_ot::core::{DocumentTree, TransactionBuilder}; +use lib_ot::core::{DocumentTree, NodeData, Position, TransactionBuilder}; #[test] fn main() { @@ -9,7 +9,8 @@ fn main() { #[test] fn test_documents() { let mut document = DocumentTree::new(); - let tb = TransactionBuilder::new(&document); + let mut tb = TransactionBuilder::new(&document); + tb.insert_nodes(&Position(vec![0]), &vec![NodeData::new("type")]); let transaction = tb.finalize(); document.apply(transaction); } From 8401fa0983cd49dc4acb5ae22c5b27c2530ab8dc Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Thu, 18 Aug 2022 20:15:34 +0800 Subject: [PATCH 27/86] feat: update attributes --- .../lib-ot/src/core/document/attributes.rs | 2 +- .../lib-ot/src/core/document/transaction.rs | 24 ++++++++++++++++++- shared-lib/lib-ot/tests/main.rs | 15 +++++++++++- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/shared-lib/lib-ot/src/core/document/attributes.rs b/shared-lib/lib-ot/src/core/document/attributes.rs index 011c4cec43..b682fd8404 100644 --- a/shared-lib/lib-ot/src/core/document/attributes.rs +++ b/shared-lib/lib-ot/src/core/document/attributes.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; #[derive(Clone)] -pub struct NodeAttributes(HashMap>); +pub struct NodeAttributes(pub HashMap>); impl NodeAttributes { pub fn new() -> NodeAttributes { diff --git a/shared-lib/lib-ot/src/core/document/transaction.rs b/shared-lib/lib-ot/src/core/document/transaction.rs index 875b1bebdf..f240b8ea89 100644 --- a/shared-lib/lib-ot/src/core/document/transaction.rs +++ b/shared-lib/lib-ot/src/core/document/transaction.rs @@ -1,5 +1,6 @@ +use std::collections::HashMap; use crate::core::document::position::Position; -use crate::core::{DeleteOperation, DocumentOperation, DocumentTree, InsertOperation, NodeData}; +use crate::core::{DeleteOperation, DocumentOperation, DocumentTree, InsertOperation, NodeAttributes, NodeData, UpdateOperation}; pub struct Transaction { pub operations: Vec, @@ -31,6 +32,27 @@ impl<'a> TransactionBuilder<'a> { })); } + pub fn update_attributes(&mut self, path: &Position, attributes: HashMap>) { + let mut old_attributes: HashMap> = HashMap::new(); + let node = self.document.node_at_path(path).unwrap(); + let node_data = self.document.arena.get(node).unwrap().get(); + + for key in attributes.keys() { + let old_attrs = node_data.attributes.borrow(); + let old_value = match old_attrs.0.get(key.as_str()) { + Some(value) => value.clone(), + None => None, + }; + old_attributes.insert(key.clone(), old_value); + } + + self.push(DocumentOperation::Update(UpdateOperation { + path: path.clone(), + attributes: NodeAttributes(attributes), + old_attributes: NodeAttributes(old_attributes), + })) + } + pub fn delete_node(&mut self, path: &Position) { self.delete_nodes(path, 1); } diff --git a/shared-lib/lib-ot/tests/main.rs b/shared-lib/lib-ot/tests/main.rs index 8ed7c8a74e..2d1813f2c9 100644 --- a/shared-lib/lib-ot/tests/main.rs +++ b/shared-lib/lib-ot/tests/main.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use lib_ot::core::{DocumentTree, NodeData, Position, TransactionBuilder}; #[test] @@ -10,7 +11,19 @@ fn main() { fn test_documents() { let mut document = DocumentTree::new(); let mut tb = TransactionBuilder::new(&document); - tb.insert_nodes(&Position(vec![0]), &vec![NodeData::new("type")]); + tb.insert_nodes(&Position(vec![0]), &vec![NodeData::new("text")]); + let transaction = tb.finalize(); + document.apply(transaction); + + assert!(document.node_at_path(&Position(vec![0])).is_some()); + let node = document.node_at_path(&Position(vec![0])).unwrap(); + let node_data = document.arena.get(node).unwrap().get(); + assert_eq!(node_data.node_type, "text"); + + let mut tb = TransactionBuilder::new(&document); + tb.update_attributes(&Position(vec![0]), HashMap::from([ + ("subtype".into(), Some("bullet-list".into())), + ])); let transaction = tb.finalize(); document.apply(transaction); } From 61d181b22864849aa939f9152dc956a187eb3652 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Fri, 19 Aug 2022 12:04:43 +0800 Subject: [PATCH 28/86] feat: vec to position convertion --- shared-lib/lib-ot/src/core/document/position.rs | 6 ++++++ shared-lib/lib-ot/tests/main.rs | 16 +++++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/shared-lib/lib-ot/src/core/document/position.rs b/shared-lib/lib-ot/src/core/document/position.rs index 60a69ce8be..34e0c784d5 100644 --- a/shared-lib/lib-ot/src/core/document/position.rs +++ b/shared-lib/lib-ot/src/core/document/position.rs @@ -6,3 +6,9 @@ impl Position { self.0.is_empty() } } + +impl From> for Position { + fn from(v: Vec) -> Self { + Position(v) + } +} diff --git a/shared-lib/lib-ot/tests/main.rs b/shared-lib/lib-ot/tests/main.rs index 2d1813f2c9..c2e0226eb6 100644 --- a/shared-lib/lib-ot/tests/main.rs +++ b/shared-lib/lib-ot/tests/main.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; -use lib_ot::core::{DocumentTree, NodeData, Position, TransactionBuilder}; +use lib_ot::core::{DocumentTree, NodeData, TransactionBuilder}; #[test] fn main() { @@ -11,19 +11,25 @@ fn main() { fn test_documents() { let mut document = DocumentTree::new(); let mut tb = TransactionBuilder::new(&document); - tb.insert_nodes(&Position(vec![0]), &vec![NodeData::new("text")]); + tb.insert_nodes(&vec![0].into(), &vec![NodeData::new("text")]); let transaction = tb.finalize(); document.apply(transaction); - assert!(document.node_at_path(&Position(vec![0])).is_some()); - let node = document.node_at_path(&Position(vec![0])).unwrap(); + assert!(document.node_at_path(&vec![0].into()).is_some()); + let node = document.node_at_path(&vec![0].into()).unwrap(); let node_data = document.arena.get(node).unwrap().get(); assert_eq!(node_data.node_type, "text"); let mut tb = TransactionBuilder::new(&document); - tb.update_attributes(&Position(vec![0]), HashMap::from([ + tb.update_attributes(&vec![0].into(), HashMap::from([ ("subtype".into(), Some("bullet-list".into())), ])); let transaction = tb.finalize(); document.apply(transaction); + + let mut tb = TransactionBuilder::new(&document); + tb.delete_node(&vec![0].into()); + let transaction = tb.finalize(); + document.apply(transaction); + assert!(document.node_at_path(&vec![0].into()).is_none()); } From c61b4d08656696c3f6401833fe264e6d429e59bc Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Fri, 19 Aug 2022 14:43:11 +0800 Subject: [PATCH 29/86] fix: CI format error --- .../src/core/document/document_operation.rs | 8 +++++ .../lib-ot/src/core/document/position.rs | 32 +++++++++++++++++++ .../lib-ot/src/core/document/transaction.rs | 6 ++-- shared-lib/lib-ot/tests/main.rs | 9 +++--- 4 files changed, 49 insertions(+), 6 deletions(-) diff --git a/shared-lib/lib-ot/src/core/document/document_operation.rs b/shared-lib/lib-ot/src/core/document/document_operation.rs index 7b7b0c4a62..ff1a968af5 100644 --- a/shared-lib/lib-ot/src/core/document/document_operation.rs +++ b/shared-lib/lib-ot/src/core/document/document_operation.rs @@ -1,6 +1,7 @@ use crate::core::document::position::Position; use crate::core::{NodeAttributes, NodeData, TextDelta}; +#[derive(Clone)] pub enum DocumentOperation { Insert(InsertOperation), Update(UpdateOperation), @@ -31,24 +32,31 @@ impl DocumentOperation { }), } } + pub fn transform(_a: &DocumentOperation, b: &DocumentOperation) -> DocumentOperation { + b.clone() + } } +#[derive(Clone)] pub struct InsertOperation { pub path: Position, pub nodes: Vec, } +#[derive(Clone)] pub struct UpdateOperation { pub path: Position, pub attributes: NodeAttributes, pub old_attributes: NodeAttributes, } +#[derive(Clone)] pub struct DeleteOperation { pub path: Position, pub nodes: Vec, } +#[derive(Clone)] pub struct TextEditOperation { pub path: Position, pub delta: TextDelta, diff --git a/shared-lib/lib-ot/src/core/document/position.rs b/shared-lib/lib-ot/src/core/document/position.rs index 34e0c784d5..b463f207da 100644 --- a/shared-lib/lib-ot/src/core/document/position.rs +++ b/shared-lib/lib-ot/src/core/document/position.rs @@ -5,6 +5,38 @@ impl Position { pub fn is_empty(&self) -> bool { self.0.is_empty() } + pub fn len(&self) -> usize { + self.0.len() + } +} + +impl Position { + // delta is default to be 1 + pub fn transform(pre_insert_path: &Position, b: &Position, delta: usize) -> Position { + if pre_insert_path.len() > b.len() { + return b.clone(); + } + if pre_insert_path.is_empty() || b.is_empty() { + return b.clone(); + } + // check the prefix + for i in 0..(pre_insert_path.len()) { + if pre_insert_path.0[i] != b.0[i] { + return b.clone(); + } + } + let mut prefix: Vec = pre_insert_path.0[0..(pre_insert_path.len() - 1)].into(); + let mut suffix: Vec = b.0[pre_insert_path.0.len()..].into(); + let prev_insert_last: usize = *pre_insert_path.0.last().unwrap(); + let b_at_index = b.0[pre_insert_path.0.len() - 1]; + if prev_insert_last <= b_at_index { + prefix.push(b_at_index + delta); + } else { + prefix.push(b_at_index); + } + prefix.append(&mut suffix); + return Position(prefix); + } } impl From> for Position { diff --git a/shared-lib/lib-ot/src/core/document/transaction.rs b/shared-lib/lib-ot/src/core/document/transaction.rs index f240b8ea89..c0e7cc3243 100644 --- a/shared-lib/lib-ot/src/core/document/transaction.rs +++ b/shared-lib/lib-ot/src/core/document/transaction.rs @@ -1,6 +1,8 @@ -use std::collections::HashMap; use crate::core::document::position::Position; -use crate::core::{DeleteOperation, DocumentOperation, DocumentTree, InsertOperation, NodeAttributes, NodeData, UpdateOperation}; +use crate::core::{ + DeleteOperation, DocumentOperation, DocumentTree, InsertOperation, NodeAttributes, NodeData, UpdateOperation, +}; +use std::collections::HashMap; pub struct Transaction { pub operations: Vec, diff --git a/shared-lib/lib-ot/tests/main.rs b/shared-lib/lib-ot/tests/main.rs index c2e0226eb6..f6d5aa6ace 100644 --- a/shared-lib/lib-ot/tests/main.rs +++ b/shared-lib/lib-ot/tests/main.rs @@ -1,5 +1,5 @@ -use std::collections::HashMap; use lib_ot::core::{DocumentTree, NodeData, TransactionBuilder}; +use std::collections::HashMap; #[test] fn main() { @@ -21,9 +21,10 @@ fn test_documents() { assert_eq!(node_data.node_type, "text"); let mut tb = TransactionBuilder::new(&document); - tb.update_attributes(&vec![0].into(), HashMap::from([ - ("subtype".into(), Some("bullet-list".into())), - ])); + tb.update_attributes( + &vec![0].into(), + HashMap::from([("subtype".into(), Some("bullet-list".into()))]), + ); let transaction = tb.finalize(); document.apply(transaction); From 0def48d0ca917f3194d2aa563127e6a4726be727 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 22 Aug 2022 11:10:06 +0800 Subject: [PATCH 30/86] feat: transform patht --- .../lib/src/operation/operation.dart | 4 +- .../lib-ot/src/core/document/document.rs | 9 ++++ .../src/core/document/document_operation.rs | 44 ++++++++++++++++++- .../lib-ot/src/core/document/position.rs | 4 +- shared-lib/lib-ot/tests/main.rs | 20 +++++++++ 5 files changed, 75 insertions(+), 6 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/operation.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/operation.dart index d1a0024a98..456be06766 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/operation.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/operation.dart @@ -207,10 +207,10 @@ Path transformPath(Path preInsertPath, Path b, [int delta = 1]) { Operation transformOperation(Operation a, Operation b) { if (a is InsertOperation) { - final newPath = transformPath(a.path, b.path); + final newPath = transformPath(a.path, b.path, a.nodes.length); return b.copyWithPath(newPath); } else if (a is DeleteOperation) { - final newPath = transformPath(a.path, b.path, -1); + final newPath = transformPath(a.path, b.path, -1 * a.nodes.length); return b.copyWithPath(newPath); } // TODO: transform update and textedit diff --git a/shared-lib/lib-ot/src/core/document/document.rs b/shared-lib/lib-ot/src/core/document/document.rs index 9d1d071f95..07626e1d75 100644 --- a/shared-lib/lib-ot/src/core/document/document.rs +++ b/shared-lib/lib-ot/src/core/document/document.rs @@ -124,6 +124,15 @@ impl DocumentTree { return; } + let children_length = parent.children(&self.arena).fold(0, |counter, _| counter + 1); + + if index == children_length { + for id in insert_children { + parent.append(*id, &mut self.arena); + } + return; + } + let node_to_insert = self.child_at_index_of_path(parent, index).unwrap(); for id in insert_children { diff --git a/shared-lib/lib-ot/src/core/document/document_operation.rs b/shared-lib/lib-ot/src/core/document/document_operation.rs index ff1a968af5..5a19f45a75 100644 --- a/shared-lib/lib-ot/src/core/document/document_operation.rs +++ b/shared-lib/lib-ot/src/core/document/document_operation.rs @@ -10,6 +10,14 @@ pub enum DocumentOperation { } impl DocumentOperation { + pub fn path(&self) -> &Position { + match self { + DocumentOperation::Insert(insert) => &insert.path, + DocumentOperation::Update(update) => &update.path, + DocumentOperation::Delete(delete) => &delete.path, + DocumentOperation::TextEdit(text_edit) => &text_edit.path, + } + } pub fn invert(&self) -> DocumentOperation { match self { DocumentOperation::Insert(insert_operation) => DocumentOperation::Delete(DeleteOperation { @@ -32,8 +40,40 @@ impl DocumentOperation { }), } } - pub fn transform(_a: &DocumentOperation, b: &DocumentOperation) -> DocumentOperation { - b.clone() + pub fn clone_with_new_path(&self, path: Position) -> DocumentOperation { + match self { + DocumentOperation::Insert(insert) => DocumentOperation::Insert(InsertOperation { + path, + nodes: insert.nodes.clone(), + }), + DocumentOperation::Update(update) => DocumentOperation::Update(UpdateOperation { + path, + attributes: update.attributes.clone(), + old_attributes: update.old_attributes.clone(), + }), + DocumentOperation::Delete(delete) => DocumentOperation::Delete(DeleteOperation { + path, + nodes: delete.nodes.clone(), + }), + DocumentOperation::TextEdit(text_edit) => DocumentOperation::TextEdit(TextEditOperation { + path, + delta: text_edit.delta.clone(), + inverted: text_edit.inverted.clone(), + }), + } + } + pub fn transform(a: &DocumentOperation, b: &DocumentOperation) -> DocumentOperation { + match a { + DocumentOperation::Insert(insert_op) => { + let new_path = Position::transform(a.path(), b.path(), insert_op.nodes.len() as i64); + b.clone_with_new_path(new_path) + } + DocumentOperation::Delete(delete_op) => { + let new_path = Position::transform(a.path(), b.path(), delete_op.nodes.len() as i64); + b.clone_with_new_path(new_path) + } + _ => b.clone(), + } } } diff --git a/shared-lib/lib-ot/src/core/document/position.rs b/shared-lib/lib-ot/src/core/document/position.rs index b463f207da..76f6a0eec4 100644 --- a/shared-lib/lib-ot/src/core/document/position.rs +++ b/shared-lib/lib-ot/src/core/document/position.rs @@ -12,7 +12,7 @@ impl Position { impl Position { // delta is default to be 1 - pub fn transform(pre_insert_path: &Position, b: &Position, delta: usize) -> Position { + pub fn transform(pre_insert_path: &Position, b: &Position, delta: i64) -> Position { if pre_insert_path.len() > b.len() { return b.clone(); } @@ -30,7 +30,7 @@ impl Position { let prev_insert_last: usize = *pre_insert_path.0.last().unwrap(); let b_at_index = b.0[pre_insert_path.0.len() - 1]; if prev_insert_last <= b_at_index { - prefix.push(b_at_index + delta); + prefix.push(((b_at_index as i64) + delta) as usize); } else { prefix.push(b_at_index); } diff --git a/shared-lib/lib-ot/tests/main.rs b/shared-lib/lib-ot/tests/main.rs index f6d5aa6ace..bfaf470dfa 100644 --- a/shared-lib/lib-ot/tests/main.rs +++ b/shared-lib/lib-ot/tests/main.rs @@ -34,3 +34,23 @@ fn test_documents() { document.apply(transaction); assert!(document.node_at_path(&vec![0].into()).is_none()); } + +#[test] +fn test_transform_paths() { + let mut document = DocumentTree::new(); + let transaction = { + let mut tb = TransactionBuilder::new(&document); + tb.insert_nodes(&vec![0].into(), &vec![NodeData::new("text")]); + tb.insert_nodes(&vec![1].into(), &vec![NodeData::new("text")]); + tb.insert_nodes(&vec![2].into(), &vec![NodeData::new("text")]); + tb.finalize() + }; + document.apply(transaction); + + let transaction = { + let mut tb = TransactionBuilder::new(&document); + tb.insert_nodes(&vec![1].into(), &vec![NodeData::new("text")]); + tb.finalize() + }; + document.apply(transaction); +} From 1801a47b1d7c384e8332e3104b99178bbd10390a Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 22 Aug 2022 14:44:59 +0800 Subject: [PATCH 31/86] feat: test update nodes --- shared-lib/lib-ot/tests/main.rs | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/shared-lib/lib-ot/tests/main.rs b/shared-lib/lib-ot/tests/main.rs index bfaf470dfa..becc91a31f 100644 --- a/shared-lib/lib-ot/tests/main.rs +++ b/shared-lib/lib-ot/tests/main.rs @@ -1,4 +1,4 @@ -use lib_ot::core::{DocumentTree, NodeData, TransactionBuilder}; +use lib_ot::core::{DocumentTree, NodeData, Position, TransactionBuilder}; use std::collections::HashMap; #[test] @@ -36,7 +36,7 @@ fn test_documents() { } #[test] -fn test_transform_paths() { +fn test_inserts_nodes() { let mut document = DocumentTree::new(); let transaction = { let mut tb = TransactionBuilder::new(&document); @@ -54,3 +54,30 @@ fn test_transform_paths() { }; document.apply(transaction); } + +#[test] +fn test_update_nodes() { + let mut document = DocumentTree::new(); + let transaction = { + let mut tb = TransactionBuilder::new(&document); + tb.insert_nodes(&vec![0].into(), &vec![NodeData::new("text")]); + tb.insert_nodes(&vec![1].into(), &vec![NodeData::new("text")]); + tb.insert_nodes(&vec![2].into(), &vec![NodeData::new("text")]); + tb.finalize() + }; + document.apply(transaction); + + let transaction = { + let mut tb = TransactionBuilder::new(&document); + tb.update_attributes(&vec![1].into(), HashMap::from([ + ("bolded".into(), Some("true".into())), + ])); + tb.finalize() + }; + document.apply(transaction); + + let node = document.node_at_path(&Position(vec![1])).unwrap(); + let node_data = document.arena.get(node).unwrap().get(); + let is_bold = node_data.attributes.borrow().0.get("bolded").unwrap().clone(); + assert_eq!(is_bold.unwrap(), "true"); +} From ef185cd5d5097e26b8908e7b19a609300662c14e Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 22 Aug 2022 14:58:44 +0800 Subject: [PATCH 32/86] refactor: add at_path suffix to transaction builder --- .../lib-ot/src/core/document/transaction.rs | 10 +-- shared-lib/lib-ot/tests/main.rs | 71 +++++++++++++------ 2 files changed, 54 insertions(+), 27 deletions(-) diff --git a/shared-lib/lib-ot/src/core/document/transaction.rs b/shared-lib/lib-ot/src/core/document/transaction.rs index c0e7cc3243..99d5ee8169 100644 --- a/shared-lib/lib-ot/src/core/document/transaction.rs +++ b/shared-lib/lib-ot/src/core/document/transaction.rs @@ -27,14 +27,14 @@ impl<'a> TransactionBuilder<'a> { } } - pub fn insert_nodes(&mut self, path: &Position, nodes: &[NodeData]) { + pub fn insert_nodes_at_path(&mut self, path: &Position, nodes: &[NodeData]) { self.push(DocumentOperation::Insert(InsertOperation { path: path.clone(), nodes: nodes.to_vec(), })); } - pub fn update_attributes(&mut self, path: &Position, attributes: HashMap>) { + pub fn update_attributes_at_path(&mut self, path: &Position, attributes: HashMap>) { let mut old_attributes: HashMap> = HashMap::new(); let node = self.document.node_at_path(path).unwrap(); let node_data = self.document.arena.get(node).unwrap().get(); @@ -55,11 +55,11 @@ impl<'a> TransactionBuilder<'a> { })) } - pub fn delete_node(&mut self, path: &Position) { - self.delete_nodes(path, 1); + pub fn delete_node_at_path(&mut self, path: &Position) { + self.delete_nodes_at_path(path, 1); } - pub fn delete_nodes(&mut self, path: &Position, length: usize) { + pub fn delete_nodes_at_path(&mut self, path: &Position, length: usize) { let mut node = self.document.node_at_path(path).unwrap(); let mut deleted_nodes: Vec = Vec::new(); diff --git a/shared-lib/lib-ot/tests/main.rs b/shared-lib/lib-ot/tests/main.rs index becc91a31f..1595c20f29 100644 --- a/shared-lib/lib-ot/tests/main.rs +++ b/shared-lib/lib-ot/tests/main.rs @@ -10,9 +10,11 @@ fn main() { #[test] fn test_documents() { let mut document = DocumentTree::new(); - let mut tb = TransactionBuilder::new(&document); - tb.insert_nodes(&vec![0].into(), &vec![NodeData::new("text")]); - let transaction = tb.finalize(); + let transaction = { + let mut tb = TransactionBuilder::new(&document); + tb.insert_nodes_at_path(&vec![0].into(), &vec![NodeData::new("text")]); + tb.finalize() + }; document.apply(transaction); assert!(document.node_at_path(&vec![0].into()).is_some()); @@ -20,17 +22,21 @@ fn test_documents() { let node_data = document.arena.get(node).unwrap().get(); assert_eq!(node_data.node_type, "text"); - let mut tb = TransactionBuilder::new(&document); - tb.update_attributes( - &vec![0].into(), - HashMap::from([("subtype".into(), Some("bullet-list".into()))]), - ); - let transaction = tb.finalize(); + let transaction = { + let mut tb = TransactionBuilder::new(&document); + tb.update_attributes_at_path( + &vec![0].into(), + HashMap::from([("subtype".into(), Some("bullet-list".into()))]), + ); + tb.finalize() + }; document.apply(transaction); - let mut tb = TransactionBuilder::new(&document); - tb.delete_node(&vec![0].into()); - let transaction = tb.finalize(); + let transaction = { + let mut tb = TransactionBuilder::new(&document); + tb.delete_node_at_path(&vec![0].into()); + tb.finalize() + }; document.apply(transaction); assert!(document.node_at_path(&vec![0].into()).is_none()); } @@ -40,16 +46,16 @@ fn test_inserts_nodes() { let mut document = DocumentTree::new(); let transaction = { let mut tb = TransactionBuilder::new(&document); - tb.insert_nodes(&vec![0].into(), &vec![NodeData::new("text")]); - tb.insert_nodes(&vec![1].into(), &vec![NodeData::new("text")]); - tb.insert_nodes(&vec![2].into(), &vec![NodeData::new("text")]); + tb.insert_nodes_at_path(&vec![0].into(), &vec![NodeData::new("text")]); + tb.insert_nodes_at_path(&vec![1].into(), &vec![NodeData::new("text")]); + tb.insert_nodes_at_path(&vec![2].into(), &vec![NodeData::new("text")]); tb.finalize() }; document.apply(transaction); let transaction = { let mut tb = TransactionBuilder::new(&document); - tb.insert_nodes(&vec![1].into(), &vec![NodeData::new("text")]); + tb.insert_nodes_at_path(&vec![1].into(), &vec![NodeData::new("text")]); tb.finalize() }; document.apply(transaction); @@ -60,18 +66,16 @@ fn test_update_nodes() { let mut document = DocumentTree::new(); let transaction = { let mut tb = TransactionBuilder::new(&document); - tb.insert_nodes(&vec![0].into(), &vec![NodeData::new("text")]); - tb.insert_nodes(&vec![1].into(), &vec![NodeData::new("text")]); - tb.insert_nodes(&vec![2].into(), &vec![NodeData::new("text")]); + tb.insert_nodes_at_path(&vec![0].into(), &vec![NodeData::new("text")]); + tb.insert_nodes_at_path(&vec![1].into(), &vec![NodeData::new("text")]); + tb.insert_nodes_at_path(&vec![2].into(), &vec![NodeData::new("text")]); tb.finalize() }; document.apply(transaction); let transaction = { let mut tb = TransactionBuilder::new(&document); - tb.update_attributes(&vec![1].into(), HashMap::from([ - ("bolded".into(), Some("true".into())), - ])); + tb.update_attributes_at_path(&vec![1].into(), HashMap::from([("bolded".into(), Some("true".into()))])); tb.finalize() }; document.apply(transaction); @@ -81,3 +85,26 @@ fn test_update_nodes() { let is_bold = node_data.attributes.borrow().0.get("bolded").unwrap().clone(); assert_eq!(is_bold.unwrap(), "true"); } + +#[test] +fn test_delete_nodes() { + let mut document = DocumentTree::new(); + let transaction = { + let mut tb = TransactionBuilder::new(&document); + tb.insert_nodes_at_path(&vec![0].into(), &vec![NodeData::new("text")]); + tb.insert_nodes_at_path(&vec![1].into(), &vec![NodeData::new("text")]); + tb.insert_nodes_at_path(&vec![2].into(), &vec![NodeData::new("text")]); + tb.finalize() + }; + document.apply(transaction); + + let transaction = { + let mut tb = TransactionBuilder::new(&document); + tb.delete_node_at_path(&Position(vec![1])); + tb.finalize() + }; + document.apply(transaction); + + let len = document.root.children(&document.arena).fold(0, |count, _| count + 1); + assert_eq!(len, 2); +} From b0bafff22c92c5016e99f90cef32826dd7b04c89 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 22 Aug 2022 16:46:24 +0800 Subject: [PATCH 33/86] feat: introduce error to apply method --- frontend/rust-lib/Cargo.lock | 7 + .../lib-ot/src/core/document/document.rs | 120 +++++++++++----- .../src/core/document/document_operation.rs | 136 +++++++++--------- shared-lib/lib-ot/src/core/document/node.rs | 9 +- .../lib-ot/src/core/document/transaction.rs | 18 ++- shared-lib/lib-ot/src/errors.rs | 3 +- shared-lib/lib-ot/tests/main.rs | 34 +++-- 7 files changed, 193 insertions(+), 134 deletions(-) diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 7534492ab0..4119cc1edf 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -1618,6 +1618,12 @@ dependencies = [ "serde", ] +[[package]] +name = "indextree" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b4b46b3311ebd8e5cd44f6b03b36e0f48a70552cf6b036afcebc5626794066" + [[package]] name = "instant" version = "0.1.12" @@ -1766,6 +1772,7 @@ dependencies = [ "bytes", "dashmap", "derive_more", + "indextree", "lazy_static", "log", "md5", diff --git a/shared-lib/lib-ot/src/core/document/document.rs b/shared-lib/lib-ot/src/core/document/document.rs index 07626e1d75..063fe408b0 100644 --- a/shared-lib/lib-ot/src/core/document/document.rs +++ b/shared-lib/lib-ot/src/core/document/document.rs @@ -1,8 +1,6 @@ use crate::core::document::position::Position; -use crate::core::{ - DeleteOperation, DocumentOperation, InsertOperation, NodeAttributes, NodeData, TextEditOperation, Transaction, - UpdateOperation, -}; +use crate::core::{DocumentOperation, NodeAttributes, NodeData, OperationTransform, TextDelta, Transaction}; +use crate::errors::{ErrorBuilder, OTError, OTErrorCode}; use indextree::{Arena, NodeId}; pub struct DocumentTree { @@ -86,42 +84,48 @@ impl DocumentTree { None } - pub fn apply(&mut self, transaction: Transaction) { + pub fn apply(&mut self, transaction: Transaction) -> Result<(), OTError> { for op in &transaction.operations { - self.apply_op(op); + self.apply_op(op)?; } + Ok(()) } - fn apply_op(&mut self, op: &DocumentOperation) { + fn apply_op(&mut self, op: &DocumentOperation) -> Result<(), OTError> { match op { - DocumentOperation::Insert(op) => self.apply_insert(op), - DocumentOperation::Update(op) => self.apply_update(op), - DocumentOperation::Delete(op) => self.apply_delete(op), - DocumentOperation::TextEdit(op) => self.apply_text_edit(op), + DocumentOperation::Insert { path, nodes } => self.apply_insert(path, nodes), + DocumentOperation::Update { path, attributes, .. } => self.apply_update(path, attributes), + DocumentOperation::Delete { path, nodes } => self.apply_delete(path, nodes.len()), + DocumentOperation::TextEdit { path, delta, .. } => self.apply_text_edit(path, delta), } } - fn apply_insert(&mut self, op: &InsertOperation) { - let parent_path = &op.path.0[0..(op.path.0.len() - 1)]; - let last_index = op.path.0[op.path.0.len() - 1]; - let parent_node = self.node_at_path(&Position(parent_path.to_vec())); - if let Some(parent_node) = parent_node { - let mut inserted_nodes = Vec::new(); + fn apply_insert(&mut self, path: &Position, nodes: &Vec) -> Result<(), OTError> { + let parent_path = &path.0[0..(path.0.len() - 1)]; + let last_index = path.0[path.0.len() - 1]; + let parent_node = self + .node_at_path(&Position(parent_path.to_vec())) + .ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; + let mut inserted_nodes = Vec::new(); - for node in &op.nodes { - inserted_nodes.push(self.arena.new_node(node.clone())); - } - - self.insert_child_at_index(parent_node, last_index, &inserted_nodes); + for node in nodes { + inserted_nodes.push(self.arena.new_node(node.clone())); } + + self.insert_child_at_index(parent_node, last_index, &inserted_nodes) } - fn insert_child_at_index(&mut self, parent: NodeId, index: usize, insert_children: &[NodeId]) { + fn insert_child_at_index( + &mut self, + parent: NodeId, + index: usize, + insert_children: &[NodeId], + ) -> Result<(), OTError> { if index == 0 && parent.children(&self.arena).next().is_none() { for id in insert_children { parent.append(*id, &mut self.arena); } - return; + return Ok(()); } let children_length = parent.children(&self.arena).fold(0, |counter, _| counter + 1); @@ -130,32 +134,72 @@ impl DocumentTree { for id in insert_children { parent.append(*id, &mut self.arena); } - return; + return Ok(()); } - let node_to_insert = self.child_at_index_of_path(parent, index).unwrap(); + let node_to_insert = self + .child_at_index_of_path(parent, index) + .ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; for id in insert_children { node_to_insert.insert_before(*id, &mut self.arena); } + Ok(()) } - fn apply_update(&self, op: &UpdateOperation) { - let update_node = self.node_at_path(&op.path).unwrap(); - let node_data = self.arena.get(update_node).unwrap(); - let new_attributes = { - let old_attributes = node_data.get().attributes.borrow(); - NodeAttributes::compose(&old_attributes, &op.attributes) + fn apply_update(&mut self, path: &Position, attributes: &NodeAttributes) -> Result<(), OTError> { + let update_node = self + .node_at_path(path) + .ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; + let node_data = self.arena.get_mut(update_node).unwrap(); + // let new_node = NodeData { + // ..node_data.get().clone() + // attributes: + // }; + let new_node = { + let old_attributes = &node_data.get().attributes; + let new_attributes = NodeAttributes::compose(&old_attributes, attributes); + NodeData { + attributes: new_attributes, + ..node_data.get().clone() + } }; - node_data.get().attributes.replace(new_attributes); + *node_data.get_mut() = new_node; + Ok(()) } - fn apply_delete(&mut self, op: &DeleteOperation) { - let update_node = self.node_at_path(&op.path).unwrap(); - update_node.remove_subtree(&mut self.arena); + fn apply_delete(&mut self, path: &Position, len: usize) -> Result<(), OTError> { + let mut update_node = self + .node_at_path(path) + .ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; + for _ in 0..len { + let next = update_node.following_siblings(&self.arena).next(); + update_node.remove_subtree(&mut self.arena); + if let Some(next_id) = next { + update_node = next_id; + } else { + break; + } + } + Ok(()) } - fn apply_text_edit(&self, _op: &TextEditOperation) { - unimplemented!() + fn apply_text_edit(&mut self, path: &Position, delta: &TextDelta) -> Result<(), OTError> { + let edit_node = self + .node_at_path(path) + .ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; + let node_data = self.arena.get_mut(edit_node).unwrap(); + let new_delta = if let Some(old_delta) = &node_data.get().delta { + Some(old_delta.compose(delta)?) + } else { + None + }; + if let Some(new_delta) = new_delta { + *node_data.get_mut() = NodeData { + delta: Some(new_delta), + ..node_data.get().clone() + }; + }; + Ok(()) } } diff --git a/shared-lib/lib-ot/src/core/document/document_operation.rs b/shared-lib/lib-ot/src/core/document/document_operation.rs index 5a19f45a75..0ce16ae89c 100644 --- a/shared-lib/lib-ot/src/core/document/document_operation.rs +++ b/shared-lib/lib-ot/src/core/document/document_operation.rs @@ -3,102 +3,98 @@ use crate::core::{NodeAttributes, NodeData, TextDelta}; #[derive(Clone)] pub enum DocumentOperation { - Insert(InsertOperation), - Update(UpdateOperation), - Delete(DeleteOperation), - TextEdit(TextEditOperation), + Insert { + path: Position, + nodes: Vec, + }, + Update { + path: Position, + attributes: NodeAttributes, + old_attributes: NodeAttributes, + }, + Delete { + path: Position, + nodes: Vec, + }, + TextEdit { + path: Position, + delta: TextDelta, + inverted: TextDelta, + }, } impl DocumentOperation { pub fn path(&self) -> &Position { match self { - DocumentOperation::Insert(insert) => &insert.path, - DocumentOperation::Update(update) => &update.path, - DocumentOperation::Delete(delete) => &delete.path, - DocumentOperation::TextEdit(text_edit) => &text_edit.path, + DocumentOperation::Insert { path, .. } => path, + DocumentOperation::Update { path, .. } => path, + DocumentOperation::Delete { path, .. } => path, + DocumentOperation::TextEdit { path, .. } => path, } } pub fn invert(&self) -> DocumentOperation { match self { - DocumentOperation::Insert(insert_operation) => DocumentOperation::Delete(DeleteOperation { - path: insert_operation.path.clone(), - nodes: insert_operation.nodes.clone(), - }), - DocumentOperation::Update(update_operation) => DocumentOperation::Update(UpdateOperation { - path: update_operation.path.clone(), - attributes: update_operation.old_attributes.clone(), - old_attributes: update_operation.attributes.clone(), - }), - DocumentOperation::Delete(delete_operation) => DocumentOperation::Insert(InsertOperation { - path: delete_operation.path.clone(), - nodes: delete_operation.nodes.clone(), - }), - DocumentOperation::TextEdit(text_edit_operation) => DocumentOperation::TextEdit(TextEditOperation { - path: text_edit_operation.path.clone(), - delta: text_edit_operation.inverted.clone(), - inverted: text_edit_operation.delta.clone(), - }), + DocumentOperation::Insert { path, nodes } => DocumentOperation::Delete { + path: path.clone(), + nodes: nodes.clone(), + }, + DocumentOperation::Update { + path, + attributes, + old_attributes, + } => DocumentOperation::Update { + path: path.clone(), + attributes: old_attributes.clone(), + old_attributes: attributes.clone(), + }, + DocumentOperation::Delete { path, nodes } => DocumentOperation::Insert { + path: path.clone(), + nodes: nodes.clone(), + }, + DocumentOperation::TextEdit { path, delta, inverted } => DocumentOperation::TextEdit { + path: path.clone(), + delta: inverted.clone(), + inverted: delta.clone(), + }, } } pub fn clone_with_new_path(&self, path: Position) -> DocumentOperation { match self { - DocumentOperation::Insert(insert) => DocumentOperation::Insert(InsertOperation { + DocumentOperation::Insert { nodes, .. } => DocumentOperation::Insert { path, - nodes: insert.nodes.clone(), - }), - DocumentOperation::Update(update) => DocumentOperation::Update(UpdateOperation { + nodes: nodes.clone(), + }, + DocumentOperation::Update { + attributes, + old_attributes, + .. + } => DocumentOperation::Update { path, - attributes: update.attributes.clone(), - old_attributes: update.old_attributes.clone(), - }), - DocumentOperation::Delete(delete) => DocumentOperation::Delete(DeleteOperation { + attributes: attributes.clone(), + old_attributes: old_attributes.clone(), + }, + DocumentOperation::Delete { nodes, .. } => DocumentOperation::Delete { path, - nodes: delete.nodes.clone(), - }), - DocumentOperation::TextEdit(text_edit) => DocumentOperation::TextEdit(TextEditOperation { + nodes: nodes.clone(), + }, + DocumentOperation::TextEdit { delta, inverted, .. } => DocumentOperation::TextEdit { path, - delta: text_edit.delta.clone(), - inverted: text_edit.inverted.clone(), - }), + delta: delta.clone(), + inverted: inverted.clone(), + }, } } pub fn transform(a: &DocumentOperation, b: &DocumentOperation) -> DocumentOperation { match a { - DocumentOperation::Insert(insert_op) => { - let new_path = Position::transform(a.path(), b.path(), insert_op.nodes.len() as i64); + DocumentOperation::Insert { path: a_path, nodes } => { + let new_path = Position::transform(a_path, b.path(), nodes.len() as i64); b.clone_with_new_path(new_path) } - DocumentOperation::Delete(delete_op) => { - let new_path = Position::transform(a.path(), b.path(), delete_op.nodes.len() as i64); + DocumentOperation::Delete { path: a_path, nodes } => { + let new_path = Position::transform(a_path, b.path(), nodes.len() as i64); b.clone_with_new_path(new_path) } _ => b.clone(), } } } - -#[derive(Clone)] -pub struct InsertOperation { - pub path: Position, - pub nodes: Vec, -} - -#[derive(Clone)] -pub struct UpdateOperation { - pub path: Position, - pub attributes: NodeAttributes, - pub old_attributes: NodeAttributes, -} - -#[derive(Clone)] -pub struct DeleteOperation { - pub path: Position, - pub nodes: Vec, -} - -#[derive(Clone)] -pub struct TextEditOperation { - pub path: Position, - pub delta: TextDelta, - pub inverted: TextDelta, -} diff --git a/shared-lib/lib-ot/src/core/document/node.rs b/shared-lib/lib-ot/src/core/document/node.rs index a3ec5e729a..e7c5e0e68f 100644 --- a/shared-lib/lib-ot/src/core/document/node.rs +++ b/shared-lib/lib-ot/src/core/document/node.rs @@ -1,19 +1,18 @@ use crate::core::{NodeAttributes, TextDelta}; -use std::cell::RefCell; #[derive(Clone)] pub struct NodeData { pub node_type: String, - pub attributes: RefCell, - pub delta: RefCell>, + pub attributes: NodeAttributes, + pub delta: Option, } impl NodeData { pub fn new(node_type: &str) -> NodeData { NodeData { node_type: node_type.into(), - attributes: RefCell::new(NodeAttributes::new()), - delta: RefCell::new(None), + attributes: NodeAttributes::new(), + delta: None, } } } diff --git a/shared-lib/lib-ot/src/core/document/transaction.rs b/shared-lib/lib-ot/src/core/document/transaction.rs index 99d5ee8169..00697b7da1 100644 --- a/shared-lib/lib-ot/src/core/document/transaction.rs +++ b/shared-lib/lib-ot/src/core/document/transaction.rs @@ -1,7 +1,5 @@ use crate::core::document::position::Position; -use crate::core::{ - DeleteOperation, DocumentOperation, DocumentTree, InsertOperation, NodeAttributes, NodeData, UpdateOperation, -}; +use crate::core::{DocumentOperation, DocumentTree, NodeAttributes, NodeData}; use std::collections::HashMap; pub struct Transaction { @@ -28,10 +26,10 @@ impl<'a> TransactionBuilder<'a> { } pub fn insert_nodes_at_path(&mut self, path: &Position, nodes: &[NodeData]) { - self.push(DocumentOperation::Insert(InsertOperation { + self.push(DocumentOperation::Insert { path: path.clone(), nodes: nodes.to_vec(), - })); + }); } pub fn update_attributes_at_path(&mut self, path: &Position, attributes: HashMap>) { @@ -40,7 +38,7 @@ impl<'a> TransactionBuilder<'a> { let node_data = self.document.arena.get(node).unwrap().get(); for key in attributes.keys() { - let old_attrs = node_data.attributes.borrow(); + let old_attrs = &node_data.attributes; let old_value = match old_attrs.0.get(key.as_str()) { Some(value) => value.clone(), None => None, @@ -48,11 +46,11 @@ impl<'a> TransactionBuilder<'a> { old_attributes.insert(key.clone(), old_value); } - self.push(DocumentOperation::Update(UpdateOperation { + self.push(DocumentOperation::Update { path: path.clone(), attributes: NodeAttributes(attributes), old_attributes: NodeAttributes(old_attributes), - })) + }) } pub fn delete_node_at_path(&mut self, path: &Position) { @@ -69,10 +67,10 @@ impl<'a> TransactionBuilder<'a> { node = node.following_siblings(&self.document.arena).next().unwrap(); } - self.operations.push(DocumentOperation::Delete(DeleteOperation { + self.operations.push(DocumentOperation::Delete { path: path.clone(), nodes: deleted_nodes, - })) + }) } pub fn push(&mut self, op: DocumentOperation) { diff --git a/shared-lib/lib-ot/src/errors.rs b/shared-lib/lib-ot/src/errors.rs index e7aea28bcf..eb313c784f 100644 --- a/shared-lib/lib-ot/src/errors.rs +++ b/shared-lib/lib-ot/src/errors.rs @@ -60,7 +60,7 @@ impl std::convert::From for OTError { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Eq, PartialEq)] pub enum OTErrorCode { IncompatibleLength, ApplyInsertFail, @@ -74,6 +74,7 @@ pub enum OTErrorCode { DuplicatedRevision, RevisionIDConflict, Internal, + PathNotFound, } pub struct ErrorBuilder { diff --git a/shared-lib/lib-ot/tests/main.rs b/shared-lib/lib-ot/tests/main.rs index 1595c20f29..7daf74e957 100644 --- a/shared-lib/lib-ot/tests/main.rs +++ b/shared-lib/lib-ot/tests/main.rs @@ -1,4 +1,5 @@ use lib_ot::core::{DocumentTree, NodeData, Position, TransactionBuilder}; +use lib_ot::errors::OTErrorCode; use std::collections::HashMap; #[test] @@ -15,7 +16,7 @@ fn test_documents() { tb.insert_nodes_at_path(&vec![0].into(), &vec![NodeData::new("text")]); tb.finalize() }; - document.apply(transaction); + document.apply(transaction).unwrap(); assert!(document.node_at_path(&vec![0].into()).is_some()); let node = document.node_at_path(&vec![0].into()).unwrap(); @@ -30,14 +31,14 @@ fn test_documents() { ); tb.finalize() }; - document.apply(transaction); + document.apply(transaction).unwrap(); let transaction = { let mut tb = TransactionBuilder::new(&document); tb.delete_node_at_path(&vec![0].into()); tb.finalize() }; - document.apply(transaction); + document.apply(transaction).unwrap(); assert!(document.node_at_path(&vec![0].into()).is_none()); } @@ -51,14 +52,14 @@ fn test_inserts_nodes() { tb.insert_nodes_at_path(&vec![2].into(), &vec![NodeData::new("text")]); tb.finalize() }; - document.apply(transaction); + document.apply(transaction).unwrap(); let transaction = { let mut tb = TransactionBuilder::new(&document); tb.insert_nodes_at_path(&vec![1].into(), &vec![NodeData::new("text")]); tb.finalize() }; - document.apply(transaction); + document.apply(transaction).unwrap(); } #[test] @@ -71,18 +72,18 @@ fn test_update_nodes() { tb.insert_nodes_at_path(&vec![2].into(), &vec![NodeData::new("text")]); tb.finalize() }; - document.apply(transaction); + document.apply(transaction).unwrap(); let transaction = { let mut tb = TransactionBuilder::new(&document); tb.update_attributes_at_path(&vec![1].into(), HashMap::from([("bolded".into(), Some("true".into()))])); tb.finalize() }; - document.apply(transaction); + document.apply(transaction).unwrap(); let node = document.node_at_path(&Position(vec![1])).unwrap(); let node_data = document.arena.get(node).unwrap().get(); - let is_bold = node_data.attributes.borrow().0.get("bolded").unwrap().clone(); + let is_bold = node_data.attributes.0.get("bolded").unwrap().clone(); assert_eq!(is_bold.unwrap(), "true"); } @@ -96,15 +97,28 @@ fn test_delete_nodes() { tb.insert_nodes_at_path(&vec![2].into(), &vec![NodeData::new("text")]); tb.finalize() }; - document.apply(transaction); + document.apply(transaction).unwrap(); let transaction = { let mut tb = TransactionBuilder::new(&document); tb.delete_node_at_path(&Position(vec![1])); tb.finalize() }; - document.apply(transaction); + document.apply(transaction).unwrap(); let len = document.root.children(&document.arena).fold(0, |count, _| count + 1); assert_eq!(len, 2); } + +#[test] +fn test_errors() { + let mut document = DocumentTree::new(); + let transaction = { + let mut tb = TransactionBuilder::new(&document); + tb.insert_nodes_at_path(&vec![0].into(), &vec![NodeData::new("text")]); + tb.insert_nodes_at_path(&vec![100].into(), &vec![NodeData::new("text")]); + tb.finalize() + }; + let result = document.apply(transaction); + assert_eq!(result.err().unwrap().code, OTErrorCode::PathNotFound); +} From ef65551340a77f1504a8f542566e29eeed81f45a Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 22 Aug 2022 18:33:02 +0800 Subject: [PATCH 34/86] feat: add serialize/deserialize --- .../lib-ot/src/core/document/attributes.rs | 2 +- .../src/core/document/document_operation.rs | 55 ++++++++++++++++++- shared-lib/lib-ot/src/core/document/node.rs | 2 +- .../lib-ot/src/core/document/position.rs | 4 +- 4 files changed, 58 insertions(+), 5 deletions(-) diff --git a/shared-lib/lib-ot/src/core/document/attributes.rs b/shared-lib/lib-ot/src/core/document/attributes.rs index b682fd8404..52b33dce08 100644 --- a/shared-lib/lib-ot/src/core/document/attributes.rs +++ b/shared-lib/lib-ot/src/core/document/attributes.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -#[derive(Clone)] +#[derive(Clone, serde::Serialize, serde::Deserialize)] pub struct NodeAttributes(pub HashMap>); impl NodeAttributes { diff --git a/shared-lib/lib-ot/src/core/document/document_operation.rs b/shared-lib/lib-ot/src/core/document/document_operation.rs index 0ce16ae89c..46cb79be4a 100644 --- a/shared-lib/lib-ot/src/core/document/document_operation.rs +++ b/shared-lib/lib-ot/src/core/document/document_operation.rs @@ -1,7 +1,7 @@ use crate::core::document::position::Position; use crate::core::{NodeAttributes, NodeData, TextDelta}; -#[derive(Clone)] +#[derive(Clone, serde::Serialize, serde::Deserialize)] pub enum DocumentOperation { Insert { path: Position, @@ -98,3 +98,56 @@ impl DocumentOperation { } } } + +#[cfg(test)] +mod tests { + use crate::core::Position; + + #[test] + fn test_transform_path_1() { + assert_eq!( + { Position::transform(&Position(vec![0, 1]), &Position(vec![0, 1]), 1) }.0, + vec![0, 2] + ); + } + + #[test] + fn test_transform_path_2() { + assert_eq!( + { Position::transform(&Position(vec![0, 1]), &Position(vec![0, 2]), 1) }.0, + vec![0, 3] + ); + } + + #[test] + fn test_transform_path_3() { + assert_eq!( + { Position::transform(&Position(vec![0, 1]), &Position(vec![0, 2, 7, 8, 9]), 1) }.0, + vec![0, 3, 7, 8, 9] + ); + } + + #[test] + fn test_transform_path_not_changed() { + assert_eq!( + { Position::transform(&Position(vec![0, 1, 2]), &Position(vec![0, 0, 7, 8, 9]), 1) }.0, + vec![0, 0, 7, 8, 9] + ); + assert_eq!( + { Position::transform(&Position(vec![0, 1, 2]), &Position(vec![0, 1]), 1) }.0, + vec![0, 1] + ); + assert_eq!( + { Position::transform(&Position(vec![1, 1]), &Position(vec![1, 0]), 1) }.0, + vec![1, 0] + ); + } + + #[test] + fn test_transform_delta() { + assert_eq!( + { Position::transform(&Position(vec![0, 1]), &Position(vec![0, 1]), 5) }.0, + vec![0, 6] + ); + } +} diff --git a/shared-lib/lib-ot/src/core/document/node.rs b/shared-lib/lib-ot/src/core/document/node.rs index e7c5e0e68f..96747b0fd8 100644 --- a/shared-lib/lib-ot/src/core/document/node.rs +++ b/shared-lib/lib-ot/src/core/document/node.rs @@ -1,6 +1,6 @@ use crate::core::{NodeAttributes, TextDelta}; -#[derive(Clone)] +#[derive(Clone, serde::Serialize, serde::Deserialize)] pub struct NodeData { pub node_type: String, pub attributes: NodeAttributes, diff --git a/shared-lib/lib-ot/src/core/document/position.rs b/shared-lib/lib-ot/src/core/document/position.rs index 76f6a0eec4..521d0e9a60 100644 --- a/shared-lib/lib-ot/src/core/document/position.rs +++ b/shared-lib/lib-ot/src/core/document/position.rs @@ -1,4 +1,4 @@ -#[derive(Clone)] +#[derive(Clone, serde::Serialize, serde::Deserialize)] pub struct Position(pub Vec); impl Position { @@ -20,7 +20,7 @@ impl Position { return b.clone(); } // check the prefix - for i in 0..(pre_insert_path.len()) { + for i in 0..(pre_insert_path.len() - 1) { if pre_insert_path.0[i] != b.0[i] { return b.clone(); } From 70ea80878adea6c15e94c973d7155a1edd0cbf7a Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 22 Aug 2022 20:38:24 +0800 Subject: [PATCH 35/86] feat: single tap to edit link and double tap to open the link --- .../render/rich_text/bulleted_list_text.dart | 5 +- .../src/render/rich_text/checkbox_text.dart | 6 +- .../src/render/rich_text/flowy_rich_text.dart | 97 ++++++++++++------- .../render/rich_text/number_list_text.dart | 4 +- .../lib/src/render/rich_text/quoted_text.dart | 4 +- .../src/render/rich_text/rich_text_style.dart | 24 +---- .../lib/src/render/toolbar/toolbar_item.dart | 33 ++++++- .../src/render/toolbar/toolbar_widget.dart | 5 +- .../lib/src/service/toolbar_service.dart | 16 ++- 9 files changed, 115 insertions(+), 79 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart index 267a5acc66..7d69ff459f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart @@ -56,8 +56,6 @@ class _BulletedListTextNodeWidgetState extends State @override Widget build(BuildContext context) { - final topPadding = RichTextStyle.fromTextNode(widget.textNode).topPadding; - return SizedBox( width: defaultMaxTextNodeWidth, child: Padding( @@ -69,8 +67,7 @@ class _BulletedListTextNodeWidgetState extends State key: iconKey, width: _iconWidth, height: _iconWidth, - padding: - EdgeInsets.only(top: topPadding, right: _iconRightPadding), + padding: EdgeInsets.only(right: _iconRightPadding), name: 'point', ), Expanded( diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart index 9b7d3a730f..0255a84049 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart @@ -63,7 +63,6 @@ class _CheckboxNodeWidgetState extends State Widget _buildWithSingle(BuildContext context) { final check = widget.textNode.attributes.check; - final topPadding = RichTextStyle.fromTextNode(widget.textNode).topPadding; return SizedBox( width: defaultMaxTextNodeWidth, child: Padding( @@ -76,10 +75,7 @@ class _CheckboxNodeWidgetState extends State child: FlowySvg( width: _iconWidth, height: _iconWidth, - padding: EdgeInsets.only( - top: topPadding, - right: _iconRightPadding, - ), + padding: EdgeInsets.only(right: _iconRightPadding), name: check ? 'check' : 'uncheck', ), onTap: () { diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart index 89df8a54b1..39f484c23f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart @@ -1,5 +1,7 @@ +import 'dart:async'; import 'dart:ui'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -11,6 +13,7 @@ import 'package:appflowy_editor/src/document/text_delta.dart'; import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; import 'package:appflowy_editor/src/render/selection/selectable.dart'; +import 'package:url_launcher/url_launcher_string.dart'; typedef FlowyTextSpanDecorator = TextSpan Function(TextSpan textSpan); @@ -143,6 +146,11 @@ class _FlowyRichTextState extends State with Selectable { ); } + @override + Offset localToGlobal(Offset offset) { + return _renderParagraph.localToGlobal(offset); + } + Widget _buildRichText(BuildContext context) { return MouseRegion( cursor: SystemMouseCursors.text, @@ -181,44 +189,63 @@ class _FlowyRichTextState extends State with Selectable { ); } - // unused now. - // Widget _buildRichTextWithChildren(BuildContext context) { - // return Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // _buildSingleRichText(context), - // ...widget.textNode.children - // .map( - // (child) => widget.editorState.service.renderPluginService - // .buildPluginWidget( - // NodeWidgetContext( - // context: context, - // node: child, - // editorState: widget.editorState, - // ), - // ), - // ) - // .toList() - // ], - // ); - // } + TextSpan get _textSpan { + var offset = 0; + return TextSpan( + children: widget.textNode.delta.whereType().map((insert) { + GestureRecognizer? gestureDetector; + if (insert.attributes?[StyleKey.href] != null) { + final startOffset = offset; + Timer? timer; + var tapCount = 0; + gestureDetector = TapGestureRecognizer() + ..onTap = () async { + // implement a simple double tap logic + tapCount += 1; + timer?.cancel(); - @override - Offset localToGlobal(Offset offset) { - return _renderParagraph.localToGlobal(offset); + if (tapCount == 2) { + tapCount = 0; + final href = insert.attributes![StyleKey.href]; + final uri = Uri.parse(href); + // url_launcher cannot open a link without scheme. + final newHref = + (uri.scheme.isNotEmpty ? href : 'http://$href').trim(); + if (await canLaunchUrlString(newHref)) { + await launchUrlString(newHref); + } + return; + } + + timer = Timer(const Duration(milliseconds: 200), () { + tapCount = 0; + // update selection + final selection = Selection.single( + path: widget.textNode.path, + startOffset: startOffset, + endOffset: startOffset + insert.length, + ); + widget.editorState.service.selectionService + .updateSelection(selection); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + widget.editorState.service.toolbarService + ?.triggerHandler('appflowy.toolbar.link'); + }); + }); + }; + } + offset += insert.length; + final textSpan = RichTextStyle( + attributes: insert.attributes ?? {}, + text: insert.content, + height: _lineHeight, + gestureRecognizer: gestureDetector, + ).toTextSpan(); + return textSpan; + }).toList(growable: false), + ); } - TextSpan get _textSpan => TextSpan( - children: widget.textNode.delta - .whereType() - .map((insert) => RichTextStyle( - attributes: insert.attributes ?? {}, - text: insert.content, - height: _lineHeight, - ).toTextSpan()) - .toList(growable: false), - ); - TextSpan get _placeholderTextSpan => TextSpan(children: [ RichTextStyle( text: widget.placeholderText, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart index de3b0b55b6..c1062e1c3c 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart @@ -56,7 +56,6 @@ class _NumberListTextNodeWidgetState extends State @override Widget build(BuildContext context) { - final topPadding = RichTextStyle.fromTextNode(widget.textNode).topPadding; return Padding( padding: EdgeInsets.only(bottom: defaultLinePadding), child: SizedBox( @@ -68,8 +67,7 @@ class _NumberListTextNodeWidgetState extends State key: iconKey, width: _iconWidth, height: _iconWidth, - padding: - EdgeInsets.only(top: topPadding, right: _iconRightPadding), + padding: EdgeInsets.only(right: _iconRightPadding), number: widget.textNode.attributes.number, ), Expanded( diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart index 0389dfa50f..3391729779 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart @@ -55,7 +55,6 @@ class _QuotedTextNodeWidgetState extends State @override Widget build(BuildContext context) { - final topPadding = RichTextStyle.fromTextNode(widget.textNode).topPadding; return SizedBox( width: defaultMaxTextNodeWidth, child: Padding( @@ -67,8 +66,7 @@ class _QuotedTextNodeWidgetState extends State FlowySvg( key: iconKey, width: _iconWidth, - padding: EdgeInsets.only( - top: topPadding, right: _iconRightPadding), + padding: EdgeInsets.only(right: _iconRightPadding), name: 'quote', ), Expanded( diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart index 6bc50c5115..efcdd3790f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart @@ -1,8 +1,6 @@ import 'package:appflowy_editor/src/document/attributes.dart'; -import 'package:appflowy_editor/src/document/node.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:url_launcher/url_launcher_string.dart'; /// /// Supported partial rendering types: @@ -182,14 +180,13 @@ class RichTextStyle { RichTextStyle({ required this.attributes, required this.text, + this.gestureRecognizer, this.height = 1.5, }); - RichTextStyle.fromTextNode(TextNode textNode) - : this(attributes: textNode.attributes, text: textNode.toRawString()); - final Attributes attributes; final String text; + final GestureRecognizer? gestureRecognizer; final double height; TextSpan toTextSpan() => _toTextSpan(height); @@ -201,6 +198,7 @@ class RichTextStyle { TextSpan _toTextSpan(double? height) { return TextSpan( text: text, + recognizer: _recognizer, style: TextStyle( fontWeight: _fontWeight, fontStyle: _fontStyle, @@ -210,7 +208,6 @@ class RichTextStyle { background: _background, height: height, ), - recognizer: _recognizer, ); } @@ -273,19 +270,6 @@ class RichTextStyle { // recognizer GestureRecognizer? get _recognizer { - final href = attributes.href; - if (href != null) { - return TapGestureRecognizer() - ..onTap = () async { - final uri = Uri.parse(href); - // url_launcher cannot open a link without scheme. - final newHref = - (uri.scheme.isNotEmpty ? href : 'http://$href').trim(); - if (await canLaunchUrlString(newHref)) { - await launchUrlString(newHref); - } - }; - } - return null; + return gestureRecognizer; } } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart index a068722cb2..b2f6a9b2f7 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart @@ -14,6 +14,7 @@ typedef ToolbarShowValidator = bool Function(EditorState editorState); class ToolbarItem { ToolbarItem({ required this.id, + required this.type, required this.icon, this.tooltipsMessage = '', required this.validator, @@ -21,6 +22,7 @@ class ToolbarItem { }); final String id; + final int type; final Widget icon; final String tooltipsMessage; final ToolbarShowValidator validator; @@ -29,16 +31,32 @@ class ToolbarItem { factory ToolbarItem.divider() { return ToolbarItem( id: 'divider', + type: -1, icon: const FlowySvg(name: 'toolbar/divider'), validator: (editorState) => true, handler: (editorState, context) {}, ); } + + @override + bool operator ==(Object other) { + if (other is! ToolbarItem) { + return false; + } + if (identical(this, other)) { + return true; + } + return id == other.id; + } + + @override + int get hashCode => id.hashCode; } List defaultToolbarItems = [ ToolbarItem( id: 'appflowy.toolbar.h1', + type: 1, tooltipsMessage: 'Heading 1', icon: const FlowySvg(name: 'toolbar/h1'), validator: _onlyShowInSingleTextSelection, @@ -46,6 +64,7 @@ List defaultToolbarItems = [ ), ToolbarItem( id: 'appflowy.toolbar.h2', + type: 1, tooltipsMessage: 'Heading 2', icon: const FlowySvg(name: 'toolbar/h2'), validator: _onlyShowInSingleTextSelection, @@ -53,14 +72,15 @@ List defaultToolbarItems = [ ), ToolbarItem( id: 'appflowy.toolbar.h3', + type: 1, tooltipsMessage: 'Heading 3', icon: const FlowySvg(name: 'toolbar/h3'), validator: _onlyShowInSingleTextSelection, handler: (editorState, context) => formatHeading(editorState, StyleKey.h3), ), - ToolbarItem.divider(), ToolbarItem( id: 'appflowy.toolbar.bold', + type: 2, tooltipsMessage: 'Bold', icon: const FlowySvg(name: 'toolbar/bold'), validator: _showInTextSelection, @@ -68,6 +88,7 @@ List defaultToolbarItems = [ ), ToolbarItem( id: 'appflowy.toolbar.italic', + type: 2, tooltipsMessage: 'Italic', icon: const FlowySvg(name: 'toolbar/italic'), validator: _showInTextSelection, @@ -75,6 +96,7 @@ List defaultToolbarItems = [ ), ToolbarItem( id: 'appflowy.toolbar.underline', + type: 2, tooltipsMessage: 'Underline', icon: const FlowySvg(name: 'toolbar/underline'), validator: _showInTextSelection, @@ -82,14 +104,15 @@ List defaultToolbarItems = [ ), ToolbarItem( id: 'appflowy.toolbar.strikethrough', + type: 2, tooltipsMessage: 'Strikethrough', icon: const FlowySvg(name: 'toolbar/strikethrough'), validator: _showInTextSelection, handler: (editorState, context) => formatStrikethrough(editorState), ), - ToolbarItem.divider(), ToolbarItem( id: 'appflowy.toolbar.quote', + type: 3, tooltipsMessage: 'Quote', icon: const FlowySvg(name: 'toolbar/quote'), validator: _onlyShowInSingleTextSelection, @@ -97,14 +120,15 @@ List defaultToolbarItems = [ ), ToolbarItem( id: 'appflowy.toolbar.bulleted_list', + type: 3, tooltipsMessage: 'Bulleted list', icon: const FlowySvg(name: 'toolbar/bulleted_list'), validator: _onlyShowInSingleTextSelection, handler: (editorState, context) => formatBulletedList(editorState), ), - ToolbarItem.divider(), ToolbarItem( id: 'appflowy.toolbar.link', + type: 4, tooltipsMessage: 'Link', icon: const FlowySvg(name: 'toolbar/link'), validator: _onlyShowInSingleTextSelection, @@ -112,6 +136,7 @@ List defaultToolbarItems = [ ), ToolbarItem( id: 'appflowy.toolbar.highlight', + type: 4, tooltipsMessage: 'Highlight', icon: const FlowySvg(name: 'toolbar/highlight'), validator: _showInTextSelection, @@ -159,7 +184,7 @@ void _showLinkMenu(EditorState editorState, BuildContext context) { final linkText = node.getAttributeInSelection(selection, StyleKey.href); _linkMenuOverlay = OverlayEntry(builder: (context) { return Positioned( - top: matchRect.bottom, + top: matchRect.bottom + 5.0, left: matchRect.left, child: Material( child: LinkMenu( diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart index 167f8e79ba..395c6818bb 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart @@ -50,9 +50,6 @@ class _ToolbarWidgetState extends State with ToolbarMixin { } Widget _buildToolbar(BuildContext context) { - final items = widget.items.where( - (item) => item.validator(widget.editorState), - ); return Material( borderRadius: BorderRadius.circular(8.0), color: const Color(0xFF333333), @@ -62,7 +59,7 @@ class _ToolbarWidgetState extends State with ToolbarMixin { height: 32.0, child: Row( crossAxisAlignment: CrossAxisAlignment.start, - children: items + children: widget.items .map( (item) => Center( child: ToolbarItemWidget( diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart index 290fe4b4bb..143f899926 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart @@ -39,13 +39,27 @@ class _FlowyToolbarState extends State void showInOffset(Offset offset, LayerLink layerLink) { hide(); + final items = defaultToolbarItems + .where((item) => item.validator(widget.editorState)) + .toList(growable: false) + ..sort((a, b) => a.type.compareTo(b.type)); + if (items.isEmpty) { + return; + } + final List dividedItems = [items.first]; + for (var i = 1; i < items.length; i++) { + if (items[i].type != items[i - 1].type) { + dividedItems.add(ToolbarItem.divider()); + } + dividedItems.add(items[i]); + } _toolbarOverlay = OverlayEntry( builder: (context) => ToolbarWidget( key: _toolbarWidgetKey, editorState: widget.editorState, layerLink: layerLink, offset: offset.translate(0, -37.0), - items: defaultToolbarItems, + items: dividedItems, ), ); Overlay.of(context)?.insert(_toolbarOverlay!); From 44b4ef4ad77a147535cc41a2c56230359dd4a774 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 23 Aug 2022 09:59:14 +0800 Subject: [PATCH 36/86] feat: filter the toolbar item should not be displayed --- .../lib/src/service/toolbar_service.dart | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart index 143f899926..40914a70e9 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart @@ -39,27 +39,13 @@ class _FlowyToolbarState extends State void showInOffset(Offset offset, LayerLink layerLink) { hide(); - final items = defaultToolbarItems - .where((item) => item.validator(widget.editorState)) - .toList(growable: false) - ..sort((a, b) => a.type.compareTo(b.type)); - if (items.isEmpty) { - return; - } - final List dividedItems = [items.first]; - for (var i = 1; i < items.length; i++) { - if (items[i].type != items[i - 1].type) { - dividedItems.add(ToolbarItem.divider()); - } - dividedItems.add(items[i]); - } _toolbarOverlay = OverlayEntry( builder: (context) => ToolbarWidget( key: _toolbarWidgetKey, editorState: widget.editorState, layerLink: layerLink, offset: offset.translate(0, -37.0), - items: dividedItems, + items: _filterItems(defaultToolbarItems), ), ); Overlay.of(context)?.insert(_toolbarOverlay!); @@ -96,4 +82,24 @@ class _FlowyToolbarState extends State super.dispose(); } + + // Filter items that should not be displayed, sort according to type, + // and insert dividers between different types. + List _filterItems(List items) { + final filterItems = items + .where((item) => item.validator(widget.editorState)) + .toList(growable: false) + ..sort((a, b) => a.type.compareTo(b.type)); + if (items.isEmpty) { + return []; + } + final List dividedItems = [filterItems.first]; + for (var i = 1; i < filterItems.length; i++) { + if (filterItems[i].type != filterItems[i - 1].type) { + dividedItems.add(ToolbarItem.divider()); + } + dividedItems.add(filterItems[i]); + } + return dividedItems; + } } From 60a7557520064929692914b66bf5ee818ced6822 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 23 Aug 2022 10:30:56 +0800 Subject: [PATCH 37/86] fix: link text is only displayed if all selected text satisfy linked style --- .../src/extensions/text_node_extensions.dart | 34 +++++++++++++------ .../appflowy_editor/lib/src/infra/log.dart | 5 +++ .../lib/src/render/toolbar/toolbar_item.dart | 7 +++- .../lib/src/service/service.dart | 6 ++-- .../lib/src/service/toolbar_service.dart | 4 +-- ..._text_style_by_command_x_handler_test.dart | 27 ++++++++++++--- 6 files changed, 63 insertions(+), 20 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart index 8416485a5a..3ca8c89f21 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart @@ -29,24 +29,34 @@ extension TextNodeExtension on TextNode { } bool allSatisfyLinkInSelection(Selection selection) => - allSatisfyInSelection(StyleKey.href, null, selection); + allSatisfyInSelection(StyleKey.href, selection, (value) { + return value != null; + }); bool allSatisfyBoldInSelection(Selection selection) => - allSatisfyInSelection(StyleKey.bold, true, selection); + allSatisfyInSelection(StyleKey.bold, selection, (value) { + return value == true; + }); bool allSatisfyItalicInSelection(Selection selection) => - allSatisfyInSelection(StyleKey.italic, true, selection); + allSatisfyInSelection(StyleKey.italic, selection, (value) { + return value == true; + }); bool allSatisfyUnderlineInSelection(Selection selection) => - allSatisfyInSelection(StyleKey.underline, true, selection); + allSatisfyInSelection(StyleKey.underline, selection, (value) { + return value == true; + }); bool allSatisfyStrikethroughInSelection(Selection selection) => - allSatisfyInSelection(StyleKey.strikethrough, true, selection); + allSatisfyInSelection(StyleKey.strikethrough, selection, (value) { + return value == true; + }); bool allSatisfyInSelection( String styleKey, - dynamic value, Selection selection, + bool Function(dynamic value) compare, ) { final ops = delta.whereType(); final startOffset = @@ -62,7 +72,7 @@ extension TextNodeExtension on TextNode { if (start < endOffset && start + length > startOffset) { if (op.attributes == null || !op.attributes!.containsKey(styleKey) || - op.attributes![styleKey] != value) { + !compare(op.attributes![styleKey])) { return false; } } @@ -116,13 +126,15 @@ extension TextNodesExtension on List { bool allSatisfyInSelection( String styleKey, Selection selection, - dynamic value, + dynamic matchValue, ) { if (isEmpty) { return false; } if (length == 1) { - return first.allSatisfyInSelection(styleKey, value, selection); + return first.allSatisfyInSelection(styleKey, selection, (value) { + return value == matchValue; + }); } else { for (var i = 0; i < length; i++) { final node = this[i]; @@ -142,7 +154,9 @@ extension TextNodesExtension on List { end: Position(path: node.path, offset: node.toRawString().length), ); } - if (!node.allSatisfyInSelection(styleKey, value, newSelection)) { + if (!node.allSatisfyInSelection(styleKey, newSelection, (value) { + return value == matchValue; + })) { return false; } } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/infra/log.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/infra/log.dart index 2218b10181..8175ecb705 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/infra/log.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/infra/log.dart @@ -75,6 +75,11 @@ class Log { /// For example, uses the logger when processing scroll events. static Log scroll = Log._(name: 'scroll'); + /// For logging message related to [AppFlowyToolbarService]. + /// + /// For example, uses the logger when processing toolbar events. + static Log toolbar = Log._(name: 'toolbar'); + /// For logging message related to UI. /// /// For example, uses the logger when building the widget. diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart index b2f6a9b2f7..e20ebb798d 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart @@ -181,7 +181,12 @@ void _showLinkMenu(EditorState editorState, BuildContext context) { final length = (selection.start.offset - selection.end.offset).abs(); final node = editorState.service.selectionService.currentSelectedNodes.first as TextNode; - final linkText = node.getAttributeInSelection(selection, StyleKey.href); + final String linkText; + if (node.allSatisfyLinkInSelection(selection)) { + linkText = node.getAttributeInSelection(selection, StyleKey.href); + } else { + linkText = ''; + } _linkMenuOverlay = OverlayEntry(builder: (context) { return Positioned( top: matchRect.bottom + 5.0, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/service.dart index e3436ea7ee..b9b6ac390a 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/service.dart @@ -36,10 +36,10 @@ class FlowyService { // toolbar service final toolbarServiceKey = GlobalKey(debugLabel: 'flowy_toolbar_service'); - FlowyToolbarService? get toolbarService { + AppFlowyToolbarService? get toolbarService { if (toolbarServiceKey.currentState != null && - toolbarServiceKey.currentState is FlowyToolbarService) { - return toolbarServiceKey.currentState! as FlowyToolbarService; + toolbarServiceKey.currentState is AppFlowyToolbarService) { + return toolbarServiceKey.currentState! as AppFlowyToolbarService; } return null; } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart index 40914a70e9..e26a186387 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart @@ -5,7 +5,7 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart'; import 'package:appflowy_editor/src/extensions/object_extensions.dart'; -abstract class FlowyToolbarService { +abstract class AppFlowyToolbarService { /// Show the toolbar widget beside the offset. void showInOffset(Offset offset, LayerLink layerLink); @@ -31,7 +31,7 @@ class FlowyToolbar extends StatefulWidget { } class _FlowyToolbarState extends State - implements FlowyToolbarService { + implements AppFlowyToolbarService { OverlayEntry? _toolbarOverlay; final _toolbarWidgetKey = GlobalKey(debugLabel: '_toolbar_widget'); diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart index 2e93d4c5f5..f27b11b37d 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart @@ -82,7 +82,14 @@ Future _testUpdateTextStyleByCommandX( ); var textNode = editor.nodeAtPath([1]) as TextNode; expect( - textNode.allSatisfyInSelection(matchStyle, matchValue, selection), true); + textNode.allSatisfyInSelection( + matchStyle, + selection, + (value) { + return value == matchValue; + }, + ), + true); selection = Selection.single(path: [1], startOffset: 0, endOffset: text.length); @@ -94,7 +101,14 @@ Future _testUpdateTextStyleByCommandX( ); textNode = editor.nodeAtPath([1]) as TextNode; expect( - textNode.allSatisfyInSelection(matchStyle, matchValue, selection), true); + textNode.allSatisfyInSelection( + matchStyle, + selection, + (value) { + return value == matchValue; + }, + ), + true); await editor.updateSelection(selection); await editor.pressLogicKey( @@ -123,9 +137,14 @@ Future _testUpdateTextStyleByCommandX( expect( node.allSatisfyInSelection( matchStyle, - matchValue, Selection.single( - path: node.path, startOffset: 0, endOffset: text.length), + path: node.path, + startOffset: 0, + endOffset: text.length, + ), + (value) { + return value == matchValue; + }, ), true, ); From 1262457755865166142c0009f9b92d6251dd99d0 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 23 Aug 2022 11:29:05 +0800 Subject: [PATCH 38/86] feat: add link menu test --- .../lib/src/render/link_menu/link_menu.dart | 29 ++++++------- .../lib/src/render/toolbar/toolbar_item.dart | 4 +- .../test/render/link_menu/link_menu_test.dart | 41 +++++++++++++++++++ 3 files changed, 57 insertions(+), 17 deletions(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart index 3a519ed70f..2c4ba2b975 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart @@ -40,24 +40,25 @@ class _LinkMenuState extends State { @override Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - color: Colors.white, - boxShadow: [ - BoxShadow( - blurRadius: 5, - spreadRadius: 1, - color: Colors.black.withOpacity(0.1), - ), - ], - borderRadius: BorderRadius.circular(6.0), - ), - child: SizedBox( - width: 350, + return SizedBox( + width: 350, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + blurRadius: 5, + spreadRadius: 1, + color: Colors.black.withOpacity(0.1), + ), + ], + borderRadius: BorderRadius.circular(6.0), + ), child: Padding( padding: const EdgeInsets.all(20.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ _buildHeader(), const SizedBox(height: 16.0), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart index e20ebb798d..41283e6b6a 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart @@ -181,11 +181,9 @@ void _showLinkMenu(EditorState editorState, BuildContext context) { final length = (selection.start.offset - selection.end.offset).abs(); final node = editorState.service.selectionService.currentSelectedNodes.first as TextNode; - final String linkText; + String? linkText; if (node.allSatisfyLinkInSelection(selection)) { linkText = node.getAttributeInSelection(selection, StyleKey.href); - } else { - linkText = ''; } _linkMenuOverlay = OverlayEntry(builder: (context) { return Positioned( diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart new file mode 100644 index 0000000000..7b4541033b --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart @@ -0,0 +1,41 @@ +import 'package:appflowy_editor/src/render/link_menu/link_menu.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('link_menu.dart', () { + testWidgets('test empty link menu actions', (tester) async { + const link = 'appflowy.io'; + var submittedText = ''; + final linkMenu = LinkMenu( + onCopyLink: () {}, + onRemoveLink: () {}, + onSubmitted: (text) { + submittedText = text; + }, + ); + await tester.pumpWidget( + MaterialApp( + home: Material( + child: linkMenu, + ), + ), + ); + + expect(find.byType(TextButton), findsNothing); + expect(find.byType(TextField), findsOneWidget); + + await tester.tap(find.byType(TextField)); + await tester.enterText(find.byType(TextField), link); + await tester.pumpAndSettle(); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + + expect(submittedText, link); + }); + }); +} From b762151d446f1eb020c9815a4301c228c74549b1 Mon Sep 17 00:00:00 2001 From: appflowy Date: Tue, 23 Aug 2022 13:13:01 +0800 Subject: [PATCH 39/86] fix: create document errors --- frontend/app_flowy/lib/startup/plugin/plugin.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app_flowy/lib/startup/plugin/plugin.dart b/frontend/app_flowy/lib/startup/plugin/plugin.dart index 6e06450295..168be76359 100644 --- a/frontend/app_flowy/lib/startup/plugin/plugin.dart +++ b/frontend/app_flowy/lib/startup/plugin/plugin.dart @@ -56,7 +56,7 @@ abstract class PluginBuilder { ViewDataTypePB get dataType => ViewDataTypePB.Text; - ViewLayoutTypePB? get subDataType => null; + ViewLayoutTypePB? get subDataType => ViewLayoutTypePB.Document; } abstract class PluginConfig { From 104b4cca9def05cd5d8f99a791055997ce217d70 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 23 Aug 2022 13:25:05 +0800 Subject: [PATCH 40/86] test: test the toolbar_service, toolbar_*.dart --- .../lib/src/render/toolbar/toolbar_item.dart | 3 +- .../test/infra/test_raw_key_event.dart | 3 + .../toolbar/toolbar_item_widget_test.dart | 46 +++++++++++ .../render/toolbar/toolbar_widget_test.dart | 11 +++ ..._text_style_by_command_x_handler_test.dart | 79 +++++++++++++++++++ .../test/service/toolbar_service_test.dart | 36 +++++++++ 6 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/test/render/toolbar/toolbar_item_widget_test.dart create mode 100644 frontend/app_flowy/packages/appflowy_editor/test/render/toolbar/toolbar_widget_test.dart create mode 100644 frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart index 41283e6b6a..107ae23b6f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart @@ -158,8 +158,6 @@ ToolbarShowValidator _showInTextSelection = (editorState) { OverlayEntry? _linkMenuOverlay; EditorState? _editorState; void _showLinkMenu(EditorState editorState, BuildContext context) { - _editorState = editorState; - final rects = editorState.service.selectionService.selectionRects; var maxBottom = 0.0; late Rect matchRect; @@ -171,6 +169,7 @@ void _showLinkMenu(EditorState editorState, BuildContext context) { } _dismissLinkMenu(); + _editorState = editorState; // Since the link menu will only show in single text selection, // We get the text node directly instead of judging details again. diff --git a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart index 150a3e2d00..2450c4e6db 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart @@ -115,6 +115,9 @@ extension on LogicalKeyboardKey { if (this == LogicalKeyboardKey.keyI) { return PhysicalKeyboardKey.keyI; } + if (this == LogicalKeyboardKey.keyK) { + return PhysicalKeyboardKey.keyK; + } if (this == LogicalKeyboardKey.keyS) { return PhysicalKeyboardKey.keyS; } diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/toolbar/toolbar_item_widget_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/toolbar/toolbar_item_widget_test.dart new file mode 100644 index 0000000000..87ae922d91 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/toolbar/toolbar_item_widget_test.dart @@ -0,0 +1,46 @@ +import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart'; +import 'package:appflowy_editor/src/render/toolbar/toolbar_item_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('toolbar_item_widget.dart', () { + testWidgets('test single toolbar item widget', (tester) async { + final key = GlobalKey(); + var hit = false; + final item = ToolbarItem( + id: 'appflowy.toolbar.test', + type: 1, + icon: const Icon(Icons.abc), + validator: (editorState) => true, + handler: (editorState, context) {}, + ); + final widget = ToolbarItemWidget( + key: key, + item: item, + onPressed: (() { + hit = true; + }), + ); + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: widget, + ), + ), + ); + + expect(find.byKey(key), findsOneWidget); + + await tester.tap(find.byKey(key)); + await tester.pumpAndSettle(); + + expect(hit, true); + }); + }); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/toolbar/toolbar_widget_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/toolbar/toolbar_widget_test.dart new file mode 100644 index 0000000000..d7e6b906f8 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/toolbar/toolbar_widget_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('toolbar_widget.dart', () { + testWidgets('test toolbar widget', (tester) async {}); + }); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart index f27b11b37d..e29308ebbc 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart @@ -1,6 +1,10 @@ import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/render/link_menu/link_menu.dart'; import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; import 'package:appflowy_editor/src/extensions/text_node_extensions.dart'; +import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart'; +import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import '../../infra/test_editor.dart'; @@ -54,6 +58,10 @@ void main() async { LogicalKeyboardKey.keyH, ); }); + + testWidgets('Presses Command + K to trigger link menu', (tester) async { + await _testLinkMenuInSingleTextSelection(tester); + }); }); } @@ -171,3 +179,74 @@ Future _testUpdateTextStyleByCommandX( ); } } + +Future _testLinkMenuInSingleTextSelection(WidgetTester tester) async { + const link = 'appflowy.io'; + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text) + ..insertTextNode(text); + await editor.startTesting(); + + final selection = + Selection.single(path: [1], startOffset: 0, endOffset: text.length); + await editor.updateSelection(selection); + + // show toolbar + expect(find.byType(ToolbarWidget), findsOneWidget); + + final item = defaultToolbarItems + .where((item) => item.id == 'appflowy.toolbar.link') + .first; + expect(find.byWidget(item.icon), findsOneWidget); + + // trigger the link menu + await editor.pressLogicKey(LogicalKeyboardKey.keyK, isMetaPressed: true); + + expect(find.byType(LinkMenu), findsOneWidget); + + await tester.enterText(find.byType(TextField), link); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + + expect(find.byType(LinkMenu), findsNothing); + + final node = editor.nodeAtPath([1]) as TextNode; + expect( + node.allSatisfyInSelection( + StyleKey.href, + selection, + (value) => value == link, + ), + true); + + await editor.updateSelection(selection); + await editor.pressLogicKey(LogicalKeyboardKey.keyK, isMetaPressed: true); + expect(find.byType(LinkMenu), findsOneWidget); + expect( + find.text(link, findRichText: true, skipOffstage: false), findsOneWidget); + + // Copy link + final copyLink = find.text('Copy link'); + expect(copyLink, findsOneWidget); + await tester.tap(copyLink); + await tester.pumpAndSettle(); + expect(find.byType(LinkMenu), findsNothing); + + // Remove link + await editor.pressLogicKey(LogicalKeyboardKey.keyK, isMetaPressed: true); + final removeLink = find.text('Remove link'); + expect(removeLink, findsOneWidget); + await tester.tap(removeLink); + await tester.pumpAndSettle(); + expect(find.byType(LinkMenu), findsNothing); + + expect( + node.allSatisfyInSelection( + StyleKey.href, + selection, + (value) => value == link, + ), + false); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart new file mode 100644 index 0000000000..9d833095e7 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart @@ -0,0 +1,36 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart'; +import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../infra/test_editor.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('toolbar_service.dart', () { + testWidgets('Test toolbar service in multi text selection', (tester) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text) + ..insertTextNode(text); + await editor.startTesting(); + + final selection = Selection( + start: Position(path: [0], offset: 0), + end: Position(path: [1], offset: text.length), + ); + await editor.updateSelection(selection); + + expect(find.byType(ToolbarWidget), findsOneWidget); + + // no link item + final item = defaultToolbarItems + .where((item) => item.id == 'appflowy.toolbar.link') + .first; + expect(find.byWidget(item.icon), findsNothing); + }); + }); +} From a2c4f73e7d9cddd7a7fff01f2771403ceea15ec7 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 23 Aug 2022 13:44:01 +0800 Subject: [PATCH 41/86] feat: add clear icon into link menu --- .../appflowy_editor/assets/images/clear.svg | 5 +++++ .../lib/src/render/link_menu/link_menu.dart | 21 ++++++++++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/assets/images/clear.svg diff --git a/frontend/app_flowy/packages/appflowy_editor/assets/images/clear.svg b/frontend/app_flowy/packages/appflowy_editor/assets/images/clear.svg new file mode 100644 index 0000000000..7f303d737f --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/assets/images/clear.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart index 2c4ba2b975..a33adf3b8c 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart @@ -100,15 +100,26 @@ class _LinkMenuState extends State { textAlign: TextAlign.left, controller: _textEditingController, onSubmitted: widget.onSubmitted, - decoration: const InputDecoration( + decoration: InputDecoration( hintText: 'URL', - hintStyle: TextStyle(fontSize: 14.0), - border: OutlineInputBorder( + hintStyle: const TextStyle(fontSize: 14.0), + contentPadding: const EdgeInsets.all(16.0), + isDense: true, + suffixIcon: IconButton( + padding: const EdgeInsets.all(4.0), + icon: const FlowySvg( + name: 'clear', + width: 24, + height: 24, + ), + onPressed: () { + _textEditingController.clear(); + }, + ), + border: const OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(12.0)), borderSide: BorderSide(color: Color(0xFFBDBDBD)), ), - contentPadding: EdgeInsets.all(16.0), - isDense: true, ), ); } From 36e1131a744871cbf2776367c65f0ce56cf562db Mon Sep 17 00:00:00 2001 From: Annie Date: Tue, 23 Aug 2022 16:02:40 +0800 Subject: [PATCH 42/86] Update CONTRIBUTING.md --- doc/CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CONTRIBUTING.md b/doc/CONTRIBUTING.md index d6879070ab..349c087c70 100644 --- a/doc/CONTRIBUTING.md +++ b/doc/CONTRIBUTING.md @@ -2,6 +2,6 @@ # Contributing to AppFlowy -Hello, and welcome! Whether you are trying to report a bug, proposing a feature request, or want to work on the code you should go visit [our documentation](https://appflowy.gitbook.io) +Hello, and welcome! Whether you are trying to report a bug, proposing a feature request, or want to work on the code you should go visit [Contributing to AppFlowy](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy) We look forward to hearing from you! From 15c47edad3b230a2ac0b8a23118812478eacd2cf Mon Sep 17 00:00:00 2001 From: Annie Date: Tue, 23 Aug 2022 16:12:36 +0800 Subject: [PATCH 43/86] Update README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index e441496c96..dc49a70ed9 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,11 @@ Please see the [changelog](https://www.appflowy.io/whatsnew) for more details ab Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. Please look at [CONTRIBUTING.md](https://github.com/AppFlowy-IO/appflowy/blob/main/doc/CONTRIBUTING.md) for details. +If your Pull Request is accepted as it fixes a bug, adds functionality, or makes AppFlowy's codebase significantly easier to use or understand, congratulations! If your administrative and managerial work behind the scenes that sustains the community as a whole, congratulations! You are now an official contributor to AppFlowy. Get in touch with us [link](https://tally.so/r/mKP5z3) to receive the very special Contributor T-shirt! +Proudly wear your T-shirt and show it to us by tagging @appflowy on Twitter. + +![DSCF3560](https://user-images.githubusercontent.com/12026239/186106423-a923f6fe-b169-477b-87e4-ffb2e375e0f6.jpg) + ## Why Are We Building This? Notion has been our favorite project and knowledge management tool in recent years because of its aesthetic appeal and functionality. Our team uses it daily, and we are on its paid plan. However, as we all know Notion has its limitations. These include weak data security and poor compatibility with mobile devices. Likewise, alternative collaborative workplace management tools also have their constraints. From 9378c1c99c251f63d2ca0f67d98f134b9bf3efcb Mon Sep 17 00:00:00 2001 From: Annie Date: Tue, 23 Aug 2022 16:16:44 +0800 Subject: [PATCH 44/86] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index dc49a70ed9..1053c4917c 100644 --- a/README.md +++ b/README.md @@ -58,8 +58,8 @@ Please see the [changelog](https://www.appflowy.io/whatsnew) for more details ab Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. Please look at [CONTRIBUTING.md](https://github.com/AppFlowy-IO/appflowy/blob/main/doc/CONTRIBUTING.md) for details. -If your Pull Request is accepted as it fixes a bug, adds functionality, or makes AppFlowy's codebase significantly easier to use or understand, congratulations! If your administrative and managerial work behind the scenes that sustains the community as a whole, congratulations! You are now an official contributor to AppFlowy. Get in touch with us [link](https://tally.so/r/mKP5z3) to receive the very special Contributor T-shirt! -Proudly wear your T-shirt and show it to us by tagging @appflowy on Twitter. +If your Pull Request is accepted as it fixes a bug, adds functionality, or makes AppFlowy's codebase significantly easier to use or understand, congratulations! If your administrative and managerial work behind the scenes that sustains the community as a whole, congratulations! You are now an official contributor to AppFlowy. Get in touch with us ([link](https://tally.so/r/mKP5z3)) to receive the very special Contributor T-shirt! +Proudly wear your T-shirt and show it to us by tagging [@appflowy](https://twitter.com/appflowy) on Twitter. ![DSCF3560](https://user-images.githubusercontent.com/12026239/186106423-a923f6fe-b169-477b-87e4-ffb2e375e0f6.jpg) From 9395eb2fd8c75d6ba28f688f71d7a4616ee92674 Mon Sep 17 00:00:00 2001 From: Annie Date: Tue, 23 Aug 2022 16:18:15 +0800 Subject: [PATCH 45/86] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1053c4917c..e77238e0dc 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ Please see the [changelog](https://www.appflowy.io/whatsnew) for more details ab ## Contributing -Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. Please look at [CONTRIBUTING.md](https://github.com/AppFlowy-IO/appflowy/blob/main/doc/CONTRIBUTING.md) for details. +Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. Please look at [Contributing to AppFlowy](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy) for details. If your Pull Request is accepted as it fixes a bug, adds functionality, or makes AppFlowy's codebase significantly easier to use or understand, congratulations! If your administrative and managerial work behind the scenes that sustains the community as a whole, congratulations! You are now an official contributor to AppFlowy. Get in touch with us ([link](https://tally.so/r/mKP5z3)) to receive the very special Contributor T-shirt! Proudly wear your T-shirt and show it to us by tagging [@appflowy](https://twitter.com/appflowy) on Twitter. From aec5c3e8070d137c29f8c268c63137c5f7383b0d Mon Sep 17 00:00:00 2001 From: appflowy Date: Tue, 23 Aug 2022 13:10:26 +0800 Subject: [PATCH 46/86] chore: fix insert animation --- .../packages/appflowy_board/CHANGELOG.md | 4 +- .../example/lib/multi_board_list_example.dart | 13 ++-- .../appflowy_board/lib/src/utils/log.dart | 8 +-- .../lib/src/widgets/board_data.dart | 3 +- .../src/widgets/reorder_flex/drag_state.dart | 6 +- .../src/widgets/reorder_flex/drag_target.dart | 10 ++- .../reorder_flex/drag_target_interceptor.dart | 22 ++++++- .../widgets/reorder_flex/reorder_flex.dart | 22 ++++--- .../reorder_phantom/phantom_controller.dart | 61 +++++++++++++------ .../reorder_phantom/phantom_state.dart | 20 +++--- 10 files changed, 111 insertions(+), 58 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_board/CHANGELOG.md b/frontend/app_flowy/packages/appflowy_board/CHANGELOG.md index 76f33711b1..fec474244d 100644 --- a/frontend/app_flowy/packages/appflowy_board/CHANGELOG.md +++ b/frontend/app_flowy/packages/appflowy_board/CHANGELOG.md @@ -6,11 +6,11 @@ * Update example * Add AppFlowy style widget -## 0.0.2 +# 0.0.2 * Update documentation -## 0.0.1 +# 0.0.1 * Support drag and drop column * Support drag and drop column items from one to another diff --git a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart index 01aba725a5..218331d198 100644 --- a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart +++ b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart @@ -26,13 +26,18 @@ class _MultiBoardListExampleState extends State { List a = [ TextItem("Card 1"), TextItem("Card 2"), - // RichTextItem(title: "Card 3", subtitle: 'Aug 1, 2020 4:05 PM'), + RichTextItem(title: "Card 3", subtitle: 'Aug 1, 2020 4:05 PM'), TextItem("Card 4"), + TextItem("Card 5"), + TextItem("Card 6"), + RichTextItem(title: "Card 7", subtitle: 'Aug 1, 2020 4:05 PM'), + RichTextItem(title: "Card 8", subtitle: 'Aug 1, 2020 4:05 PM'), + TextItem("Card 9"), ]; final column1 = AFBoardColumnData(id: "To Do", items: a); final column2 = AFBoardColumnData(id: "In Progress", items: [ - // RichTextItem(title: "Card 5", subtitle: 'Aug 1, 2020 4:05 PM'), - // TextItem("Card 6"), + RichTextItem(title: "Card 10", subtitle: 'Aug 1, 2020 4:05 PM'), + TextItem("Card 11"), ]); final column3 = AFBoardColumnData(id: "Done", items: []); @@ -93,7 +98,7 @@ class _MultiBoardListExampleState extends State { return Align( alignment: Alignment.centerLeft, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 40), child: Text(item.s), ), ); diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart index 20f810a966..d3b795f5f8 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart @@ -6,7 +6,7 @@ const DART_LOG = "Dart_LOG"; class Log { // static const enableLog = bool.hasEnvironment(DART_LOG); // static final shared = Log(); - static const enableLog = false; + static const enableLog = true; static void info(String? message) { if (enableLog) { @@ -16,19 +16,19 @@ class Log { static void debug(String? message) { if (enableLog) { - debugPrint('🐛[Debug]=> $message'); + debugPrint('🐛[Debug] - ${DateTime.now().second}=> $message'); } } static void warn(String? message) { if (enableLog) { - debugPrint('🐛[Warn]=> $message'); + debugPrint('🐛[Warn] - ${DateTime.now().second} => $message'); } } static void trace(String? message) { if (enableLog) { - // debugPrint('❗️[Trace]=> $message'); + debugPrint('❗️[Trace] - ${DateTime.now().second}=> $message'); } } } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart index e8d5471939..d0d102cba3 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart @@ -197,7 +197,8 @@ class AFBoardDataController extends ChangeNotifier assert(index != -1); if (index != -1) { if (index != newIndex) { - // Log.debug('[$BoardPhantomController] update $toColumnId:$index to $toColumnId:$phantomIndex'); + Log.debug( + '[$BoardPhantomController] update $columnId:$index to $columnId:$newIndex'); final item = columnDataController.removeAt(index, notify: false); columnDataController.insert(newIndex, item, notify: false); } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_state.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_state.dart index f5f7250834..250a4eb020 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_state.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_state.dart @@ -43,7 +43,7 @@ class FlexDragTargetData extends DragTargetData { } class DraggingState { - final String id; + final String reorderFlexId; /// The member of widget.children currently being dragged. Widget? _draggingWidget; @@ -72,7 +72,7 @@ class DraggingState { /// The additional margin to place around a computed drop area. static const double _dropAreaMargin = 0.0; - DraggingState(this.id); + DraggingState(this.reorderFlexId); Size get dropAreaSize { if (feedbackSize == null) { @@ -132,7 +132,7 @@ class DraggingState { } void updateNextIndex(int index) { - Log.trace('updateNextIndex: $index'); + Log.trace('$reorderFlexId updateNextIndex: $index'); nextIndex = index; } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart index 132d3d9bc4..57bd7343d9 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart @@ -1,3 +1,4 @@ +import 'package:appflowy_board/src/utils/log.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:provider/provider.dart'; @@ -222,10 +223,10 @@ class DragTargetAnimation { value: 0, vsync: vsync, duration: reorderAnimationDuration); insertController = AnimationController( - value: 0.0, vsync: vsync, duration: const Duration(milliseconds: 100)); + value: 0.0, vsync: vsync, duration: const Duration(milliseconds: 200)); deleteController = AnimationController( - value: 0.0, vsync: vsync, duration: const Duration(milliseconds: 10)); + value: 0.0, vsync: vsync, duration: const Duration(milliseconds: 1)); } void startDragging() { @@ -371,6 +372,7 @@ class _DragTargeMovePlaceholderState extends State { } abstract class FakeDragTargetEventTrigger { + void fakeOnDragStart(void Function(int?) callback); void fakeOnDragEnded(VoidCallback callback); } @@ -421,6 +423,10 @@ class _FakeDragTargetState /// Start insert animation widget.insertAnimationController.forward(from: 0.0); + widget.eventTrigger.fakeOnDragStart((insertIndex) { + Log.debug("[$FakeDragTarget] on drag $insertIndex"); + }); + widget.eventTrigger.fakeOnDragEnded(() { WidgetsBinding.instance.addPostFrameCallback((_) { widget.onDragEnded(widget.eventData.dragTargetData as T); diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart index be74b4eef8..1438e396a4 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import '../../utils/log.dart'; @@ -8,6 +10,8 @@ import 'reorder_flex.dart'; /// [DragTargetInterceptor] is used to intercept the [DragTarget]'s /// [onWillAccept], [OnAccept], and [onLeave] event. abstract class DragTargetInterceptor { + String get reorderFlexId; + /// Returns [yes] to receive the [DragTarget]'s event. bool canHandler(FlexDragTargetData dragTargetData); @@ -37,7 +41,7 @@ abstract class OverlapDragTargetDelegate { int dragTargetIndex, ); - bool canMoveTo(String dragTargetId); + int canMoveTo(String dragTargetId); } /// [OverlappingDragTargetInterceptor] is used to receive the overlapping @@ -47,6 +51,7 @@ abstract class OverlapDragTargetDelegate { /// Receive the [DragTarget] event if the [acceptedReorderFlexId] contains /// the passed in dragTarget' reorderFlexId. class OverlappingDragTargetInterceptor extends DragTargetInterceptor { + @override final String reorderFlexId; final List acceptedReorderFlexId; final OverlapDragTargetDelegate delegate; @@ -72,8 +77,11 @@ class OverlappingDragTargetInterceptor extends DragTargetInterceptor { if (dragTargetId == dragTargetData.reorderFlexId) { delegate.cancel(); } else { - if (delegate.canMoveTo(dragTargetId)) { - delegate.moveTo(dragTargetId, dragTargetData, 0); + final index = delegate.canMoveTo(dragTargetId); + Log.trace( + '[$OverlappingDragTargetInterceptor] move to $dragTargetId at $index'); + if (index != -1) { + delegate.moveTo(dragTargetId, dragTargetData, index); } } @@ -96,6 +104,7 @@ abstract class CrossReorderFlexDragTargetDelegate { } class CrossReorderFlexDragTargetInterceptor extends DragTargetInterceptor { + @override final String reorderFlexId; final List acceptedReorderFlexIds; final CrossReorderFlexDragTargetDelegate delegate; @@ -119,8 +128,12 @@ class CrossReorderFlexDragTargetInterceptor extends DragTargetInterceptor { /// If the columnId equal to the dragTargetData's columnId, /// it means the dragTarget is dragging on the top of its own list. /// Otherwise, it means the dargTarget was moved to another list. + Log.trace( + "[$CrossReorderFlexDragTargetInterceptor] $reorderFlexId accept ${dragTargetData.reorderFlexId} ${reorderFlexId != dragTargetData.reorderFlexId}"); return reorderFlexId != dragTargetData.reorderFlexId; } else { + Log.trace( + "[$CrossReorderFlexDragTargetInterceptor] not accept ${dragTargetData.reorderFlexId}"); return false; } } @@ -151,6 +164,9 @@ class CrossReorderFlexDragTargetInterceptor extends DragTargetInterceptor { dragTargetIndex, ); + Log.debug( + '[$CrossReorderFlexDragTargetInterceptor] dargTargetIndex: $dragTargetIndex, reorderFlexId: $reorderFlexId'); + if (isNewDragTarget == false) { delegate.updateDragTargetData(reorderFlexId, dragTargetIndex); reorderFlexState.handleOnWillAccept(context, dragTargetIndex); diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart index 7fa1a405e1..9916523782 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart @@ -36,10 +36,10 @@ class ReorderFlexConfig { final double draggingWidgetOpacity = 0.3; // How long an animation to reorder an element - final Duration reorderAnimationDuration = const Duration(milliseconds: 250); + final Duration reorderAnimationDuration = const Duration(milliseconds: 300); // How long an animation to scroll to an off-screen element - final Duration scrollAnimationDuration = const Duration(milliseconds: 250); + final Duration scrollAnimationDuration = const Duration(milliseconds: 300); final bool useMoveAnimation; @@ -213,8 +213,8 @@ class ReorderFlexState extends State shiftedIndex = dragState.calculateShiftedIndex(childIndex); } - Log.trace( - 'Rebuild: Column:[${dragState.id}] ${dragState.toString()}, childIndex: $childIndex shiftedIndex: $shiftedIndex'); + // Log.trace( + // 'Rebuild: Column:[${dragState.id}] ${dragState.toString()}, childIndex: $childIndex shiftedIndex: $shiftedIndex'); final currentIndex = dragState.currentIndex; final dragPhantomIndex = dragState.phantomIndex; @@ -330,6 +330,8 @@ class ReorderFlexState extends State widget.onDragStarted?.call(draggingIndex); }, onDragEnded: (dragTargetData) { + if (!mounted) return; + Log.debug( "[DragTarget]: Column:[${widget.dataSource.identifier}] end dragging"); _notifier.updateDragTargetIndex(-1); @@ -346,21 +348,21 @@ class ReorderFlexState extends State }); }, onWillAccept: (FlexDragTargetData dragTargetData) { + // Do not receive any events if the Insert item is animating. if (_animation.deleteController.isAnimating) { return false; } assert(widget.dataSource.items.length > dragTargetIndex); - if (_interceptDragTarget( - dragTargetData, - (interceptor) => interceptor.onWillAccept( + if (_interceptDragTarget(dragTargetData, (interceptor) { + interceptor.onWillAccept( context: builderContext, reorderFlexState: this, dragTargetData: dragTargetData, dragTargetId: reorderFlexItem.id, dragTargetIndex: dragTargetIndex, - ), - )) { + ); + })) { return true; } else { return handleOnWillAccept(builderContext, dragTargetIndex); @@ -524,7 +526,7 @@ class ReorderFlexState extends State // screen, then it is already on-screen. final double margin = widget.direction == Axis.horizontal ? dragState.dropAreaSize.width - : dragState.dropAreaSize.height; + : dragState.dropAreaSize.height / 2.0; if (_scrollController.hasClients) { final double scrollOffset = _scrollController.offset; final double topOffset = max( diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart index 0db70d0bae..42f356542b 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart @@ -59,12 +59,13 @@ class BoardPhantomController extends OverlapDragTargetDelegate } void columnStartDragging(String columnId) { - columnsState.setColumnIsDragging(columnId, false); + columnsState.setColumnIsDragging(columnId, true); } /// Remove the phantom in the column when the column is end dragging. void columnEndDragging(String columnId) { - columnsState.setColumnIsDragging(columnId, true); + columnsState.setColumnIsDragging(columnId, false); + if (phantomRecord == null) return; final fromColumnId = phantomRecord!.fromColumnId; @@ -73,19 +74,18 @@ class BoardPhantomController extends OverlapDragTargetDelegate columnsState.notifyDidRemovePhantom(toColumnId); } - if (columnsState.isDragging(fromColumnId) == false) { - return; + if (phantomRecord!.toColumnId == columnId) { + delegate.swapColumnItem( + fromColumnId, + phantomRecord!.fromColumnIndex, + toColumnId, + phantomRecord!.toColumnIndex, + ); + + Log.debug( + "[$BoardPhantomController] did move ${phantomRecord.toString()}"); + phantomRecord = null; } - - delegate.swapColumnItem( - fromColumnId, - phantomRecord!.fromColumnIndex, - toColumnId, - phantomRecord!.toColumnIndex, - ); - - Log.debug("[$BoardPhantomController] did move ${phantomRecord.toString()}"); - phantomRecord = null; } /// Remove the phantom in the column if it contains phantom @@ -113,7 +113,7 @@ class BoardPhantomController extends OverlapDragTargetDelegate PhantomColumnItem(phantomContext), ); - columnsState.notifyDidInsertPhantom(toColumnId); + columnsState.notifyDidInsertPhantom(toColumnId, phantomIndex); } /// Reset or initial the [PhantomRecord] @@ -128,8 +128,9 @@ class BoardPhantomController extends OverlapDragTargetDelegate FlexDragTargetData dragTargetData, int dragTargetIndex, ) { - // Log.debug('[$BoardPhantomController] move Column:[${dragTargetData.reorderFlexId}]:${dragTargetData.draggingIndex} ' - // 'to Column:[$columnId]:$index'); + // Log.debug( + // '[$BoardPhantomController] move Column:[${dragTargetData.reorderFlexId}]:${dragTargetData.draggingIndex} ' + // 'to Column:[$columnId]:$dragTargetIndex'); phantomRecord = PhantomRecord( toColumnId: columnId, @@ -202,8 +203,23 @@ class BoardPhantomController extends OverlapDragTargetDelegate } @override - bool canMoveTo(String dragTargetId) { - return delegate.controller(dragTargetId)?.columnData.items.isEmpty ?? false; + int canMoveTo(String dragTargetId) { + // if (columnsState.isDragging(dragTargetId)) { + // return -1; + // } + + // final controller = delegate.controller(dragTargetId); + // if (controller != null) { + // return controller.columnData.items.length; + // } else { + // return 0; + // } + + if (delegate.controller(dragTargetId)?.columnData.items.isEmpty ?? false) { + return 0; + } else { + return -1; + } } } @@ -294,7 +310,7 @@ class PassthroughPhantomContext extends FakeDragTargetEventTrigger AFColumnItem get itemData => dragTargetData.reorderFlexItem as AFColumnItem; @override - VoidCallback? onInserted; + void Function(int?)? onInserted; @override VoidCallback? onDragEnded; @@ -308,6 +324,11 @@ class PassthroughPhantomContext extends FakeDragTargetEventTrigger void fakeOnDragEnded(VoidCallback callback) { onDragEnded = callback; } + + @override + void fakeOnDragStart(void Function(int? index) callback) { + onInserted = callback; + } } class PassthroughPhantomWidget extends PhantomWidget { diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_state.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_state.dart index d33b53500d..443d7fb936 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_state.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_state.dart @@ -14,7 +14,7 @@ class ColumnPhantomStateController { void addColumnListener(String columnId, PassthroughPhantomListener listener) { _stateWithId(columnId).notifier.addListener( - onInserted: (c) => listener.onInserted?.call(), + onInserted: (index) => listener.onInserted?.call(index), onDeleted: () => listener.onDragEnded?.call(), ); } @@ -24,8 +24,8 @@ class ColumnPhantomStateController { _states.remove(columnId); } - void notifyDidInsertPhantom(String columnId) { - _stateWithId(columnId).notifier.insert(); + void notifyDidInsertPhantom(String columnId, int index) { + _stateWithId(columnId).notifier.insert(index); } void notifyDidRemovePhantom(String columnId) { @@ -48,7 +48,7 @@ class ColumnState { } abstract class PassthroughPhantomListener { - VoidCallback? get onInserted; + void Function(int?)? get onInserted; VoidCallback? get onDragEnded; } @@ -57,8 +57,8 @@ class PassthroughPhantomNotifier { final removeNotifier = PhantomDeleteNotifier(); - void insert() { - insertNotifier.insert(); + void insert(int index) { + insertNotifier.insert(index); } void remove() { @@ -66,12 +66,12 @@ class PassthroughPhantomNotifier { } void addListener({ - void Function(PassthroughPhantomContext? insertedPhantom)? onInserted, + void Function(int? insertedIndex)? onInserted, void Function()? onDeleted, }) { if (onInserted != null) { insertNotifier.addListener(() { - onInserted(insertNotifier.insertedPhantom); + onInserted(insertNotifier.insertedIndex); }); } @@ -89,9 +89,11 @@ class PassthroughPhantomNotifier { } class PhantomInsertNotifier extends ChangeNotifier { + int insertedIndex = -1; PassthroughPhantomContext? insertedPhantom; - void insert() { + void insert(int index) { + insertedIndex = index; notifyListeners(); } } From bb8e0485cdd9869900b7358bd050ee58007dd68b Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Tue, 23 Aug 2022 11:15:11 +0800 Subject: [PATCH 47/86] feat: serialize --- .../lib-ot/src/core/document/document.rs | 46 ++++++++------ .../src/core/document/document_operation.rs | 61 ++++++++++++++++--- shared-lib/lib-ot/src/core/document/node.rs | 31 +++++++++- .../lib-ot/src/core/document/transaction.rs | 16 +++-- 4 files changed, 120 insertions(+), 34 deletions(-) diff --git a/shared-lib/lib-ot/src/core/document/document.rs b/shared-lib/lib-ot/src/core/document/document.rs index 063fe408b0..17cd1d7989 100644 --- a/shared-lib/lib-ot/src/core/document/document.rs +++ b/shared-lib/lib-ot/src/core/document/document.rs @@ -1,5 +1,7 @@ use crate::core::document::position::Position; -use crate::core::{DocumentOperation, NodeAttributes, NodeData, OperationTransform, TextDelta, Transaction}; +use crate::core::{ + DocumentOperation, NodeAttributes, NodeData, NodeSubTree, OperationTransform, TextDelta, Transaction, +}; use crate::errors::{ErrorBuilder, OTError, OTErrorCode}; use indextree::{Arena, NodeId}; @@ -100,40 +102,36 @@ impl DocumentTree { } } - fn apply_insert(&mut self, path: &Position, nodes: &Vec) -> Result<(), OTError> { + fn apply_insert(&mut self, path: &Position, nodes: &Vec) -> Result<(), OTError> { let parent_path = &path.0[0..(path.0.len() - 1)]; let last_index = path.0[path.0.len() - 1]; let parent_node = self .node_at_path(&Position(parent_path.to_vec())) .ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; - let mut inserted_nodes = Vec::new(); + // let mut inserted_nodes = Vec::new(); + // + // for node in nodes { + // inserted_nodes.push(self.arena.new_node(node.to_node_data())); + // } - for node in nodes { - inserted_nodes.push(self.arena.new_node(node.clone())); - } - - self.insert_child_at_index(parent_node, last_index, &inserted_nodes) + self.insert_child_at_index(parent_node, last_index, nodes.as_ref()) } fn insert_child_at_index( &mut self, parent: NodeId, index: usize, - insert_children: &[NodeId], + insert_children: &[NodeSubTree], ) -> Result<(), OTError> { if index == 0 && parent.children(&self.arena).next().is_none() { - for id in insert_children { - parent.append(*id, &mut self.arena); - } + self.append_subtree(&parent, insert_children); return Ok(()); } let children_length = parent.children(&self.arena).fold(0, |counter, _| counter + 1); if index == children_length { - for id in insert_children { - parent.append(*id, &mut self.arena); - } + self.append_subtree(&parent, insert_children); return Ok(()); } @@ -141,12 +139,24 @@ impl DocumentTree { .child_at_index_of_path(parent, index) .ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; - for id in insert_children { - node_to_insert.insert_before(*id, &mut self.arena); - } + self.insert_subtree_before(&node_to_insert, insert_children); Ok(()) } + fn append_subtree(&mut self, parent: &NodeId, insert_children: &[NodeSubTree]) { + for child in insert_children { + let child_id = self.arena.new_node(child.to_node_data()); + parent.append(child_id, &mut self.arena); + } + } + + fn insert_subtree_before(&mut self, before: &NodeId, insert_children: &[NodeSubTree]) { + for id in insert_children { + let child_id = self.arena.new_node(id.to_node_data()); + before.insert_before(child_id, &mut self.arena); + } + } + fn apply_update(&mut self, path: &Position, attributes: &NodeAttributes) -> Result<(), OTError> { let update_node = self .node_at_path(path) diff --git a/shared-lib/lib-ot/src/core/document/document_operation.rs b/shared-lib/lib-ot/src/core/document/document_operation.rs index 46cb79be4a..6a2e97e976 100644 --- a/shared-lib/lib-ot/src/core/document/document_operation.rs +++ b/shared-lib/lib-ot/src/core/document/document_operation.rs @@ -1,21 +1,21 @@ use crate::core::document::position::Position; -use crate::core::{NodeAttributes, NodeData, TextDelta}; +use crate::core::{NodeAttributes, NodeSubTree, TextDelta}; #[derive(Clone, serde::Serialize, serde::Deserialize)] +#[serde(tag = "type")] pub enum DocumentOperation { - Insert { - path: Position, - nodes: Vec, - }, + #[serde(rename = "insert-operation")] + Insert { path: Position, nodes: Vec }, + #[serde(rename = "update-operation")] Update { path: Position, attributes: NodeAttributes, + #[serde(rename = "oldAttributes")] old_attributes: NodeAttributes, }, - Delete { - path: Position, - nodes: Vec, - }, + #[serde(rename = "delete-operation")] + Delete { path: Position, nodes: Vec }, + #[serde(rename = "text-edit-operation")] TextEdit { path: Position, delta: TextDelta, @@ -101,7 +101,7 @@ impl DocumentOperation { #[cfg(test)] mod tests { - use crate::core::Position; + use crate::core::{Delta, DocumentOperation, NodeAttributes, NodeSubTree, Position}; #[test] fn test_transform_path_1() { @@ -150,4 +150,45 @@ mod tests { vec![0, 6] ); } + + #[test] + fn test_serialize_insert_operation() { + let insert = DocumentOperation::Insert { + path: Position(vec![0, 1]), + nodes: vec![NodeSubTree::new("text")], + }; + let result = serde_json::to_string(&insert).unwrap(); + assert_eq!( + result, + r#"{"type":"insert-operation","path":[0,1],"nodes":[{"node_type":"text","attributes":{}}]}"# + ); + } + + #[test] + fn test_serialize_update_operation() { + let insert = DocumentOperation::Update { + path: Position(vec![0, 1]), + attributes: NodeAttributes::new(), + old_attributes: NodeAttributes::new(), + }; + let result = serde_json::to_string(&insert).unwrap(); + assert_eq!( + result, + r#"{"type":"update-operation","path":[0,1],"attributes":{},"oldAttributes":{}}"# + ); + } + + #[test] + fn test_serialize_text_edit_operation() { + let insert = DocumentOperation::TextEdit { + path: Position(vec![0, 1]), + delta: Delta::new(), + inverted: Delta::new(), + }; + let result = serde_json::to_string(&insert).unwrap(); + assert_eq!( + result, + r#"{"type":"text-edit-operation","path":[0,1],"delta":[],"inverted":[]}"# + ); + } } diff --git a/shared-lib/lib-ot/src/core/document/node.rs b/shared-lib/lib-ot/src/core/document/node.rs index 96747b0fd8..1e298bc72e 100644 --- a/shared-lib/lib-ot/src/core/document/node.rs +++ b/shared-lib/lib-ot/src/core/document/node.rs @@ -1,6 +1,6 @@ use crate::core::{NodeAttributes, TextDelta}; -#[derive(Clone, serde::Serialize, serde::Deserialize)] +#[derive(Clone)] pub struct NodeData { pub node_type: String, pub attributes: NodeAttributes, @@ -16,3 +16,32 @@ impl NodeData { } } } + +#[derive(Clone, serde::Serialize, serde::Deserialize)] +pub struct NodeSubTree { + pub node_type: String, + pub attributes: NodeAttributes, + #[serde(skip_serializing_if = "Option::is_none")] + pub delta: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub children: Vec>, +} + +impl NodeSubTree { + pub fn new(node_type: &str) -> NodeSubTree { + NodeSubTree { + node_type: node_type.into(), + attributes: NodeAttributes::new(), + delta: None, + children: Vec::new(), + } + } + + pub fn to_node_data(&self) -> NodeData { + NodeData { + node_type: self.node_type.clone(), + attributes: self.attributes.clone(), + delta: self.delta.clone(), + } + } +} diff --git a/shared-lib/lib-ot/src/core/document/transaction.rs b/shared-lib/lib-ot/src/core/document/transaction.rs index 00697b7da1..b24a085eae 100644 --- a/shared-lib/lib-ot/src/core/document/transaction.rs +++ b/shared-lib/lib-ot/src/core/document/transaction.rs @@ -1,5 +1,5 @@ use crate::core::document::position::Position; -use crate::core::{DocumentOperation, DocumentTree, NodeAttributes, NodeData}; +use crate::core::{DocumentOperation, DocumentTree, NodeAttributes, NodeSubTree}; use std::collections::HashMap; pub struct Transaction { @@ -25,7 +25,7 @@ impl<'a> TransactionBuilder<'a> { } } - pub fn insert_nodes_at_path(&mut self, path: &Position, nodes: &[NodeData]) { + pub fn insert_nodes_at_path(&mut self, path: &Position, nodes: &[NodeSubTree]) { self.push(DocumentOperation::Insert { path: path.clone(), nodes: nodes.to_vec(), @@ -59,11 +59,17 @@ impl<'a> TransactionBuilder<'a> { pub fn delete_nodes_at_path(&mut self, path: &Position, length: usize) { let mut node = self.document.node_at_path(path).unwrap(); - let mut deleted_nodes: Vec = Vec::new(); + let mut deleted_nodes: Vec = Vec::new(); for _ in 0..length { - let data = self.document.arena.get(node).unwrap(); - deleted_nodes.push(data.get().clone()); + let node_data = self.document.arena.get(node).unwrap(); + let data = node_data.get(); + deleted_nodes.push(NodeSubTree { + node_type: data.node_type.clone(), + attributes: data.attributes.clone(), + delta: data.delta.clone(), + children: vec![], + }); node = node.following_siblings(&self.document.arena).next().unwrap(); } From 9d1475df2bfd9caf3e0de74415248a938438726c Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Tue, 23 Aug 2022 17:12:11 +0800 Subject: [PATCH 48/86] feat: recursive append children --- shared-lib/lib-ot/src/core/document/document.rs | 17 +++++++++++------ .../src/core/document/document_operation.rs | 6 +++--- .../lib-ot/src/core/document/transaction.rs | 8 ++++---- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/shared-lib/lib-ot/src/core/document/document.rs b/shared-lib/lib-ot/src/core/document/document.rs index 17cd1d7989..340f4fdc57 100644 --- a/shared-lib/lib-ot/src/core/document/document.rs +++ b/shared-lib/lib-ot/src/core/document/document.rs @@ -102,7 +102,7 @@ impl DocumentTree { } } - fn apply_insert(&mut self, path: &Position, nodes: &Vec) -> Result<(), OTError> { + fn apply_insert(&mut self, path: &Position, nodes: &[Box]) -> Result<(), OTError> { let parent_path = &path.0[0..(path.0.len() - 1)]; let last_index = path.0[path.0.len() - 1]; let parent_node = self @@ -121,7 +121,7 @@ impl DocumentTree { &mut self, parent: NodeId, index: usize, - insert_children: &[NodeSubTree], + insert_children: &[Box], ) -> Result<(), OTError> { if index == 0 && parent.children(&self.arena).next().is_none() { self.append_subtree(&parent, insert_children); @@ -143,17 +143,22 @@ impl DocumentTree { Ok(()) } - fn append_subtree(&mut self, parent: &NodeId, insert_children: &[NodeSubTree]) { + // recursive append the subtrees to the node + fn append_subtree(&mut self, parent: &NodeId, insert_children: &[Box]) { for child in insert_children { let child_id = self.arena.new_node(child.to_node_data()); parent.append(child_id, &mut self.arena); + + self.append_subtree(&child_id, child.children.as_ref()); } } - fn insert_subtree_before(&mut self, before: &NodeId, insert_children: &[NodeSubTree]) { - for id in insert_children { - let child_id = self.arena.new_node(id.to_node_data()); + fn insert_subtree_before(&mut self, before: &NodeId, insert_children: &[Box]) { + for child in insert_children { + let child_id = self.arena.new_node(child.to_node_data()); before.insert_before(child_id, &mut self.arena); + + self.append_subtree(&child_id, child.children.as_ref()); } } diff --git a/shared-lib/lib-ot/src/core/document/document_operation.rs b/shared-lib/lib-ot/src/core/document/document_operation.rs index 6a2e97e976..77c74127bd 100644 --- a/shared-lib/lib-ot/src/core/document/document_operation.rs +++ b/shared-lib/lib-ot/src/core/document/document_operation.rs @@ -5,7 +5,7 @@ use crate::core::{NodeAttributes, NodeSubTree, TextDelta}; #[serde(tag = "type")] pub enum DocumentOperation { #[serde(rename = "insert-operation")] - Insert { path: Position, nodes: Vec }, + Insert { path: Position, nodes: Vec> }, #[serde(rename = "update-operation")] Update { path: Position, @@ -14,7 +14,7 @@ pub enum DocumentOperation { old_attributes: NodeAttributes, }, #[serde(rename = "delete-operation")] - Delete { path: Position, nodes: Vec }, + Delete { path: Position, nodes: Vec> }, #[serde(rename = "text-edit-operation")] TextEdit { path: Position, @@ -155,7 +155,7 @@ mod tests { fn test_serialize_insert_operation() { let insert = DocumentOperation::Insert { path: Position(vec![0, 1]), - nodes: vec![NodeSubTree::new("text")], + nodes: vec![Box::new(NodeSubTree::new("text"))], }; let result = serde_json::to_string(&insert).unwrap(); assert_eq!( diff --git a/shared-lib/lib-ot/src/core/document/transaction.rs b/shared-lib/lib-ot/src/core/document/transaction.rs index b24a085eae..86448820bc 100644 --- a/shared-lib/lib-ot/src/core/document/transaction.rs +++ b/shared-lib/lib-ot/src/core/document/transaction.rs @@ -25,7 +25,7 @@ impl<'a> TransactionBuilder<'a> { } } - pub fn insert_nodes_at_path(&mut self, path: &Position, nodes: &[NodeSubTree]) { + pub fn insert_nodes_at_path(&mut self, path: &Position, nodes: &[Box]) { self.push(DocumentOperation::Insert { path: path.clone(), nodes: nodes.to_vec(), @@ -59,17 +59,17 @@ impl<'a> TransactionBuilder<'a> { pub fn delete_nodes_at_path(&mut self, path: &Position, length: usize) { let mut node = self.document.node_at_path(path).unwrap(); - let mut deleted_nodes: Vec = Vec::new(); + let mut deleted_nodes: Vec> = Vec::new(); for _ in 0..length { let node_data = self.document.arena.get(node).unwrap(); let data = node_data.get(); - deleted_nodes.push(NodeSubTree { + deleted_nodes.push(Box::new(NodeSubTree { node_type: data.node_type.clone(), attributes: data.attributes.clone(), delta: data.delta.clone(), children: vec![], - }); + })); node = node.following_siblings(&self.document.arena).next().unwrap(); } From d941db29a133f750583bc43fe905d7658424a6cb Mon Sep 17 00:00:00 2001 From: appflowy Date: Tue, 23 Aug 2022 17:21:08 +0800 Subject: [PATCH 49/86] chore: disable children receive events under fake drag target --- .../src/widgets/reorder_flex/drag_target.dart | 29 +++++++++++++++++-- .../reorder_phantom/phantom_controller.dart | 18 ++++++------ 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart index 57bd7343d9..621179455a 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart @@ -277,6 +277,31 @@ class IgnorePointerWidget extends StatelessWidget { } } +class AbsorbPointerWidget extends StatelessWidget { + final Widget? child; + final bool useIntrinsicSize; + const AbsorbPointerWidget({ + required this.child, + this.useIntrinsicSize = false, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final sizedChild = useIntrinsicSize + ? child + : SizedBox(width: 0.0, height: 0.0, child: child); + + final opacity = useIntrinsicSize ? 0.3 : 0.0; + return AbsorbPointer( + child: Opacity( + opacity: opacity, + child: sizedChild, + ), + ); + } +} + class PhantomWidget extends StatelessWidget { final Widget? child; final double opacity; @@ -442,7 +467,7 @@ class _FakeDragTargetState return SizeTransitionWithIntrinsicSize( sizeFactor: widget.deleteAnimationController, axis: Axis.vertical, - child: IgnorePointerWidget( + child: AbsorbPointerWidget( child: widget.child, ), ); @@ -450,7 +475,7 @@ class _FakeDragTargetState return SizeTransitionWithIntrinsicSize( sizeFactor: widget.insertAnimationController, axis: Axis.vertical, - child: IgnorePointerWidget( + child: AbsorbPointerWidget( useIntrinsicSize: true, child: widget.child, ), diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart index 42f356542b..45a7ce2392 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart @@ -204,16 +204,16 @@ class BoardPhantomController extends OverlapDragTargetDelegate @override int canMoveTo(String dragTargetId) { - // if (columnsState.isDragging(dragTargetId)) { - // return -1; - // } + if (columnsState.isDragging(dragTargetId)) { + return -1; + } - // final controller = delegate.controller(dragTargetId); - // if (controller != null) { - // return controller.columnData.items.length; - // } else { - // return 0; - // } + final controller = delegate.controller(dragTargetId); + if (controller != null) { + return controller.columnData.items.length; + } else { + return 0; + } if (delegate.controller(dragTargetId)?.columnData.items.isEmpty ?? false) { return 0; From 7be510fa22cd102386f05f476d3d4b50644d5d87 Mon Sep 17 00:00:00 2001 From: appflowy Date: Tue, 23 Aug 2022 17:44:53 +0800 Subject: [PATCH 50/86] chore: delay overlap dragtarget event --- .../appflowy_board/lib/src/utils/log.dart | 2 +- .../lib/src/widgets/board_data.dart | 2 +- .../reorder_flex/drag_target_interceptor.dart | 18 ++++++++++++------ .../src/widgets/reorder_flex/reorder_flex.dart | 2 +- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart index d3b795f5f8..3d5baab30d 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart @@ -28,7 +28,7 @@ class Log { static void trace(String? message) { if (enableLog) { - debugPrint('❗️[Trace] - ${DateTime.now().second}=> $message'); + // debugPrint('❗️[Trace] - ${DateTime.now().second}=> $message'); } } } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart index d0d102cba3..a08bba378f 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart @@ -197,7 +197,7 @@ class AFBoardDataController extends ChangeNotifier assert(index != -1); if (index != -1) { if (index != newIndex) { - Log.debug( + Log.trace( '[$BoardPhantomController] update $columnId:$index to $columnId:$newIndex'); final item = columnDataController.removeAt(index, notify: false); columnDataController.insert(newIndex, item, notify: false); diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart index 1438e396a4..d3a7e405e8 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart @@ -55,6 +55,7 @@ class OverlappingDragTargetInterceptor extends DragTargetInterceptor { final String reorderFlexId; final List acceptedReorderFlexId; final OverlapDragTargetDelegate delegate; + Timer? _delayOperation; OverlappingDragTargetInterceptor({ required this.delegate, @@ -77,12 +78,17 @@ class OverlappingDragTargetInterceptor extends DragTargetInterceptor { if (dragTargetId == dragTargetData.reorderFlexId) { delegate.cancel(); } else { - final index = delegate.canMoveTo(dragTargetId); - Log.trace( - '[$OverlappingDragTargetInterceptor] move to $dragTargetId at $index'); - if (index != -1) { - delegate.moveTo(dragTargetId, dragTargetData, index); - } + /// The priority of the column interactions is high than the cross column. + /// Workaround: delay 100 milliseconds to lower the cross column event priority. + _delayOperation?.cancel(); + _delayOperation = Timer(const Duration(milliseconds: 100), () { + final index = delegate.canMoveTo(dragTargetId); + if (index != -1) { + Log.trace( + '[$OverlappingDragTargetInterceptor] move to $dragTargetId at $index'); + delegate.moveTo(dragTargetId, dragTargetData, index); + } + }); } return true; diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart index 9916523782..f84fe8c991 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart @@ -437,7 +437,7 @@ class ReorderFlexState extends State /// The [willAccept] will be true if the dargTarget is the widget that gets /// dragged and it is dragged on top of the other dragTargets. /// - Log.debug( + Log.trace( '[$ReorderDragTarget] ${widget.dataSource.identifier} on will accept, dragIndex:$dragIndex, dragTargetIndex:$dragTargetIndex, count: ${widget.dataSource.items.length}'); bool willAccept = From d6ef13adaeb5400a0c7c3b4ea6618a6a350336f5 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Tue, 23 Aug 2022 17:58:54 +0800 Subject: [PATCH 51/86] feat: get deleted subtrees from the document --- .../lib-ot/src/core/document/document.rs | 9 ----- .../lib-ot/src/core/document/transaction.rs | 33 ++++++++++++++----- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/shared-lib/lib-ot/src/core/document/document.rs b/shared-lib/lib-ot/src/core/document/document.rs index 340f4fdc57..73ff03fe64 100644 --- a/shared-lib/lib-ot/src/core/document/document.rs +++ b/shared-lib/lib-ot/src/core/document/document.rs @@ -108,11 +108,6 @@ impl DocumentTree { let parent_node = self .node_at_path(&Position(parent_path.to_vec())) .ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; - // let mut inserted_nodes = Vec::new(); - // - // for node in nodes { - // inserted_nodes.push(self.arena.new_node(node.to_node_data())); - // } self.insert_child_at_index(parent_node, last_index, nodes.as_ref()) } @@ -167,10 +162,6 @@ impl DocumentTree { .node_at_path(path) .ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; let node_data = self.arena.get_mut(update_node).unwrap(); - // let new_node = NodeData { - // ..node_data.get().clone() - // attributes: - // }; let new_node = { let old_attributes = &node_data.get().attributes; let new_attributes = NodeAttributes::compose(&old_attributes, attributes); diff --git a/shared-lib/lib-ot/src/core/document/transaction.rs b/shared-lib/lib-ot/src/core/document/transaction.rs index 86448820bc..e22ba21c3a 100644 --- a/shared-lib/lib-ot/src/core/document/transaction.rs +++ b/shared-lib/lib-ot/src/core/document/transaction.rs @@ -1,6 +1,7 @@ use crate::core::document::position::Position; use crate::core::{DocumentOperation, DocumentTree, NodeAttributes, NodeSubTree}; use std::collections::HashMap; +use indextree::NodeId; pub struct Transaction { pub operations: Vec, @@ -62,14 +63,7 @@ impl<'a> TransactionBuilder<'a> { let mut deleted_nodes: Vec> = Vec::new(); for _ in 0..length { - let node_data = self.document.arena.get(node).unwrap(); - let data = node_data.get(); - deleted_nodes.push(Box::new(NodeSubTree { - node_type: data.node_type.clone(), - attributes: data.attributes.clone(), - delta: data.delta.clone(), - children: vec![], - })); + deleted_nodes.push(self.get_deleted_nodes(node.clone())); node = node.following_siblings(&self.document.arena).next().unwrap(); } @@ -79,6 +73,29 @@ impl<'a> TransactionBuilder<'a> { }) } + fn get_deleted_nodes(&self, node_id: NodeId) -> Box { + let node = self.document.arena.get(node_id.clone()).unwrap(); + let node_data = node.get(); + let mut children: Vec> = vec![]; + + let mut children_iterators = node_id.children(&self.document.arena); + loop { + let next_child = children_iterators.next(); + if let Some(child_id) = next_child { + children.push(self.get_deleted_nodes(child_id)); + } else { + break; + } + }; + + Box::new(NodeSubTree { + node_type: node_data.node_type.clone(), + attributes: node_data.attributes.clone(), + delta: node_data.delta.clone(), + children, + }) + } + pub fn push(&mut self, op: DocumentOperation) { self.operations.push(op); } From 096544d6a314e08e10606935d8d260d1e6fc03ba Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Tue, 23 Aug 2022 19:39:00 +0800 Subject: [PATCH 52/86] feat: test insert sub trees --- .../src/core/document/document_operation.rs | 30 +++++++++-- shared-lib/lib-ot/src/core/document/node.rs | 1 + .../lib-ot/src/core/document/transaction.rs | 4 +- shared-lib/lib-ot/tests/main.rs | 51 ++++++++++++++----- 4 files changed, 67 insertions(+), 19 deletions(-) diff --git a/shared-lib/lib-ot/src/core/document/document_operation.rs b/shared-lib/lib-ot/src/core/document/document_operation.rs index 77c74127bd..caca29e110 100644 --- a/shared-lib/lib-ot/src/core/document/document_operation.rs +++ b/shared-lib/lib-ot/src/core/document/document_operation.rs @@ -5,7 +5,10 @@ use crate::core::{NodeAttributes, NodeSubTree, TextDelta}; #[serde(tag = "type")] pub enum DocumentOperation { #[serde(rename = "insert-operation")] - Insert { path: Position, nodes: Vec> }, + Insert { + path: Position, + nodes: Vec>, + }, #[serde(rename = "update-operation")] Update { path: Position, @@ -14,7 +17,10 @@ pub enum DocumentOperation { old_attributes: NodeAttributes, }, #[serde(rename = "delete-operation")] - Delete { path: Position, nodes: Vec> }, + Delete { + path: Position, + nodes: Vec>, + }, #[serde(rename = "text-edit-operation")] TextEdit { path: Position, @@ -160,7 +166,25 @@ mod tests { let result = serde_json::to_string(&insert).unwrap(); assert_eq!( result, - r#"{"type":"insert-operation","path":[0,1],"nodes":[{"node_type":"text","attributes":{}}]}"# + r#"{"type":"insert-operation","path":[0,1],"nodes":[{"type":"text","attributes":{}}]}"# + ); + } + + #[test] + fn test_serialize_insert_sub_trees() { + let insert = DocumentOperation::Insert { + path: Position(vec![0, 1]), + nodes: vec![Box::new(NodeSubTree { + node_type: "text".into(), + attributes: NodeAttributes::new(), + delta: None, + children: vec![Box::new(NodeSubTree::new("text".into()))], + })], + }; + let result = serde_json::to_string(&insert).unwrap(); + assert_eq!( + result, + r#"{"type":"insert-operation","path":[0,1],"nodes":[{"type":"text","attributes":{},"children":[{"type":"text","attributes":{}}]}]}"# ); } diff --git a/shared-lib/lib-ot/src/core/document/node.rs b/shared-lib/lib-ot/src/core/document/node.rs index 1e298bc72e..e74c7d4918 100644 --- a/shared-lib/lib-ot/src/core/document/node.rs +++ b/shared-lib/lib-ot/src/core/document/node.rs @@ -19,6 +19,7 @@ impl NodeData { #[derive(Clone, serde::Serialize, serde::Deserialize)] pub struct NodeSubTree { + #[serde(rename = "type")] pub node_type: String, pub attributes: NodeAttributes, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/shared-lib/lib-ot/src/core/document/transaction.rs b/shared-lib/lib-ot/src/core/document/transaction.rs index e22ba21c3a..73fce7d8ad 100644 --- a/shared-lib/lib-ot/src/core/document/transaction.rs +++ b/shared-lib/lib-ot/src/core/document/transaction.rs @@ -1,7 +1,7 @@ use crate::core::document::position::Position; use crate::core::{DocumentOperation, DocumentTree, NodeAttributes, NodeSubTree}; -use std::collections::HashMap; use indextree::NodeId; +use std::collections::HashMap; pub struct Transaction { pub operations: Vec, @@ -86,7 +86,7 @@ impl<'a> TransactionBuilder<'a> { } else { break; } - }; + } Box::new(NodeSubTree { node_type: node_data.node_type.clone(), diff --git a/shared-lib/lib-ot/tests/main.rs b/shared-lib/lib-ot/tests/main.rs index 7daf74e957..31e7748b3a 100644 --- a/shared-lib/lib-ot/tests/main.rs +++ b/shared-lib/lib-ot/tests/main.rs @@ -1,4 +1,4 @@ -use lib_ot::core::{DocumentTree, NodeData, Position, TransactionBuilder}; +use lib_ot::core::{DocumentTree, NodeAttributes, NodeSubTree, Position, TransactionBuilder}; use lib_ot::errors::OTErrorCode; use std::collections::HashMap; @@ -13,7 +13,7 @@ fn test_documents() { let mut document = DocumentTree::new(); let transaction = { let mut tb = TransactionBuilder::new(&document); - tb.insert_nodes_at_path(&vec![0].into(), &vec![NodeData::new("text")]); + tb.insert_nodes_at_path(&vec![0].into(), &vec![Box::new(NodeSubTree::new("text"))]); tb.finalize() }; document.apply(transaction).unwrap(); @@ -47,29 +47,52 @@ fn test_inserts_nodes() { let mut document = DocumentTree::new(); let transaction = { let mut tb = TransactionBuilder::new(&document); - tb.insert_nodes_at_path(&vec![0].into(), &vec![NodeData::new("text")]); - tb.insert_nodes_at_path(&vec![1].into(), &vec![NodeData::new("text")]); - tb.insert_nodes_at_path(&vec![2].into(), &vec![NodeData::new("text")]); + tb.insert_nodes_at_path(&vec![0].into(), &vec![Box::new(NodeSubTree::new("text"))]); + tb.insert_nodes_at_path(&vec![1].into(), &vec![Box::new(NodeSubTree::new("text"))]); + tb.insert_nodes_at_path(&vec![2].into(), &vec![Box::new(NodeSubTree::new("text"))]); tb.finalize() }; document.apply(transaction).unwrap(); let transaction = { let mut tb = TransactionBuilder::new(&document); - tb.insert_nodes_at_path(&vec![1].into(), &vec![NodeData::new("text")]); + tb.insert_nodes_at_path(&vec![1].into(), &vec![Box::new(NodeSubTree::new("text"))]); tb.finalize() }; document.apply(transaction).unwrap(); } +#[test] +fn test_inserts_subtrees() { + let mut document = DocumentTree::new(); + let transaction = { + let mut tb = TransactionBuilder::new(&document); + tb.insert_nodes_at_path( + &vec![0].into(), + &vec![Box::new(NodeSubTree { + node_type: "text".into(), + attributes: NodeAttributes::new(), + delta: None, + children: vec![Box::new(NodeSubTree::new("image".into()))], + })], + ); + tb.finalize() + }; + document.apply(transaction).unwrap(); + + let node = document.node_at_path(&Position(vec![0, 0])).unwrap(); + let data = document.arena.get(node).unwrap().get(); + assert_eq!(data.node_type, "image"); +} + #[test] fn test_update_nodes() { let mut document = DocumentTree::new(); let transaction = { let mut tb = TransactionBuilder::new(&document); - tb.insert_nodes_at_path(&vec![0].into(), &vec![NodeData::new("text")]); - tb.insert_nodes_at_path(&vec![1].into(), &vec![NodeData::new("text")]); - tb.insert_nodes_at_path(&vec![2].into(), &vec![NodeData::new("text")]); + tb.insert_nodes_at_path(&vec![0].into(), &vec![Box::new(NodeSubTree::new("text"))]); + tb.insert_nodes_at_path(&vec![1].into(), &vec![Box::new(NodeSubTree::new("text"))]); + tb.insert_nodes_at_path(&vec![2].into(), &vec![Box::new(NodeSubTree::new("text"))]); tb.finalize() }; document.apply(transaction).unwrap(); @@ -92,9 +115,9 @@ fn test_delete_nodes() { let mut document = DocumentTree::new(); let transaction = { let mut tb = TransactionBuilder::new(&document); - tb.insert_nodes_at_path(&vec![0].into(), &vec![NodeData::new("text")]); - tb.insert_nodes_at_path(&vec![1].into(), &vec![NodeData::new("text")]); - tb.insert_nodes_at_path(&vec![2].into(), &vec![NodeData::new("text")]); + tb.insert_nodes_at_path(&vec![0].into(), &vec![Box::new(NodeSubTree::new("text"))]); + tb.insert_nodes_at_path(&vec![1].into(), &vec![Box::new(NodeSubTree::new("text"))]); + tb.insert_nodes_at_path(&vec![2].into(), &vec![Box::new(NodeSubTree::new("text"))]); tb.finalize() }; document.apply(transaction).unwrap(); @@ -115,8 +138,8 @@ fn test_errors() { let mut document = DocumentTree::new(); let transaction = { let mut tb = TransactionBuilder::new(&document); - tb.insert_nodes_at_path(&vec![0].into(), &vec![NodeData::new("text")]); - tb.insert_nodes_at_path(&vec![100].into(), &vec![NodeData::new("text")]); + tb.insert_nodes_at_path(&vec![0].into(), &vec![Box::new(NodeSubTree::new("text"))]); + tb.insert_nodes_at_path(&vec![100].into(), &vec![Box::new(NodeSubTree::new("text"))]); tb.finalize() }; let result = document.apply(transaction); From 02e10301bf1a652ae5fd30f3c97c0897bc473ed8 Mon Sep 17 00:00:00 2001 From: appflowy Date: Tue, 23 Aug 2022 19:59:39 +0800 Subject: [PATCH 53/86] chore: update appflowy_board version --- frontend/app_flowy/packages/appflowy_board/CHANGELOG.md | 7 ++++++- frontend/app_flowy/packages/appflowy_board/pubspec.yaml | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_board/CHANGELOG.md b/frontend/app_flowy/packages/appflowy_board/CHANGELOG.md index fec474244d..c4c6495533 100644 --- a/frontend/app_flowy/packages/appflowy_board/CHANGELOG.md +++ b/frontend/app_flowy/packages/appflowy_board/CHANGELOG.md @@ -1,5 +1,10 @@ +# 0.0.5 +* Optimize insert card animation +* Enable insert card at the end of the column +* Fix some bugs + # 0.0.4 -* fix some bugs +* Fix some bugs # 0.0.3 * Support customize UI diff --git a/frontend/app_flowy/packages/appflowy_board/pubspec.yaml b/frontend/app_flowy/packages/appflowy_board/pubspec.yaml index 962fc8be80..6bb2feabfe 100644 --- a/frontend/app_flowy/packages/appflowy_board/pubspec.yaml +++ b/frontend/app_flowy/packages/appflowy_board/pubspec.yaml @@ -1,6 +1,6 @@ name: appflowy_board description: AppFlowy board implementation. -version: 0.0.4 +version: 0.0.5 homepage: https://github.com/AppFlowy-IO/AppFlowy repository: https://github.com/AppFlowy-IO/AppFlowy/tree/main/frontend/app_flowy/packages/appflowy_board From 2152bb71882f63eb08443cd4833eec62c3c308b9 Mon Sep 17 00:00:00 2001 From: appflowy Date: Tue, 23 Aug 2022 20:00:07 +0800 Subject: [PATCH 54/86] chore: disable log and try to fix some weird bugs --- .../appflowy_board/lib/src/utils/log.dart | 6 +-- .../appflowy_board/lib/src/widgets/board.dart | 42 ++++++++++++------- .../widgets/board_column/board_column.dart | 11 +++-- .../src/widgets/reorder_flex/drag_state.dart | 2 +- .../src/widgets/reorder_flex/drag_target.dart | 7 ++-- .../reorder_flex/drag_target_interceptor.dart | 16 +++++++ .../widgets/reorder_flex/reorder_flex.dart | 4 +- .../reorder_phantom/phantom_controller.dart | 6 --- 8 files changed, 59 insertions(+), 35 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart index 3d5baab30d..9c23060b26 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart @@ -4,9 +4,7 @@ import 'package:flutter/material.dart'; const DART_LOG = "Dart_LOG"; class Log { - // static const enableLog = bool.hasEnvironment(DART_LOG); - // static final shared = Log(); - static const enableLog = true; + static const enableLog = false; static void info(String? message) { if (enableLog) { @@ -28,7 +26,7 @@ class Log { static void trace(String? message) { if (enableLog) { - // debugPrint('❗️[Trace] - ${DateTime.now().second}=> $message'); + debugPrint('❗️[Trace] - ${DateTime.now().second}=> $message'); } } } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart index 20824ba6b9..e07ee39d61 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart @@ -159,7 +159,7 @@ class _BoardContentState extends State { dataSource: widget.dataController, direction: Axis.horizontal, interceptor: interceptor, - children: _buildColumns(), + children: _buildColumns(interceptor.columnKeys), ); return Stack( @@ -191,7 +191,7 @@ class _BoardContentState extends State { ); } - List _buildColumns() { + List _buildColumns(List columnKeys) { final List children = widget.dataController.columnDatas.asMap().entries.map( (item) { @@ -208,21 +208,33 @@ class _BoardContentState extends State { value: widget.dataController.columnController(columnData.id), child: Consumer( builder: (context, value, child) { + final boardColumn = AFBoardColumnWidget( + margin: _marginFromIndex(columnIndex), + itemMargin: widget.config.columnItemPadding, + headerBuilder: widget.headerBuilder, + footBuilder: widget.footBuilder, + cardBuilder: widget.cardBuilder, + dataSource: dataSource, + scrollController: ScrollController(), + phantomController: widget.phantomController, + onReorder: widget.dataController.moveColumnItem, + cornerRadius: widget.config.cornerRadius, + backgroundColor: widget.config.columnBackgroundColor, + ); + + // columnKeys + // .removeWhere((element) => element.columnId == columnData.id); + + // columnKeys.add( + // ColumnKey( + // columnId: columnData.id, + // key: boardColumn.columnGlobalKey, + // ), + // ); + return ConstrainedBox( constraints: widget.columnConstraints, - child: AFBoardColumnWidget( - margin: _marginFromIndex(columnIndex), - itemMargin: widget.config.columnItemPadding, - headerBuilder: widget.headerBuilder, - footBuilder: widget.footBuilder, - cardBuilder: widget.cardBuilder, - dataSource: dataSource, - scrollController: ScrollController(), - phantomController: widget.phantomController, - onReorder: widget.dataController.moveColumnItem, - cornerRadius: widget.config.cornerRadius, - backgroundColor: widget.config.columnBackgroundColor, - ), + child: boardColumn, ); }, ), diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart index cbc537810e..d4e5ff8800 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart @@ -87,7 +87,9 @@ class AFBoardColumnWidget extends StatefulWidget { final Color backgroundColor; - const AFBoardColumnWidget({ + final GlobalKey columnGlobalKey = GlobalKey(); + + AFBoardColumnWidget({ Key? key, this.headerBuilder, this.footBuilder, @@ -136,8 +138,8 @@ class _AFBoardColumnWidgetState extends State { draggableTargetBuilder: PhantomDraggableBuilder(), ); - final reorderFlex = ReorderFlex( - key: widget.key, + Widget reorderFlex = ReorderFlex( + key: widget.columnGlobalKey, scrollController: widget.scrollController, config: widget.config, onDragStarted: (index) { @@ -160,6 +162,9 @@ class _AFBoardColumnWidgetState extends State { children: children, ); + // reorderFlex = + // KeyedSubtree(key: widget.columnGlobalKey, child: reorderFlex); + return Container( margin: widget.margin, clipBehavior: Clip.hardEdge, diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_state.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_state.dart index 250a4eb020..592277afbc 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_state.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_state.dart @@ -132,7 +132,7 @@ class DraggingState { } void updateNextIndex(int index) { - Log.trace('$reorderFlexId updateNextIndex: $index'); + Log.debug('$reorderFlexId updateNextIndex: $index'); nextIndex = index; } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart index 621179455a..8217d2736f 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart @@ -1,4 +1,3 @@ -import 'package:appflowy_board/src/utils/log.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:provider/provider.dart'; @@ -448,9 +447,9 @@ class _FakeDragTargetState /// Start insert animation widget.insertAnimationController.forward(from: 0.0); - widget.eventTrigger.fakeOnDragStart((insertIndex) { - Log.debug("[$FakeDragTarget] on drag $insertIndex"); - }); + // widget.eventTrigger.fakeOnDragStart((insertIndex) { + // Log.trace("[$FakeDragTarget] on drag $insertIndex"); + // }); widget.eventTrigger.fakeOnDragEnded(() { WidgetsBinding.instance.addPostFrameCallback((_) { diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart index d3a7e405e8..36366cd1e0 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart @@ -55,6 +55,7 @@ class OverlappingDragTargetInterceptor extends DragTargetInterceptor { final String reorderFlexId; final List acceptedReorderFlexId; final OverlapDragTargetDelegate delegate; + final List columnKeys = []; Timer? _delayOperation; OverlappingDragTargetInterceptor({ @@ -87,6 +88,15 @@ class OverlappingDragTargetInterceptor extends DragTargetInterceptor { Log.trace( '[$OverlappingDragTargetInterceptor] move to $dragTargetId at $index'); delegate.moveTo(dragTargetId, dragTargetData, index); + + // final columnIndex = columnKeys + // .indexWhere((element) => element.columnId == dragTargetId); + // if (columnIndex != -1) { + // final state = columnKeys[columnIndex].key.currentState; + // if (state is ReorderFlexState) { + // state.handleOnWillAccept(context, index); + // } + // } } }); } @@ -95,6 +105,12 @@ class OverlappingDragTargetInterceptor extends DragTargetInterceptor { } } +class ColumnKey { + String columnId; + GlobalKey key; + ColumnKey({required this.columnId, required this.key}); +} + abstract class CrossReorderFlexDragTargetDelegate { /// * [reorderFlexId] is the id that the [ReorderFlex] passed in. bool acceptNewDragTargetData( diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart index f84fe8c991..26b68c2304 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart @@ -213,8 +213,8 @@ class ReorderFlexState extends State shiftedIndex = dragState.calculateShiftedIndex(childIndex); } - // Log.trace( - // 'Rebuild: Column:[${dragState.id}] ${dragState.toString()}, childIndex: $childIndex shiftedIndex: $shiftedIndex'); + Log.trace( + 'Rebuild: Column:[${dragState.reorderFlexId}] ${dragState.toString()}, childIndex: $childIndex shiftedIndex: $shiftedIndex'); final currentIndex = dragState.currentIndex; final dragPhantomIndex = dragState.phantomIndex; diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart index 45a7ce2392..4dd4f05a74 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart @@ -214,12 +214,6 @@ class BoardPhantomController extends OverlapDragTargetDelegate } else { return 0; } - - if (delegate.controller(dragTargetId)?.columnData.items.isEmpty ?? false) { - return 0; - } else { - return -1; - } } } From 82f1f0e3e3da813991b9d2b29656314add038969 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 23 Aug 2022 20:24:54 +0800 Subject: [PATCH 55/86] feat: support resize, copy, align, delete in image ndoe widget --- .../images/image_toolbar/align_center.svg | 5 + .../images/image_toolbar/align_left.svg | 5 + .../images/image_toolbar/align_right.svg | 5 + .../assets/images/image_toolbar/copy.svg | 4 + .../assets/images/image_toolbar/delete.svg | 6 + .../assets/images/image_toolbar/divider.svg | 3 + .../assets/images/image_toolbar/share.svg | 4 + .../example/assets/example.json | 10 +- .../appflowy_editor/example/lib/main.dart | 4 +- .../src/render/image/image_node_builder.dart | 64 ++++ .../src/render/image/image_node_widget.dart | 276 ++++++++++++++++++ .../lib/src/service/editor_service.dart | 2 + .../packages/appflowy_editor/pubspec.yaml | 1 + 13 files changed, 386 insertions(+), 3 deletions(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/align_center.svg create mode 100644 frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/align_left.svg create mode 100644 frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/align_right.svg create mode 100644 frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/copy.svg create mode 100644 frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/delete.svg create mode 100644 frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/divider.svg create mode 100644 frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/share.svg create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/align_center.svg b/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/align_center.svg new file mode 100644 index 0000000000..ae9c2cfd44 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/align_center.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/align_left.svg b/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/align_left.svg new file mode 100644 index 0000000000..b4f2d0101e --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/align_left.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/align_right.svg b/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/align_right.svg new file mode 100644 index 0000000000..86a1facaac --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/align_right.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/copy.svg b/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/copy.svg new file mode 100644 index 0000000000..101cf34205 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/copy.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/delete.svg b/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/delete.svg new file mode 100644 index 0000000000..5a3d972872 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/delete.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/divider.svg b/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/divider.svg new file mode 100644 index 0000000000..3e57a6b000 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/divider.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/share.svg b/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/share.svg new file mode 100644 index 0000000000..279e7ac471 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/share.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json b/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json index 901e57f796..99943dea38 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json +++ b/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json @@ -6,7 +6,8 @@ { "type": "image", "attributes": { - "image_src": "https://images.squarespace-cdn.com/content/v1/617f6f16b877c06711e87373/c3f23723-37f4-44d7-9c5d-6e2a53064ae7/Asset+10.png" + "image_src": "https://images.squarespace-cdn.com/content/v1/617f6f16b877c06711e87373/c3f23723-37f4-44d7-9c5d-6e2a53064ae7/Asset+10.png", + "align": "center" } }, { @@ -58,6 +59,13 @@ } ] }, + { + "type": "image", + "attributes": { + "image_src": "https://images.unsplash.com/photo-1616530940355-351fabd9524b?ixlib=rb-1.2.1&q=80&cs=tinysrgb&fm=jpg", + "align": "center" + } + }, { "type": "text", "delta": [ diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart index e17088c954..539bb74425 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'expandable_floating_action_button.dart'; -import 'plugin/image_node_widget.dart'; +// import 'plugin/image_node_widget.dart'; import 'plugin/youtube_link_node_widget.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -139,7 +139,7 @@ class _MyHomePageState extends State { editorState: editorState, keyEventHandlers: const [], customBuilders: { - 'image': ImageNodeBuilder(), + // 'image': ImageNodeBuilder(), 'youtube_link': YouTubeLinkNodeBuilder() }, ), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart new file mode 100644 index 0000000000..5824849540 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart @@ -0,0 +1,64 @@ +import 'package:appflowy_editor/src/document/node.dart'; +import 'package:appflowy_editor/src/operation/transaction_builder.dart'; +import 'package:appflowy_editor/src/service/render_plugin_service.dart'; +import 'package:flutter/material.dart'; +import 'package:rich_clipboard/rich_clipboard.dart'; + +import 'image_node_widget.dart'; + +class ImageNodeBuilder extends NodeWidgetBuilder { + @override + Widget build(NodeWidgetContext context) { + final src = context.node.attributes['image_src']; + final align = context.node.attributes['align']; + return ImageNodeWidget( + key: context.node.key, + src: src, + alignment: _textToAlignment(align), + onCopy: () { + RichClipboard.setData(RichClipboardData(text: src)); + }, + onDelete: () { + TransactionBuilder(context.editorState) + ..deleteNode(context.node) + ..commit(); + }, + onAlign: (alignment) { + TransactionBuilder(context.editorState) + ..updateNode(context.node, { + 'align': _alignmentToText(alignment), + }) + ..commit(); + }, + ); + } + + @override + NodeValidator get nodeValidator => ((node) { + return node.type == 'image' && + node.attributes.containsKey('image_src') && + node.attributes.containsKey('align'); + }); + + Alignment _textToAlignment(String text) { + if (text == 'center') { + return Alignment.center; + } else if (text == 'left') { + return Alignment.centerLeft; + } else if (text == 'right') { + return Alignment.centerRight; + } + throw UnimplementedError(); + } + + String _alignmentToText(Alignment alignment) { + if (alignment == Alignment.center) { + return 'center'; + } else if (alignment == Alignment.centerLeft) { + return 'left'; + } else if (alignment == Alignment.centerRight) { + return 'right'; + } + throw UnimplementedError(); + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart new file mode 100644 index 0000000000..12b22787ff --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart @@ -0,0 +1,276 @@ +import 'package:appflowy_editor/src/infra/flowy_svg.dart'; +import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; +import 'package:flutter/material.dart'; + +class ImageNodeWidget extends StatefulWidget { + const ImageNodeWidget({ + Key? key, + required this.src, + required this.alignment, + required this.onCopy, + required this.onDelete, + required this.onAlign, + }) : super(key: key); + + final String src; + final Alignment alignment; + final VoidCallback onCopy; + final VoidCallback onDelete; + final void Function(Alignment alignment) onAlign; + + @override + State createState() => _ImageNodeWidgetState(); +} + +class _ImageNodeWidgetState extends State { + double? imageWidth = defaultMaxTextNodeWidth; + double _initial = 0; + double _distance = 0; + bool _onFocus = false; + + @override + Widget build(BuildContext context) { + // only support network image. + return _buildNetworkImage(context); + } + + Widget _buildNetworkImage(BuildContext context) { + return Align( + alignment: widget.alignment, + child: MouseRegion( + onEnter: (event) => setState(() { + _onFocus = true; + }), + onExit: (event) => setState(() { + _onFocus = false; + }), + child: _buildResizableImage(context), + ), + ); + } + + Widget _buildResizableImage(BuildContext context) { + final networkImage = Image.network( + widget.src, + width: imageWidth == null ? null : imageWidth! - _distance, + loadingBuilder: (context, child, loadingProgress) => + loadingProgress == null + ? child + : SizedBox( + width: imageWidth, + height: 300, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox.fromSize( + size: const Size(18, 18), + child: const CircularProgressIndicator(), + ), + SizedBox.fromSize( + size: const Size(10, 10), + ), + const Text('Loading'), + ], + ), + ), + ); + if (imageWidth == null) { + networkImage.image.resolve(const ImageConfiguration()).addListener( + ImageStreamListener( + (image, _) { + imageWidth = image.image.width.toDouble(); + }, + ), + ); + } + return Stack( + children: [ + networkImage, + _buildEdgeGesture( + context, + top: 0, + left: 0, + bottom: 0, + width: 5, + onUpdate: (distance) { + setState(() { + _distance = distance; + }); + }, + ), + _buildEdgeGesture( + context, + top: 0, + right: 0, + bottom: 0, + width: 5, + onUpdate: (distance) { + setState(() { + _distance = -distance; + }); + }, + ), + if (_onFocus) + _buildImageToolbar( + context, + top: 8, + right: 8, + height: 30, + ), + ], + ); + } + + Widget _buildEdgeGesture( + BuildContext context, { + double? top, + double? left, + double? right, + double? bottom, + double? width, + void Function(double distance)? onUpdate, + }) { + return Positioned( + top: top, + left: left, + right: right, + bottom: bottom, + width: width, + child: GestureDetector( + onHorizontalDragStart: (details) { + _initial = details.globalPosition.dx; + }, + onHorizontalDragUpdate: (details) { + if (onUpdate != null) { + onUpdate(details.globalPosition.dx - _initial); + } + }, + onHorizontalDragEnd: (details) { + imageWidth = imageWidth! - _distance; + _initial = 0; + _distance = 0; + }, + child: MouseRegion( + cursor: SystemMouseCursors.resizeLeftRight, + child: _onFocus + ? Center( + child: Container( + height: 40, + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.2), + borderRadius: const BorderRadius.all( + Radius.circular(5.0), + ), + ), + ), + ) + : null, + ), + ), + ); + } + + Widget _buildImageToolbar( + BuildContext context, { + double? top, + double? left, + double? right, + double? width, + double? height, + }) { + return Positioned( + top: top, + left: left, + right: right, + width: width, + height: height, + child: Container( + decoration: BoxDecoration( + color: const Color(0xFF333333), + boxShadow: [ + BoxShadow( + blurRadius: 5, + spreadRadius: 1, + color: Colors.black.withOpacity(0.1), + ), + ], + borderRadius: BorderRadius.circular(8.0), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + IconButton( + hoverColor: Colors.transparent, + constraints: const BoxConstraints(), + padding: const EdgeInsets.fromLTRB(6.0, 4.0, 0.0, 4.0), + icon: FlowySvg( + name: 'image_toolbar/align_left', + color: widget.alignment == Alignment.centerLeft + ? const Color(0xFF00BCF0) + : null, + ), + onPressed: () { + widget.onAlign(Alignment.centerLeft); + }, + ), + IconButton( + hoverColor: Colors.transparent, + constraints: const BoxConstraints(), + padding: const EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 4.0), + icon: FlowySvg( + name: 'image_toolbar/align_center', + color: widget.alignment == Alignment.center + ? const Color(0xFF00BCF0) + : null, + ), + onPressed: () { + widget.onAlign(Alignment.center); + }, + ), + IconButton( + hoverColor: Colors.transparent, + constraints: const BoxConstraints(), + padding: const EdgeInsets.fromLTRB(0.0, 4.0, 4.0, 4.0), + icon: FlowySvg( + name: 'image_toolbar/align_right', + color: widget.alignment == Alignment.centerRight + ? const Color(0xFF00BCF0) + : null, + ), + onPressed: () { + widget.onAlign(Alignment.centerRight); + }, + ), + const Center( + child: FlowySvg( + name: 'image_toolbar/divider', + ), + ), + IconButton( + hoverColor: Colors.transparent, + constraints: const BoxConstraints(), + padding: const EdgeInsets.fromLTRB(4.0, 4.0, 0.0, 4.0), + icon: const FlowySvg( + name: 'image_toolbar/copy', + ), + onPressed: () { + widget.onCopy(); + }, + ), + IconButton( + hoverColor: Colors.transparent, + constraints: const BoxConstraints(), + padding: const EdgeInsets.fromLTRB(0.0, 4.0, 6.0, 4.0), + icon: const FlowySvg( + name: 'image_toolbar/delete', + ), + onPressed: () { + widget.onDelete(); + }, + ), + ], + ), + ), + ); + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart index d7b4f33914..2781471b46 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart @@ -1,3 +1,4 @@ +import 'package:appflowy_editor/src/render/image/image_node_builder.dart'; import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/default_key_event_handlers.dart'; import 'package:flutter/material.dart'; @@ -25,6 +26,7 @@ NodeWidgetBuilders defaultBuilders = { 'text/bulleted-list': BulletedListTextNodeWidgetBuilder(), 'text/number-list': NumberListTextNodeWidgetBuilder(), 'text/quote': QuotedTextNodeWidgetBuilder(), + 'image': ImageNodeBuilder(), }; class AppFlowyEditor extends StatefulWidget { diff --git a/frontend/app_flowy/packages/appflowy_editor/pubspec.yaml b/frontend/app_flowy/packages/appflowy_editor/pubspec.yaml index 33f443d066..99e431d9f1 100644 --- a/frontend/app_flowy/packages/appflowy_editor/pubspec.yaml +++ b/frontend/app_flowy/packages/appflowy_editor/pubspec.yaml @@ -32,6 +32,7 @@ flutter: assets: - assets/images/toolbar/ - assets/images/selection_menu/ + - assets/images/image_toolbar/ - assets/images/ # # For details regarding assets in packages, see From 8855822fd16a1b19f548659488f5bbf50acb9686 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 23 Aug 2022 20:34:15 +0800 Subject: [PATCH 56/86] chore: rename compare to test --- .../lib/src/extensions/text_node_extensions.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart index 3ca8c89f21..119cbae8d2 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart @@ -56,7 +56,7 @@ extension TextNodeExtension on TextNode { bool allSatisfyInSelection( String styleKey, Selection selection, - bool Function(dynamic value) compare, + bool Function(dynamic value) test, ) { final ops = delta.whereType(); final startOffset = @@ -72,7 +72,7 @@ extension TextNodeExtension on TextNode { if (start < endOffset && start + length > startOffset) { if (op.attributes == null || !op.attributes!.containsKey(styleKey) || - !compare(op.attributes![styleKey])) { + !test(op.attributes![styleKey])) { return false; } } From 012c3a851a5cccbab9cb2c6dffa34c11e6f15d29 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 23 Aug 2022 22:54:47 +0800 Subject: [PATCH 57/86] test: interge network_image_mock and add image node widget test --- .../example/assets/example.json | 2 +- .../packages/appflowy_editor/pubspec.yaml | 1 + .../test/infra/test_editor.dart | 13 ++ .../render/image/image_node_widget_test.dart | 131 ++++++++++++++++++ 4 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json b/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json index 99943dea38..e9edcfa268 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json +++ b/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json @@ -62,7 +62,7 @@ { "type": "image", "attributes": { - "image_src": "https://images.unsplash.com/photo-1616530940355-351fabd9524b?ixlib=rb-1.2.1&q=80&cs=tinysrgb&fm=jpg", + "image_src": "https://images.unsplash.com/photo-1471897488648-5eae4ac6686b?ixlib=rb-1.2.1&dl=sarah-dorweiler-QeVmJxZOv3k-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb", "align": "center" } }, diff --git a/frontend/app_flowy/packages/appflowy_editor/pubspec.yaml b/frontend/app_flowy/packages/appflowy_editor/pubspec.yaml index 99e431d9f1..9eb730f213 100644 --- a/frontend/app_flowy/packages/appflowy_editor/pubspec.yaml +++ b/frontend/app_flowy/packages/appflowy_editor/pubspec.yaml @@ -22,6 +22,7 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.0 + network_image_mock: ^2.1.1 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart index 8c89b603aa..a815d91875 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart @@ -57,6 +57,19 @@ class EditorWidgetTester { ); } + void insertImageNode(String src, {String? align}) { + insert( + Node( + type: 'image', + children: LinkedList(), + attributes: { + 'image_src': src, + 'align': align ?? 'center', + }, + ), + ); + } + Node? nodeAtPath(Path path) { return root.childAtPath(path); } diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart new file mode 100644 index 0000000000..7829c456eb --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart @@ -0,0 +1,131 @@ +import 'package:appflowy_editor/src/render/image/image_node_widget.dart'; +import 'package:appflowy_editor/src/service/editor_service.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:network_image_mock/network_image_mock.dart'; + +import '../../infra/test_editor.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('image_node_widget.dart', () { + testWidgets('render image node', (tester) async { + mockNetworkImagesFor(() async { + const text = 'Welcome to Appflowy 😁'; + const src = + 'https://images.unsplash.com/photo-1471897488648-5eae4ac6686b?ixlib=rb-1.2.1&dl=sarah-dorweiler-QeVmJxZOv3k-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb'; + final editor = tester.editor + ..insertTextNode(text) + ..insertImageNode(src) + ..insertTextNode(text); + await editor.startTesting(); + + expect(editor.documentLength, 3); + expect(find.byType(Image), findsOneWidget); + }); + }); + + testWidgets('render image align', (tester) async { + mockNetworkImagesFor(() async { + const text = 'Welcome to Appflowy 😁'; + const src = + 'https://images.unsplash.com/photo-1471897488648-5eae4ac6686b?ixlib=rb-1.2.1&dl=sarah-dorweiler-QeVmJxZOv3k-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb'; + final editor = tester.editor + ..insertTextNode(text) + ..insertImageNode(src, align: 'left') + ..insertImageNode(src, align: 'center') + ..insertImageNode(src, align: 'right') + ..insertTextNode(text); + await editor.startTesting(); + + expect(editor.documentLength, 5); + final imageFinder = find.byType(Image); + expect(imageFinder, findsNWidgets(3)); + + final editorFinder = find.byType(AppFlowyEditor); + final editorRect = tester.getRect(editorFinder); + + final leftImageRect = tester.getRect(imageFinder.at(0)); + expect(leftImageRect.left, editorRect.left); + final rightImageRect = tester.getRect(imageFinder.at(2)); + expect(rightImageRect.right, editorRect.right); + final centerImageRect = tester.getRect(imageFinder.at(1)); + expect(centerImageRect.left, + (leftImageRect.left + rightImageRect.left) / 2.0); + expect(leftImageRect.size, centerImageRect.size); + expect(rightImageRect.size, centerImageRect.size); + + final imageNodeWidgetFinder = find.byType(ImageNodeWidget); + + final leftImage = + tester.firstWidget(imageNodeWidgetFinder) as ImageNodeWidget; + + leftImage.onAlign(Alignment.center); + await tester.pump(const Duration(milliseconds: 100)); + expect( + tester.getRect(imageFinder.at(0)).left, + centerImageRect.left, + ); + + leftImage.onAlign(Alignment.centerRight); + await tester.pump(const Duration(milliseconds: 100)); + expect( + tester.getRect(imageFinder.at(0)).left, + rightImageRect.left, + ); + }); + }); + + testWidgets('render image copy', (tester) async { + mockNetworkImagesFor(() async { + const text = 'Welcome to Appflowy 😁'; + const src = + 'https://images.unsplash.com/photo-1471897488648-5eae4ac6686b?ixlib=rb-1.2.1&dl=sarah-dorweiler-QeVmJxZOv3k-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb'; + final editor = tester.editor + ..insertTextNode(text) + ..insertImageNode(src) + ..insertTextNode(text); + await editor.startTesting(); + + expect(editor.documentLength, 3); + final imageFinder = find.byType(Image); + expect(imageFinder, findsOneWidget); + + final imageNodeWidgetFinder = find.byType(ImageNodeWidget); + final image = + tester.firstWidget(imageNodeWidgetFinder) as ImageNodeWidget; + image.onCopy(); + }); + }); + + testWidgets('render image delete', (tester) async { + mockNetworkImagesFor(() async { + const text = 'Welcome to Appflowy 😁'; + const src = + 'https://images.unsplash.com/photo-1471897488648-5eae4ac6686b?ixlib=rb-1.2.1&dl=sarah-dorweiler-QeVmJxZOv3k-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb'; + final editor = tester.editor + ..insertTextNode(text) + ..insertImageNode(src) + ..insertImageNode(src) + ..insertTextNode(text); + await editor.startTesting(); + + expect(editor.documentLength, 4); + final imageFinder = find.byType(Image); + expect(imageFinder, findsNWidgets(2)); + + final imageNodeWidgetFinder = find.byType(ImageNodeWidget); + final image = + tester.firstWidget(imageNodeWidgetFinder) as ImageNodeWidget; + image.onDelete(); + + await tester.pump(const Duration(milliseconds: 100)); + expect(editor.documentLength, 3); + expect(find.byType(Image), findsNWidgets(1)); + }); + }); + }); +} From 0b553ae01355d8973edd6e0659985b44b688fb38 Mon Sep 17 00:00:00 2001 From: Pranshu Agrawal Date: Tue, 23 Aug 2022 23:20:18 +0530 Subject: [PATCH 58/86] Fix typo --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ee49d36f3..f74c1f6239 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,7 +40,7 @@ v0.0.4 - beta.1 is pre-release New features - Table-view database - - supported column types: Text, Checbox, Single-select, Multi-select, Numbers + - supported column types: Text, Checkbox, Single-select, Multi-select, Numbers - hide / delete columns - insert rows From b7297993d2093bcf0c23d5660684d7e1731919e1 Mon Sep 17 00:00:00 2001 From: Pranshu Agrawal Date: Tue, 23 Aug 2022 23:21:39 +0530 Subject: [PATCH 59/86] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f74c1f6239..06e3d600ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ - Row record open as a page - Auto resize the height of the row in the grid - Support more number formats -- Search column options, supporting Single select, Multi-select, and number format +- Search column options, supporting Single-select, Multi-select, and number format ![May-03-2022 10-03-00](https://user-images.githubusercontent.com/86001920/166394640-a8f1f3bc-5f20-4033-93e9-16bc308d7005.gif) From f5f3f51cca280cd330482a4585a62897657b0371 Mon Sep 17 00:00:00 2001 From: appflowy Date: Tue, 23 Aug 2022 20:36:28 +0800 Subject: [PATCH 60/86] chore: update column name when field was changed --- .../app_flowy/lib/plugins/board/board.dart | 2 +- .../group_entities/group_changeset.rs | 25 ++++++++++++++----- .../src/services/grid_view_editor.rs | 23 +++++++++++------ .../src/services/grid_view_manager.rs | 6 +++++ .../flowy-grid/src/services/group/action.rs | 12 +++------ .../src/services/group/controller.rs | 20 +++++++++------ .../controller_impls/checkbox_controller.rs | 16 +++--------- .../multi_select_controller.rs | 16 +++--------- .../single_select_controller.rs | 16 +++--------- .../select_option_controller/util.rs | 20 +++++++-------- .../src/services/group/group_service.rs | 15 ++++++++--- 11 files changed, 91 insertions(+), 80 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/board.dart b/frontend/app_flowy/lib/plugins/board/board.dart index c55d7f2e17..213cc8bc3c 100644 --- a/frontend/app_flowy/lib/plugins/board/board.dart +++ b/frontend/app_flowy/lib/plugins/board/board.dart @@ -31,7 +31,7 @@ class BoardPluginBuilder implements PluginBuilder { class BoardPluginConfig implements PluginConfig { @override - bool get creatable => false; + bool get creatable => true; } class BoardPlugin extends Plugin { diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs index a3ebee9cb7..b7fdda8b3d 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs @@ -1,25 +1,29 @@ use crate::entities::{GroupPB, InsertedRowPB, RowPB}; +use diesel::insertable::ColumnInsertValue::Default; use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; use std::fmt::Formatter; #[derive(Debug, Default, ProtoBuf)] -pub struct GroupRowsChangesetPB { +pub struct GroupChangesetPB { #[pb(index = 1)] pub group_id: String, - #[pb(index = 2)] - pub inserted_rows: Vec, + #[pb(index = 2, one_of)] + pub group_name: Option, #[pb(index = 3)] - pub deleted_rows: Vec, + pub inserted_rows: Vec, #[pb(index = 4)] + pub deleted_rows: Vec, + + #[pb(index = 5)] pub updated_rows: Vec, } -impl std::fmt::Display for GroupRowsChangesetPB { +impl std::fmt::Display for GroupChangesetPB { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { for inserted_row in &self.inserted_rows { let _ = f.write_fmt(format_args!( @@ -36,10 +40,19 @@ impl std::fmt::Display for GroupRowsChangesetPB { } } -impl GroupRowsChangesetPB { +impl GroupChangesetPB { pub fn is_empty(&self) -> bool { self.inserted_rows.is_empty() && self.deleted_rows.is_empty() && self.updated_rows.is_empty() } + + pub fn name(group_id: String, name: &str) -> Self { + Self { + group_id, + group_name: Some(name.to_owned()), + ..Default::default() + } + } + pub fn insert(group_id: String, inserted_rows: Vec) -> Self { Self { group_id, diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs index 4e23d9be19..015756954b 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs @@ -1,8 +1,8 @@ use crate::dart_notification::{send_dart_notification, GridNotification}; use crate::entities::{ CreateFilterParams, CreateRowParams, DeleteFilterParams, GridFilterConfiguration, GridLayout, GridLayoutPB, - GridSettingPB, GroupPB, GroupRowsChangesetPB, GroupViewChangesetPB, InsertedGroupPB, InsertedRowPB, - MoveGroupParams, RepeatedGridConfigurationFilterPB, RepeatedGridGroupConfigurationPB, RowPB, + GridSettingPB, GroupChangesetPB, GroupPB, GroupViewChangesetPB, InsertedGroupPB, InsertedRowPB, MoveGroupParams, + RepeatedGridConfigurationFilterPB, RepeatedGridGroupConfigurationPB, RowPB, }; use crate::services::grid_editor_task::GridServiceTaskScheduler; use crate::services::grid_view_manager::{GridViewFieldDelegate, GridViewRowDelegate}; @@ -99,8 +99,8 @@ impl GridViewRevisionEditor { row: row_pb.clone(), index: None, }; - let changeset = GroupRowsChangesetPB::insert(group_id.clone(), vec![inserted_row]); - self.notify_did_update_group_rows(changeset).await; + let changeset = GroupChangesetPB::insert(group_id.clone(), vec![inserted_row]); + self.notify_did_update_group(changeset).await; } } } @@ -115,7 +115,7 @@ impl GridViewRevisionEditor { .await { for changeset in changesets { - self.notify_did_update_group_rows(changeset).await; + self.notify_did_update_group(changeset).await; } } } @@ -129,7 +129,7 @@ impl GridViewRevisionEditor { .await { for changeset in changesets { - self.notify_did_update_group_rows(changeset).await; + self.notify_did_update_group(changeset).await; } } } @@ -151,7 +151,7 @@ impl GridViewRevisionEditor { .await { for changeset in changesets { - self.notify_did_update_group_rows(changeset).await; + self.notify_did_update_group(changeset).await; } } } @@ -253,7 +253,14 @@ impl GridViewRevisionEditor { .await } - async fn notify_did_update_group_rows(&self, changeset: GroupRowsChangesetPB) { + pub(crate) async fn did_update_field(&self, field_id: &str) -> FlowyResult<()> { + if let Some(field_rev) = self.field_delegate.get_field_rev(&field_id).await { + let _ = self.group_service.write().await.did_update_field(&field_rev).await?; + } + Ok(()) + } + + async fn notify_did_update_group(&self, changeset: GroupChangesetPB) { send_dart_notification(&changeset.group_id, GridNotification::DidUpdateGroup) .payload(changeset) .send(); 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 b0e578f804..b93ac2871f 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 @@ -149,6 +149,12 @@ impl GridViewManager { } } + pub(crate) async fn did_update_field(&self, field_id: &str) -> FlowyResult<()> { + let view_editor = self.get_default_view_editor().await?; + let _ = view_editor.did_update_field(field_id).await?; + Ok(()) + } + pub(crate) async fn get_view_editor(&self, view_id: &str) -> FlowyResult> { debug_assert!(!view_id.is_empty()); match self.view_editors.get(view_id) { diff --git a/frontend/rust-lib/flowy-grid/src/services/group/action.rs b/frontend/rust-lib/flowy-grid/src/services/group/action.rs index 29dc51cc37..d19be8395e 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/action.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/action.rs @@ -1,4 +1,4 @@ -use crate::entities::GroupRowsChangesetPB; +use crate::entities::GroupChangesetPB; use crate::services::group::controller::MoveGroupRowContext; use flowy_grid_data_model::revision::RowRevision; @@ -6,12 +6,8 @@ use flowy_grid_data_model::revision::RowRevision; pub trait GroupAction: Send + Sync { type CellDataType; fn can_group(&self, content: &str, cell_data: &Self::CellDataType) -> bool; - fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec; - fn remove_row_if_match( - &mut self, - row_rev: &RowRevision, - cell_data: &Self::CellDataType, - ) -> Vec; + fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec; + fn remove_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec; - fn move_row(&mut self, cell_data: &Self::CellDataType, context: MoveGroupRowContext) -> Vec; + fn move_row(&mut self, cell_data: &Self::CellDataType, context: MoveGroupRowContext) -> Vec; } 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 1a5ee23694..7cf7897dfc 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::{GroupRowsChangesetPB, RowPB}; +use crate::entities::{GroupChangesetPB, RowPB}; use crate::services::cell::{decode_any_cell_data, CellBytesParser}; use crate::services::group::action::GroupAction; use crate::services::group::configuration::GenericGroupConfiguration; @@ -51,15 +51,17 @@ pub trait GroupControllerSharedOperation: Send + Sync { &mut self, row_rev: &RowRevision, field_rev: &FieldRevision, - ) -> FlowyResult>; + ) -> FlowyResult>; fn did_delete_row( &mut self, row_rev: &RowRevision, field_rev: &FieldRevision, - ) -> FlowyResult>; + ) -> FlowyResult>; - fn move_group_row(&mut self, context: MoveGroupRowContext) -> FlowyResult>; + fn move_group_row(&mut self, context: MoveGroupRowContext) -> FlowyResult>; + + fn did_update_field(&mut self, field_rev: &FieldRevision) -> FlowyResult<()>; } /// C: represents the group configuration that impl [GroupConfigurationSerde] @@ -173,7 +175,7 @@ where &mut self, row_rev: &RowRevision, field_rev: &FieldRevision, - ) -> FlowyResult> { + ) -> FlowyResult> { 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::

()?; @@ -187,7 +189,7 @@ where &mut self, row_rev: &RowRevision, field_rev: &FieldRevision, - ) -> FlowyResult> { + ) -> FlowyResult> { 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::

()?; @@ -197,7 +199,7 @@ where } } - fn move_group_row(&mut self, context: MoveGroupRowContext) -> FlowyResult> { + fn move_group_row(&mut self, context: MoveGroupRowContext) -> FlowyResult> { if let Some(cell_rev) = context.row_rev.cells.get(&self.field_id) { let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), context.field_rev); let cell_data = cell_bytes.parser::

()?; @@ -206,6 +208,10 @@ where Ok(vec![]) } } + + fn did_update_field(&mut self, field_rev: &FieldRevision) -> FlowyResult<()> { + todo!() + } } struct GroupRow { diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs index ffcbf117fe..4c06ba63ce 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs @@ -1,4 +1,4 @@ -use crate::entities::GroupRowsChangesetPB; +use crate::entities::GroupChangesetPB; use crate::services::field::{CheckboxCellData, CheckboxCellDataParser, CheckboxTypeOptionPB, CHECK, UNCHECK}; use crate::services::group::action::GroupAction; use crate::services::group::configuration::GenericGroupConfiguration; @@ -24,11 +24,7 @@ impl GroupAction for CheckboxGroupController { false } - fn add_row_if_match( - &mut self, - _row_rev: &RowRevision, - _cell_data: &Self::CellDataType, - ) -> Vec { + fn add_row_if_match(&mut self, _row_rev: &RowRevision, _cell_data: &Self::CellDataType) -> Vec { todo!() } @@ -36,15 +32,11 @@ impl GroupAction for CheckboxGroupController { &mut self, _row_rev: &RowRevision, _cell_data: &Self::CellDataType, - ) -> Vec { + ) -> Vec { todo!() } - fn move_row( - &mut self, - _cell_data: &Self::CellDataType, - _context: MoveGroupRowContext, - ) -> Vec { + fn move_row(&mut self, _cell_data: &Self::CellDataType, _context: MoveGroupRowContext) -> Vec { todo!() } } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs index cce2698158..0c1a7117d4 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs @@ -1,4 +1,4 @@ -use crate::entities::GroupRowsChangesetPB; +use crate::entities::GroupChangesetPB; use crate::services::cell::insert_select_option_cell; use crate::services::field::{MultiSelectTypeOptionPB, SelectOptionCellDataPB, SelectOptionCellDataParser}; use crate::services::group::action::GroupAction; @@ -25,7 +25,7 @@ impl GroupAction for MultiSelectGroupController { cell_data.select_options.iter().any(|option| option.id == content) } - fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec { + fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec { let mut changesets = vec![]; self.configuration.with_mut_groups(|group| { add_row(group, &mut changesets, cell_data, row_rev); @@ -33,11 +33,7 @@ impl GroupAction for MultiSelectGroupController { changesets } - fn remove_row_if_match( - &mut self, - row_rev: &RowRevision, - cell_data: &Self::CellDataType, - ) -> Vec { + fn remove_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec { let mut changesets = vec![]; self.configuration.with_mut_groups(|group| { remove_row(group, &mut changesets, cell_data, row_rev); @@ -45,11 +41,7 @@ impl GroupAction for MultiSelectGroupController { changesets } - fn move_row( - &mut self, - cell_data: &Self::CellDataType, - mut context: MoveGroupRowContext, - ) -> Vec { + fn move_row(&mut self, cell_data: &Self::CellDataType, mut context: MoveGroupRowContext) -> Vec { let mut group_changeset = vec![]; self.configuration.with_mut_groups(|group| { move_select_option_row(group, &mut group_changeset, cell_data, &mut context); diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs index d48cdd8ee7..ad4d296fab 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs @@ -1,4 +1,4 @@ -use crate::entities::{GroupRowsChangesetPB, RowPB}; +use crate::entities::{GroupChangesetPB, RowPB}; use crate::services::cell::insert_select_option_cell; use crate::services::field::{SelectOptionCellDataPB, SelectOptionCellDataParser, SingleSelectTypeOptionPB}; use crate::services::group::action::GroupAction; @@ -25,7 +25,7 @@ impl GroupAction for SingleSelectGroupController { cell_data.select_options.iter().any(|option| option.id == content) } - fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec { + fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec { let mut changesets = vec![]; self.configuration.with_mut_groups(|group| { add_row(group, &mut changesets, cell_data, row_rev); @@ -33,11 +33,7 @@ impl GroupAction for SingleSelectGroupController { changesets } - fn remove_row_if_match( - &mut self, - row_rev: &RowRevision, - cell_data: &Self::CellDataType, - ) -> Vec { + fn remove_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec { let mut changesets = vec![]; self.configuration.with_mut_groups(|group| { remove_row(group, &mut changesets, cell_data, row_rev); @@ -45,11 +41,7 @@ impl GroupAction for SingleSelectGroupController { changesets } - fn move_row( - &mut self, - cell_data: &Self::CellDataType, - mut context: MoveGroupRowContext, - ) -> Vec { + fn move_row(&mut self, cell_data: &Self::CellDataType, mut context: MoveGroupRowContext) -> Vec { let mut group_changeset = vec![]; self.configuration.with_mut_groups(|group| { move_select_option_row(group, &mut group_changeset, cell_data, &mut context); 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 bdca688531..9a1658c19f 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 @@ -1,4 +1,4 @@ -use crate::entities::{GroupRowsChangesetPB, InsertedRowPB, RowPB}; +use crate::entities::{GroupChangesetPB, InsertedRowPB, RowPB}; use crate::services::cell::insert_select_option_cell; use crate::services::field::SelectOptionCellDataPB; use crate::services::group::configuration::GenericGroupConfiguration; @@ -11,7 +11,7 @@ pub type SelectOptionGroupConfiguration = GenericGroupConfiguration, + changesets: &mut Vec, cell_data: &SelectOptionCellDataPB, row_rev: &RowRevision, ) { @@ -19,14 +19,14 @@ pub fn add_row( if option.id == group.id { if !group.contains_row(&row_rev.id) { let row_pb = RowPB::from(row_rev); - changesets.push(GroupRowsChangesetPB::insert( + changesets.push(GroupChangesetPB::insert( group.id.clone(), vec![InsertedRowPB::new(row_pb.clone())], )); group.add_row(row_pb); } } else if group.contains_row(&row_rev.id) { - changesets.push(GroupRowsChangesetPB::delete(group.id.clone(), vec![row_rev.id.clone()])); + changesets.push(GroupChangesetPB::delete(group.id.clone(), vec![row_rev.id.clone()])); group.remove_row(&row_rev.id); } }); @@ -34,13 +34,13 @@ pub fn add_row( pub fn remove_row( group: &mut Group, - changesets: &mut Vec, + changesets: &mut Vec, cell_data: &SelectOptionCellDataPB, row_rev: &RowRevision, ) { cell_data.select_options.iter().for_each(|option| { if option.id == group.id && group.contains_row(&row_rev.id) { - changesets.push(GroupRowsChangesetPB::delete(group.id.clone(), vec![row_rev.id.clone()])); + changesets.push(GroupChangesetPB::delete(group.id.clone(), vec![row_rev.id.clone()])); group.remove_row(&row_rev.id); } }); @@ -48,7 +48,7 @@ pub fn remove_row( pub fn move_select_option_row( group: &mut Group, - group_changeset: &mut Vec, + group_changeset: &mut Vec, _cell_data: &SelectOptionCellDataPB, context: &mut MoveGroupRowContext, ) { @@ -68,7 +68,7 @@ pub fn move_select_option_row( // Remove the row in which group contains it if from_index.is_some() { - group_changeset.push(GroupRowsChangesetPB::delete(group.id.clone(), vec![row_rev.id.clone()])); + group_changeset.push(GroupChangesetPB::delete(group.id.clone(), vec![row_rev.id.clone()])); tracing::debug!("Group:{} remove row:{}", group.id, row_rev.id); group.remove_row(&row_rev.id); } @@ -78,7 +78,7 @@ pub fn move_select_option_row( let mut inserted_row = InsertedRowPB::new(row_pb.clone()); match to_index { None => { - group_changeset.push(GroupRowsChangesetPB::insert(group.id.clone(), vec![inserted_row])); + group_changeset.push(GroupChangesetPB::insert(group.id.clone(), vec![inserted_row])); tracing::debug!("Group:{} append row:{}", group.id, row_rev.id); group.add_row(row_pb); } @@ -91,7 +91,7 @@ pub fn move_select_option_row( tracing::debug!("Group:{} append row:{}", group.id, row_rev.id); group.add_row(row_pb); } - group_changeset.push(GroupRowsChangesetPB::insert(group.id.clone(), vec![inserted_row])); + group_changeset.push(GroupChangesetPB::insert(group.id.clone(), vec![inserted_row])); } } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs index 6afec9dd82..a6ae504c05 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs @@ -1,4 +1,4 @@ -use crate::entities::{FieldType, GroupRowsChangesetPB}; +use crate::entities::{FieldType, GroupChangesetPB}; use crate::services::group::configuration::GroupConfigurationReader; use crate::services::group::controller::{GroupController, MoveGroupRowContext}; use crate::services::group::{ @@ -86,7 +86,7 @@ impl GroupService { &mut self, row_rev: &RowRevision, get_field_fn: F, - ) -> Option> + ) -> Option> where F: FnOnce(String) -> O, O: Future>> + Send + Sync + 'static, @@ -111,7 +111,7 @@ impl GroupService { to_group_id: &str, to_row_id: Option, get_field_fn: F, - ) -> Option> + ) -> Option> where F: FnOnce(String) -> O, O: Future>> + Send + Sync + 'static, @@ -141,7 +141,7 @@ impl GroupService { &mut self, row_rev: &RowRevision, get_field_fn: F, - ) -> Option> + ) -> Option> where F: FnOnce(String) -> O, O: Future>> + Send + Sync + 'static, @@ -170,6 +170,13 @@ impl GroupService { } } + pub(crate) async fn did_update_field(&mut self, field_rev: &FieldRevision) -> FlowyResult<()> { + match self.group_controller.as_mut() { + None => Ok(()), + Some(group_controller) => group_controller.did_update_field(field_rev), + } + } + #[tracing::instrument(level = "trace", skip(self, field_rev), err)] async fn make_group_controller( &self, From 3832af5fa868e07007bed37db6830a3bc0362cb5 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 24 Aug 2022 12:02:50 +0800 Subject: [PATCH 61/86] test: add test for image_node_widget and image_node_builder --- .../src/render/image/image_node_builder.dart | 12 +- .../src/render/image/image_node_widget.dart | 99 +++++++----- .../render/image/image_node_builder_test.dart | 131 +++++++++++++++ .../render/image/image_node_widget_test.dart | 153 ++++++------------ 4 files changed, 245 insertions(+), 150 deletions(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_builder_test.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart index 5824849540..cab86a5dc4 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart @@ -41,24 +41,20 @@ class ImageNodeBuilder extends NodeWidgetBuilder { }); Alignment _textToAlignment(String text) { - if (text == 'center') { - return Alignment.center; - } else if (text == 'left') { + if (text == 'left') { return Alignment.centerLeft; } else if (text == 'right') { return Alignment.centerRight; } - throw UnimplementedError(); + return Alignment.center; } String _alignmentToText(Alignment alignment) { - if (alignment == Alignment.center) { - return 'center'; - } else if (alignment == Alignment.centerLeft) { + if (alignment == Alignment.centerLeft) { return 'left'; } else if (alignment == Alignment.centerRight) { return 'right'; } - throw UnimplementedError(); + return 'center'; } } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart index 12b22787ff..f5ffdeb4ce 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart @@ -54,25 +54,7 @@ class _ImageNodeWidgetState extends State { widget.src, width: imageWidth == null ? null : imageWidth! - _distance, loadingBuilder: (context, child, loadingProgress) => - loadingProgress == null - ? child - : SizedBox( - width: imageWidth, - height: 300, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox.fromSize( - size: const Size(18, 18), - child: const CircularProgressIndicator(), - ), - SizedBox.fromSize( - size: const Size(10, 10), - ), - const Text('Loading'), - ], - ), - ), + loadingProgress == null ? child : _buildLoading(context), ); if (imageWidth == null) { networkImage.image.resolve(const ImageConfiguration()).addListener( @@ -111,16 +93,39 @@ class _ImageNodeWidgetState extends State { }, ), if (_onFocus) - _buildImageToolbar( - context, + ImageToolbar( top: 8, right: 8, height: 30, - ), + alignment: widget.alignment, + onAlign: widget.onAlign, + onCopy: widget.onCopy, + onDelete: widget.onDelete, + ) ], ); } + Widget _buildLoading(BuildContext context) { + return SizedBox( + width: imageWidth, + height: 300, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox.fromSize( + size: const Size(18, 18), + child: const CircularProgressIndicator(), + ), + SizedBox.fromSize( + size: const Size(10, 10), + ), + const Text('Loading'), + ], + ), + ); + } + Widget _buildEdgeGesture( BuildContext context, { double? top, @@ -169,20 +174,34 @@ class _ImageNodeWidgetState extends State { ), ); } +} - Widget _buildImageToolbar( - BuildContext context, { - double? top, - double? left, - double? right, - double? width, - double? height, - }) { +@visibleForTesting +class ImageToolbar extends StatelessWidget { + const ImageToolbar({ + Key? key, + required this.top, + required this.right, + required this.height, + required this.alignment, + required this.onCopy, + required this.onDelete, + required this.onAlign, + }) : super(key: key); + + final double top; + final double right; + final double height; + final Alignment alignment; + final VoidCallback onCopy; + final VoidCallback onDelete; + final void Function(Alignment alignment) onAlign; + + @override + Widget build(BuildContext context) { return Positioned( top: top, - left: left, right: right, - width: width, height: height, child: Container( decoration: BoxDecoration( @@ -205,12 +224,12 @@ class _ImageNodeWidgetState extends State { padding: const EdgeInsets.fromLTRB(6.0, 4.0, 0.0, 4.0), icon: FlowySvg( name: 'image_toolbar/align_left', - color: widget.alignment == Alignment.centerLeft + color: alignment == Alignment.centerLeft ? const Color(0xFF00BCF0) : null, ), onPressed: () { - widget.onAlign(Alignment.centerLeft); + onAlign(Alignment.centerLeft); }, ), IconButton( @@ -219,12 +238,12 @@ class _ImageNodeWidgetState extends State { padding: const EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 4.0), icon: FlowySvg( name: 'image_toolbar/align_center', - color: widget.alignment == Alignment.center + color: alignment == Alignment.center ? const Color(0xFF00BCF0) : null, ), onPressed: () { - widget.onAlign(Alignment.center); + onAlign(Alignment.center); }, ), IconButton( @@ -233,12 +252,12 @@ class _ImageNodeWidgetState extends State { padding: const EdgeInsets.fromLTRB(0.0, 4.0, 4.0, 4.0), icon: FlowySvg( name: 'image_toolbar/align_right', - color: widget.alignment == Alignment.centerRight + color: alignment == Alignment.centerRight ? const Color(0xFF00BCF0) : null, ), onPressed: () { - widget.onAlign(Alignment.centerRight); + onAlign(Alignment.centerRight); }, ), const Center( @@ -254,7 +273,7 @@ class _ImageNodeWidgetState extends State { name: 'image_toolbar/copy', ), onPressed: () { - widget.onCopy(); + onCopy(); }, ), IconButton( @@ -265,7 +284,7 @@ class _ImageNodeWidgetState extends State { name: 'image_toolbar/delete', ), onPressed: () { - widget.onDelete(); + onDelete(); }, ), ], diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_builder_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_builder_test.dart new file mode 100644 index 0000000000..9121fa1868 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_builder_test.dart @@ -0,0 +1,131 @@ +import 'package:appflowy_editor/src/render/image/image_node_widget.dart'; +import 'package:appflowy_editor/src/service/editor_service.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:network_image_mock/network_image_mock.dart'; + +import '../../infra/test_editor.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('image_node_builder.dart', () { + testWidgets('render image node', (tester) async { + mockNetworkImagesFor(() async { + const text = 'Welcome to Appflowy 😁'; + const src = + 'https://images.unsplash.com/photo-1471897488648-5eae4ac6686b?ixlib=rb-1.2.1&dl=sarah-dorweiler-QeVmJxZOv3k-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb'; + final editor = tester.editor + ..insertTextNode(text) + ..insertImageNode(src) + ..insertTextNode(text); + await editor.startTesting(); + + expect(editor.documentLength, 3); + expect(find.byType(Image), findsOneWidget); + }); + }); + + testWidgets('render image align', (tester) async { + mockNetworkImagesFor(() async { + const text = 'Welcome to Appflowy 😁'; + const src = + 'https://images.unsplash.com/photo-1471897488648-5eae4ac6686b?ixlib=rb-1.2.1&dl=sarah-dorweiler-QeVmJxZOv3k-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb'; + final editor = tester.editor + ..insertTextNode(text) + ..insertImageNode(src, align: 'left') + ..insertImageNode(src, align: 'center') + ..insertImageNode(src, align: 'right') + ..insertTextNode(text); + await editor.startTesting(); + + expect(editor.documentLength, 5); + final imageFinder = find.byType(Image); + expect(imageFinder, findsNWidgets(3)); + + final editorFinder = find.byType(AppFlowyEditor); + final editorRect = tester.getRect(editorFinder); + + final leftImageRect = tester.getRect(imageFinder.at(0)); + expect(leftImageRect.left, editorRect.left); + final rightImageRect = tester.getRect(imageFinder.at(2)); + expect(rightImageRect.right, editorRect.right); + final centerImageRect = tester.getRect(imageFinder.at(1)); + expect(centerImageRect.left, + (leftImageRect.left + rightImageRect.left) / 2.0); + expect(leftImageRect.size, centerImageRect.size); + expect(rightImageRect.size, centerImageRect.size); + + final imageNodeWidgetFinder = find.byType(ImageNodeWidget); + + final leftImage = + tester.firstWidget(imageNodeWidgetFinder) as ImageNodeWidget; + + leftImage.onAlign(Alignment.center); + await tester.pump(const Duration(milliseconds: 100)); + expect( + tester.getRect(imageFinder.at(0)).left, + centerImageRect.left, + ); + + leftImage.onAlign(Alignment.centerRight); + await tester.pump(const Duration(milliseconds: 100)); + expect( + tester.getRect(imageFinder.at(0)).left, + rightImageRect.left, + ); + }); + }); + + testWidgets('render image copy', (tester) async { + mockNetworkImagesFor(() async { + const text = 'Welcome to Appflowy 😁'; + const src = + 'https://images.unsplash.com/photo-1471897488648-5eae4ac6686b?ixlib=rb-1.2.1&dl=sarah-dorweiler-QeVmJxZOv3k-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb'; + final editor = tester.editor + ..insertTextNode(text) + ..insertImageNode(src) + ..insertTextNode(text); + await editor.startTesting(); + + expect(editor.documentLength, 3); + final imageFinder = find.byType(Image); + expect(imageFinder, findsOneWidget); + + final imageNodeWidgetFinder = find.byType(ImageNodeWidget); + final image = + tester.firstWidget(imageNodeWidgetFinder) as ImageNodeWidget; + image.onCopy(); + }); + }); + + testWidgets('render image delete', (tester) async { + mockNetworkImagesFor(() async { + const text = 'Welcome to Appflowy 😁'; + const src = + 'https://images.unsplash.com/photo-1471897488648-5eae4ac6686b?ixlib=rb-1.2.1&dl=sarah-dorweiler-QeVmJxZOv3k-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb'; + final editor = tester.editor + ..insertTextNode(text) + ..insertImageNode(src) + ..insertImageNode(src) + ..insertTextNode(text); + await editor.startTesting(); + + expect(editor.documentLength, 4); + final imageFinder = find.byType(Image); + expect(imageFinder, findsNWidgets(2)); + + final imageNodeWidgetFinder = find.byType(ImageNodeWidget); + final image = + tester.firstWidget(imageNodeWidgetFinder) as ImageNodeWidget; + image.onDelete(); + + await tester.pump(const Duration(milliseconds: 100)); + expect(editor.documentLength, 3); + expect(find.byType(Image), findsNWidgets(1)); + }); + }); + }); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart index 7829c456eb..4cb68a488f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart @@ -1,130 +1,79 @@ import 'package:appflowy_editor/src/render/image/image_node_widget.dart'; -import 'package:appflowy_editor/src/service/editor_service.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:network_image_mock/network_image_mock.dart'; -import '../../infra/test_editor.dart'; - void main() async { setUpAll(() { TestWidgetsFlutterBinding.ensureInitialized(); }); group('image_node_widget.dart', () { - testWidgets('render image node', (tester) async { + testWidgets('build the image node widget', (tester) async { mockNetworkImagesFor(() async { - const text = 'Welcome to Appflowy 😁'; + var onCopyHit = false; + var onDeleteHit = false; + var onAlignHit = false; const src = 'https://images.unsplash.com/photo-1471897488648-5eae4ac6686b?ixlib=rb-1.2.1&dl=sarah-dorweiler-QeVmJxZOv3k-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb'; - final editor = tester.editor - ..insertTextNode(text) - ..insertImageNode(src) - ..insertTextNode(text); - await editor.startTesting(); - expect(editor.documentLength, 3); - expect(find.byType(Image), findsOneWidget); - }); - }); - - testWidgets('render image align', (tester) async { - mockNetworkImagesFor(() async { - const text = 'Welcome to Appflowy 😁'; - const src = - 'https://images.unsplash.com/photo-1471897488648-5eae4ac6686b?ixlib=rb-1.2.1&dl=sarah-dorweiler-QeVmJxZOv3k-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb'; - final editor = tester.editor - ..insertTextNode(text) - ..insertImageNode(src, align: 'left') - ..insertImageNode(src, align: 'center') - ..insertImageNode(src, align: 'right') - ..insertTextNode(text); - await editor.startTesting(); - - expect(editor.documentLength, 5); - final imageFinder = find.byType(Image); - expect(imageFinder, findsNWidgets(3)); - - final editorFinder = find.byType(AppFlowyEditor); - final editorRect = tester.getRect(editorFinder); - - final leftImageRect = tester.getRect(imageFinder.at(0)); - expect(leftImageRect.left, editorRect.left); - final rightImageRect = tester.getRect(imageFinder.at(2)); - expect(rightImageRect.right, editorRect.right); - final centerImageRect = tester.getRect(imageFinder.at(1)); - expect(centerImageRect.left, - (leftImageRect.left + rightImageRect.left) / 2.0); - expect(leftImageRect.size, centerImageRect.size); - expect(rightImageRect.size, centerImageRect.size); - - final imageNodeWidgetFinder = find.byType(ImageNodeWidget); - - final leftImage = - tester.firstWidget(imageNodeWidgetFinder) as ImageNodeWidget; - - leftImage.onAlign(Alignment.center); - await tester.pump(const Duration(milliseconds: 100)); - expect( - tester.getRect(imageFinder.at(0)).left, - centerImageRect.left, + final widget = ImageNodeWidget( + src: src, + alignment: Alignment.center, + onCopy: () { + onCopyHit = true; + }, + onDelete: () { + onDeleteHit = true; + }, + onAlign: (alignment) { + onAlignHit = true; + }, ); - leftImage.onAlign(Alignment.centerRight); - await tester.pump(const Duration(milliseconds: 100)); - expect( - tester.getRect(imageFinder.at(0)).left, - rightImageRect.left, + await tester.pumpWidget( + MaterialApp( + home: Material( + child: widget, + ), + ), ); - }); - }); + expect(find.byType(ImageNodeWidget), findsOneWidget); - testWidgets('render image copy', (tester) async { - mockNetworkImagesFor(() async { - const text = 'Welcome to Appflowy 😁'; - const src = - 'https://images.unsplash.com/photo-1471897488648-5eae4ac6686b?ixlib=rb-1.2.1&dl=sarah-dorweiler-QeVmJxZOv3k-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb'; - final editor = tester.editor - ..insertTextNode(text) - ..insertImageNode(src) - ..insertTextNode(text); - await editor.startTesting(); + final gesture = + await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(location: Offset.zero); - expect(editor.documentLength, 3); - final imageFinder = find.byType(Image); - expect(imageFinder, findsOneWidget); + expect(find.byType(ImageToolbar), findsNothing); - final imageNodeWidgetFinder = find.byType(ImageNodeWidget); - final image = - tester.firstWidget(imageNodeWidgetFinder) as ImageNodeWidget; - image.onCopy(); - }); - }); + addTearDown(gesture.removePointer); + await tester.pump(); + await gesture.moveTo(tester.getCenter(find.byType(ImageNodeWidget))); + await tester.pump(); - testWidgets('render image delete', (tester) async { - mockNetworkImagesFor(() async { - const text = 'Welcome to Appflowy 😁'; - const src = - 'https://images.unsplash.com/photo-1471897488648-5eae4ac6686b?ixlib=rb-1.2.1&dl=sarah-dorweiler-QeVmJxZOv3k-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb'; - final editor = tester.editor - ..insertTextNode(text) - ..insertImageNode(src) - ..insertImageNode(src) - ..insertTextNode(text); - await editor.startTesting(); + expect(find.byType(ImageToolbar), findsOneWidget); - expect(editor.documentLength, 4); - final imageFinder = find.byType(Image); - expect(imageFinder, findsNWidgets(2)); + final iconFinder = find.byType(IconButton); + expect(iconFinder, findsNWidgets(5)); - final imageNodeWidgetFinder = find.byType(ImageNodeWidget); - final image = - tester.firstWidget(imageNodeWidgetFinder) as ImageNodeWidget; - image.onDelete(); + await tester.tap(iconFinder.at(0)); + expect(onAlignHit, true); + onAlignHit = false; - await tester.pump(const Duration(milliseconds: 100)); - expect(editor.documentLength, 3); - expect(find.byType(Image), findsNWidgets(1)); + await tester.tap(iconFinder.at(1)); + expect(onAlignHit, true); + onAlignHit = false; + + await tester.tap(iconFinder.at(2)); + expect(onAlignHit, true); + onAlignHit = false; + + await tester.tap(iconFinder.at(3)); + expect(onCopyHit, true); + + await tester.tap(iconFinder.at(4)); + expect(onDeleteHit, true); }); }); }); From a309a9c82cc2ed04059071edd6f65b710d9a1157 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 24 Aug 2022 11:50:06 +0800 Subject: [PATCH 62/86] fix: minor issues --- .../lib/src/operation/operation.dart | 18 ++++++++-------- .../test/legacy/operation_test.dart | 4 ++-- .../src/core/document/document_operation.rs | 21 ++++++++----------- .../lib-ot/src/core/document/position.rs | 4 ++-- 4 files changed, 22 insertions(+), 25 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/operation.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/operation.dart index 456be06766..af2ec831d4 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/operation.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/operation.dart @@ -2,14 +2,14 @@ import 'package:appflowy_editor/appflowy_editor.dart'; abstract class Operation { factory Operation.fromJson(Map map) { - String t = map["type"] as String; - if (t == "insert-operation") { + String t = map["op"] as String; + if (t == "insert") { return InsertOperation.fromJson(map); - } else if (t == "update-operation") { + } else if (t == "update") { return UpdateOperation.fromJson(map); - } else if (t == "delete-operation") { + } else if (t == "delete") { return DeleteOperation.fromJson(map); - } else if (t == "text-edit-operation") { + } else if (t == "text-edit") { return TextEditOperation.fromJson(map); } @@ -51,7 +51,7 @@ class InsertOperation extends Operation { @override Map toJson() { return { - "type": "insert-operation", + "op": "insert", "path": path.toList(), "nodes": nodes.map((n) => n.toJson()), }; @@ -95,7 +95,7 @@ class UpdateOperation extends Operation { @override Map toJson() { return { - "type": "update-operation", + "op": "update", "path": path.toList(), "attributes": {...attributes}, "oldAttributes": {...oldAttributes}, @@ -132,7 +132,7 @@ class DeleteOperation extends Operation { @override Map toJson() { return { - "type": "delete-operation", + "op": "delete", "path": path.toList(), "nodes": nodes.map((n) => n.toJson()), }; @@ -171,7 +171,7 @@ class TextEditOperation extends Operation { @override Map toJson() { return { - "type": "text-edit-operation", + "op": "text-edit", "path": path.toList(), "delta": delta.toJson(), "invert": inverted.toJson(), diff --git a/frontend/app_flowy/packages/appflowy_editor/test/legacy/operation_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/legacy/operation_test.dart index 1f44ebfd3c..6c20ebd134 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/legacy/operation_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/legacy/operation_test.dart @@ -84,7 +84,7 @@ void main() { expect(transaction.toJson(), { "operations": [ { - "type": "insert-operation", + "op": "insert", "path": [0], "nodes": [item1.toJson()], } @@ -107,7 +107,7 @@ void main() { expect(transaction.toJson(), { "operations": [ { - "type": "delete-operation", + "op": "delete", "path": [0], "nodes": [item1.toJson()], } diff --git a/shared-lib/lib-ot/src/core/document/document_operation.rs b/shared-lib/lib-ot/src/core/document/document_operation.rs index caca29e110..4d9d3617eb 100644 --- a/shared-lib/lib-ot/src/core/document/document_operation.rs +++ b/shared-lib/lib-ot/src/core/document/document_operation.rs @@ -2,26 +2,26 @@ use crate::core::document::position::Position; use crate::core::{NodeAttributes, NodeSubTree, TextDelta}; #[derive(Clone, serde::Serialize, serde::Deserialize)] -#[serde(tag = "type")] +#[serde(tag = "op")] pub enum DocumentOperation { - #[serde(rename = "insert-operation")] + #[serde(rename = "insert")] Insert { path: Position, nodes: Vec>, }, - #[serde(rename = "update-operation")] + #[serde(rename = "update")] Update { path: Position, attributes: NodeAttributes, #[serde(rename = "oldAttributes")] old_attributes: NodeAttributes, }, - #[serde(rename = "delete-operation")] + #[serde(rename = "delete")] Delete { path: Position, nodes: Vec>, }, - #[serde(rename = "text-edit-operation")] + #[serde(rename = "text-edit")] TextEdit { path: Position, delta: TextDelta, @@ -166,7 +166,7 @@ mod tests { let result = serde_json::to_string(&insert).unwrap(); assert_eq!( result, - r#"{"type":"insert-operation","path":[0,1],"nodes":[{"type":"text","attributes":{}}]}"# + r#"{"op":"insert","path":[0,1],"nodes":[{"type":"text","attributes":{}}]}"# ); } @@ -184,7 +184,7 @@ mod tests { let result = serde_json::to_string(&insert).unwrap(); assert_eq!( result, - r#"{"type":"insert-operation","path":[0,1],"nodes":[{"type":"text","attributes":{},"children":[{"type":"text","attributes":{}}]}]}"# + r#"{"op":"insert","path":[0,1],"nodes":[{"type":"text","attributes":{},"children":[{"type":"text","attributes":{}}]}]}"# ); } @@ -198,7 +198,7 @@ mod tests { let result = serde_json::to_string(&insert).unwrap(); assert_eq!( result, - r#"{"type":"update-operation","path":[0,1],"attributes":{},"oldAttributes":{}}"# + r#"{"op":"update","path":[0,1],"attributes":{},"oldAttributes":{}}"# ); } @@ -210,9 +210,6 @@ mod tests { inverted: Delta::new(), }; let result = serde_json::to_string(&insert).unwrap(); - assert_eq!( - result, - r#"{"type":"text-edit-operation","path":[0,1],"delta":[],"inverted":[]}"# - ); + assert_eq!(result, r#"{"op":"text-edit","path":[0,1],"delta":[],"inverted":[]}"#); } } diff --git a/shared-lib/lib-ot/src/core/document/position.rs b/shared-lib/lib-ot/src/core/document/position.rs index 521d0e9a60..b98edd97f4 100644 --- a/shared-lib/lib-ot/src/core/document/position.rs +++ b/shared-lib/lib-ot/src/core/document/position.rs @@ -12,7 +12,7 @@ impl Position { impl Position { // delta is default to be 1 - pub fn transform(pre_insert_path: &Position, b: &Position, delta: i64) -> Position { + pub fn transform(pre_insert_path: &Position, b: &Position, offset: i64) -> Position { if pre_insert_path.len() > b.len() { return b.clone(); } @@ -30,7 +30,7 @@ impl Position { let prev_insert_last: usize = *pre_insert_path.0.last().unwrap(); let b_at_index = b.0[pre_insert_path.0.len() - 1]; if prev_insert_last <= b_at_index { - prefix.push(((b_at_index as i64) + delta) as usize); + prefix.push(((b_at_index as i64) + offset) as usize); } else { prefix.push(b_at_index); } From 6af85fbe56b809e94df4b8f3f12c41753feb14dc Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 24 Aug 2022 15:42:01 +0800 Subject: [PATCH 63/86] feat: add image entry into selection menu --- .../assets/images/selection_menu/image.svg | 5 + .../example/assets/example.json | 2 +- .../src/render/image/image_node_builder.dart | 9 + .../src/render/image/image_node_widget.dart | 47 +++-- .../src/render/image/image_upload_widget.dart | 194 ++++++++++++++++++ .../selection_menu_item_widget.dart | 2 +- .../selection_menu_service.dart | 26 ++- .../selection_menu/selection_menu_widget.dart | 9 +- .../render/image/image_node_widget_test.dart | 1 + .../selection_menu_item_widget_test.dart | 2 +- 10 files changed, 270 insertions(+), 27 deletions(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/assets/images/selection_menu/image.svg create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_upload_widget.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/assets/images/selection_menu/image.svg b/frontend/app_flowy/packages/appflowy_editor/assets/images/selection_menu/image.svg new file mode 100644 index 0000000000..0e2aafe0ec --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/assets/images/selection_menu/image.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json b/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json index e9edcfa268..c6b27d9ae1 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json +++ b/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json @@ -62,7 +62,7 @@ { "type": "image", "attributes": { - "image_src": "https://images.unsplash.com/photo-1471897488648-5eae4ac6686b?ixlib=rb-1.2.1&dl=sarah-dorweiler-QeVmJxZOv3k-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb", + "image_src": "https://s1.ax1x.com/2022/08/24/vgAJED.png", "align": "center" } }, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart index cab86a5dc4..f62378fc1a 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart @@ -11,9 +11,11 @@ class ImageNodeBuilder extends NodeWidgetBuilder { Widget build(NodeWidgetContext context) { final src = context.node.attributes['image_src']; final align = context.node.attributes['align']; + final width = context.node.attributes['width']; return ImageNodeWidget( key: context.node.key, src: src, + width: width, alignment: _textToAlignment(align), onCopy: () { RichClipboard.setData(RichClipboardData(text: src)); @@ -30,6 +32,13 @@ class ImageNodeBuilder extends NodeWidgetBuilder { }) ..commit(); }, + onResize: (width) { + TransactionBuilder(context.editorState) + ..updateNode(context.node, { + 'width': width, + }) + ..commit(); + }, ); } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart index f5ffdeb4ce..110cff01b3 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart @@ -1,33 +1,57 @@ import 'package:appflowy_editor/src/infra/flowy_svg.dart'; -import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; import 'package:flutter/material.dart'; class ImageNodeWidget extends StatefulWidget { const ImageNodeWidget({ Key? key, required this.src, + this.width, required this.alignment, required this.onCopy, required this.onDelete, required this.onAlign, + required this.onResize, }) : super(key: key); final String src; + final double? width; final Alignment alignment; final VoidCallback onCopy; final VoidCallback onDelete; final void Function(Alignment alignment) onAlign; + final void Function(double width) onResize; @override State createState() => _ImageNodeWidgetState(); } class _ImageNodeWidgetState extends State { - double? imageWidth = defaultMaxTextNodeWidth; + double? _imageWidth; double _initial = 0; double _distance = 0; bool _onFocus = false; + ImageStream? _imageStream; + late ImageStreamListener _imageStreamListener; + + @override + void initState() { + super.initState(); + + _imageWidth = widget.width; + _imageStreamListener = ImageStreamListener( + (image, _) { + _imageWidth = image.image.width.toDouble(); + }, + ); + } + + @override + void dispose() { + _imageStream?.removeListener(_imageStreamListener); + super.dispose(); + } + @override Widget build(BuildContext context) { // only support network image. @@ -52,18 +76,13 @@ class _ImageNodeWidgetState extends State { Widget _buildResizableImage(BuildContext context) { final networkImage = Image.network( widget.src, - width: imageWidth == null ? null : imageWidth! - _distance, + width: _imageWidth == null ? null : _imageWidth! - _distance, loadingBuilder: (context, child, loadingProgress) => loadingProgress == null ? child : _buildLoading(context), ); - if (imageWidth == null) { - networkImage.image.resolve(const ImageConfiguration()).addListener( - ImageStreamListener( - (image, _) { - imageWidth = image.image.width.toDouble(); - }, - ), - ); + if (_imageWidth == null) { + _imageStream = networkImage.image.resolve(const ImageConfiguration()) + ..addListener(_imageStreamListener); } return Stack( children: [ @@ -108,7 +127,7 @@ class _ImageNodeWidgetState extends State { Widget _buildLoading(BuildContext context) { return SizedBox( - width: imageWidth, + width: _imageWidth, height: 300, child: Row( mainAxisAlignment: MainAxisAlignment.center, @@ -151,9 +170,11 @@ class _ImageNodeWidgetState extends State { } }, onHorizontalDragEnd: (details) { - imageWidth = imageWidth! - _distance; + _imageWidth = _imageWidth! - _distance; _initial = 0; _distance = 0; + + widget.onResize(_imageWidth!); }, child: MouseRegion( cursor: SystemMouseCursors.resizeLeftRight, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_upload_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_upload_widget.dart new file mode 100644 index 0000000000..c7353532ab --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_upload_widget.dart @@ -0,0 +1,194 @@ +import 'dart:collection'; + +import 'package:appflowy_editor/src/document/node.dart'; +import 'package:appflowy_editor/src/editor_state.dart'; +import 'package:appflowy_editor/src/infra/flowy_svg.dart'; +import 'package:appflowy_editor/src/operation/transaction_builder.dart'; +import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service.dart'; +import 'package:flutter/material.dart'; + +OverlayEntry? _imageUploadMenu; +void showImageUploadMenu( + EditorState editorState, + SelectionMenuService menuService, + BuildContext context, +) { + menuService.dismiss(); + + _imageUploadMenu?.remove(); + _imageUploadMenu = OverlayEntry(builder: (context) { + return Positioned( + top: menuService.topLeft.dy, + left: menuService.topLeft.dx, + child: Material( + child: ImageUploadMenu( + onSubmitted: (text) { + _dismissImageUploadMenu(); + editorState.insertImageNode(text); + }, + onUpload: (text) { + _dismissImageUploadMenu(); + editorState.insertImageNode(text); + }, + ), + ), + ); + }); + + Overlay.of(context)?.insert(_imageUploadMenu!); +} + +void _dismissImageUploadMenu() { + _imageUploadMenu?.remove(); + _imageUploadMenu = null; +} + +class ImageUploadMenu extends StatefulWidget { + const ImageUploadMenu({ + Key? key, + required this.onSubmitted, + required this.onUpload, + }) : super(key: key); + + final void Function(String text) onSubmitted; + final void Function(String text) onUpload; + + @override + State createState() => _ImageUploadMenuState(); +} + +class _ImageUploadMenuState extends State { + final _textEditingController = TextEditingController(); + final _focusNode = FocusNode(); + + @override + void initState() { + super.initState(); + _focusNode.requestFocus(); + } + + @override + void dispose() { + _focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + width: 300, + padding: const EdgeInsets.all(24.0), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + blurRadius: 5, + spreadRadius: 1, + color: Colors.black.withOpacity(0.1), + ), + ], + borderRadius: BorderRadius.circular(6.0), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeader(context), + const SizedBox(height: 16.0), + _buildInput(), + const SizedBox(height: 18.0), + _buildUploadButton(context), + ], + ), + ); + } + + Widget _buildHeader(BuildContext context) { + return const Text( + 'URL Image', + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 14.0, + color: Colors.black, + fontWeight: FontWeight.w500, + ), + ); + } + + Widget _buildInput() { + return TextField( + focusNode: _focusNode, + style: const TextStyle(fontSize: 14.0), + textAlign: TextAlign.left, + controller: _textEditingController, + onSubmitted: widget.onSubmitted, + decoration: InputDecoration( + hintText: 'URL', + hintStyle: const TextStyle(fontSize: 14.0), + contentPadding: const EdgeInsets.all(16.0), + isDense: true, + suffixIcon: IconButton( + padding: const EdgeInsets.all(4.0), + icon: const FlowySvg( + name: 'clear', + width: 24, + height: 24, + ), + onPressed: () { + _textEditingController.clear(); + }, + ), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + borderSide: BorderSide(color: Color(0xFFBDBDBD)), + ), + ), + ); + } + + Widget _buildUploadButton(BuildContext context) { + return SizedBox( + width: 170, + height: 48, + child: TextButton( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all(const Color(0xFF00BCF0)), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), + ), + ), + onPressed: () { + widget.onUpload(_textEditingController.text); + }, + child: const Text( + 'Upload', + style: TextStyle(color: Colors.white, fontSize: 14.0), + ), + ), + ); + } +} + +extension on EditorState { + void insertImageNode(String src) { + final selection = service.selectionService.currentSelection.value; + if (selection == null) { + return; + } + final imageNode = Node( + type: 'image', + children: LinkedList(), + attributes: { + 'image_src': src, + 'align': 'center', + }, + ); + TransactionBuilder(this) + ..insertNode( + selection.start.path, + imageNode, + ) + ..commit(); + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_item_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_item_widget.dart index 36e0a2e02e..3b7307f039 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_item_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_item_widget.dart @@ -45,7 +45,7 @@ class SelectionMenuItemWidget extends StatelessWidget { ), ), onPressed: () { - item.handler(editorState, menuService); + item.handler(editorState, menuService, context); }, ), ), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart index 94fa6190d8..7f4f803610 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart @@ -1,5 +1,6 @@ import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/infra/flowy_svg.dart'; +import 'package:appflowy_editor/src/render/image/image_upload_widget.dart'; import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart'; import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart'; @@ -23,6 +24,7 @@ class SelectionMenu implements SelectionMenuService { OverlayEntry? _selectionMenuEntry; bool _selectionUpdateByInner = false; + Offset? _topLeft; @override void dismiss() { @@ -53,6 +55,7 @@ class SelectionMenu implements SelectionMenuService { return; } final offset = selectionRects.first.bottomRight + const Offset(10, 10); + _topLeft = offset; _selectionMenuEntry = OverlayEntry(builder: (context) { return Positioned( @@ -84,8 +87,9 @@ class SelectionMenu implements SelectionMenuService { } @override - // TODO: implement topLeft - Offset get topLeft => throw UnimplementedError(); + Offset get topLeft { + return _topLeft ?? Offset.zero; + } void _onSelectionChange() { // workaround: SelectionService has been released after hot reload. @@ -115,7 +119,7 @@ final List _defaultSelectionMenuItems = [ name: 'Text', icon: _selectionMenuIcon('text'), keywords: ['text'], - handler: (editorState, menuService) { + handler: (editorState, _, __) { insertTextNodeAfterSelection(editorState, {}); }, ), @@ -123,7 +127,7 @@ final List _defaultSelectionMenuItems = [ name: 'Heading 1', icon: _selectionMenuIcon('h1'), keywords: ['heading 1, h1'], - handler: (editorState, menuService) { + handler: (editorState, _, __) { insertHeadingAfterSelection(editorState, StyleKey.h1); }, ), @@ -131,7 +135,7 @@ final List _defaultSelectionMenuItems = [ name: 'Heading 2', icon: _selectionMenuIcon('h2'), keywords: ['heading 2, h2'], - handler: (editorState, menuService) { + handler: (editorState, _, __) { insertHeadingAfterSelection(editorState, StyleKey.h2); }, ), @@ -139,15 +143,21 @@ final List _defaultSelectionMenuItems = [ name: 'Heading 3', icon: _selectionMenuIcon('h3'), keywords: ['heading 3, h3'], - handler: (editorState, menuService) { + handler: (editorState, _, __) { insertHeadingAfterSelection(editorState, StyleKey.h3); }, ), + SelectionMenuItem( + name: 'Image', + icon: _selectionMenuIcon('image'), + keywords: ['image'], + handler: showImageUploadMenu, + ), SelectionMenuItem( name: 'Bulleted list', icon: _selectionMenuIcon('bulleted_list'), keywords: ['bulleted list', 'list', 'unordered list'], - handler: (editorState, menuService) { + handler: (editorState, _, __) { insertBulletedListAfterSelection(editorState); }, ), @@ -155,7 +165,7 @@ final List _defaultSelectionMenuItems = [ name: 'Checkbox', icon: _selectionMenuIcon('checkbox'), keywords: ['todo list', 'list', 'checkbox list'], - handler: (editorState, menuService) { + handler: (editorState, _, __) { insertCheckboxAfterSelection(editorState); }, ), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart index 70f7bbc337..f73251081f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart @@ -22,8 +22,11 @@ class SelectionMenuItem { /// /// The keywords are used to quickly retrieve items. final List keywords; - final void Function(EditorState editorState, SelectionMenuService menuService) - handler; + final void Function( + EditorState editorState, + SelectionMenuService menuService, + BuildContext context, + ) handler; } class SelectionMenuWidget extends StatefulWidget { @@ -203,7 +206,7 @@ class _SelectionMenuWidgetState extends State { if (0 <= _selectedIndex && _selectedIndex < _showingItems.length) { _deleteLastCharacters(length: keyword.length + 1); _showingItems[_selectedIndex] - .handler(widget.editorState, widget.menuService); + .handler(widget.editorState, widget.menuService, context); return KeyEventResult.handled; } } else if (event.logicalKey == LogicalKeyboardKey.escape) { diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart index 4cb68a488f..d2f774d33f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart @@ -30,6 +30,7 @@ void main() async { onAlign: (alignment) { onAlignHit = true; }, + onResize: (width) {}, ); await tester.pumpWidget( diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_item_widget_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_item_widget_test.dart index 1488b15b18..01c1403738 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_item_widget_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_item_widget_test.dart @@ -20,7 +20,7 @@ void main() async { name: 'example', icon: icon, keywords: ['example A', 'example B'], - handler: (editorState, menuService) { + handler: (editorState, menuService, context) { flag = true; }, ); From d3194de9e618af73fc6d549f8b81908f818786ef Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 24 Aug 2022 15:53:30 +0800 Subject: [PATCH 64/86] fix: could not delete text when insert image --- .../internal_key_event_handlers/delete_text_handler.dart | 3 --- .../render/selection_menu/selection_menu_widget_test.dart | 4 +++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart index b931ee3d61..24049dbe1b 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart @@ -13,9 +13,6 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { selection = selection.isBackward ? selection : selection.reversed; // make sure all nodes is [TextNode]. final textNodes = nodes.whereType().toList(); - if (textNodes.length != nodes.length) { - return KeyEventResult.ignored; - } final transactionBuilder = TransactionBuilder(editorState); if (textNodes.length == 1) { diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart index 1efcfa640d..2711921352 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart @@ -25,7 +25,9 @@ void main() async { find.byType(SelectionMenuWidget, skipOffstage: false), findsNothing, ); - await _testDefaultSelectionMenuItems(i, editor); + if (defaultSelectionMenuItems[i].name != 'Image') { + await _testDefaultSelectionMenuItems(i, editor); + } }); } }); From 82b44c2c982f33ad7ad3cca73ca4daf8e2dabda3 Mon Sep 17 00:00:00 2001 From: appflowy Date: Wed, 24 Aug 2022 16:57:53 +0800 Subject: [PATCH 65/86] chore: update board column name --- .../plugins/board/application/board_bloc.dart | 46 ++++--- .../application/board_data_controller.dart | 38 +++++- .../board/application/board_listener.dart | 50 ++++++++ .../board/application/group_controller.dart | 12 +- .../board/application/group_listener.dart | 4 +- .../board/presentation/board_page.dart | 8 +- .../example/lib/multi_board_list_example.dart | 32 +++-- .../lib/single_board_list_example.dart | 16 ++- .../appflowy_board/lib/src/widgets/board.dart | 20 ++- .../widgets/board_column/board_column.dart | 8 +- .../board_column/board_column_data.dart | 38 +++++- .../lib/src/widgets/board_data.dart | 32 ++--- frontend/app_flowy/pubspec.lock | 2 +- .../flowy-grid/src/entities/block_entities.rs | 2 +- .../src/entities/group_entities/group.rs | 12 ++ .../group_entities/group_changeset.rs | 22 +++- .../flowy-grid/src/services/grid_editor.rs | 102 ++++++++------- .../src/services/grid_view_editor.rs | 19 +-- .../src/services/grid_view_manager.rs | 6 +- .../src/services/group/configuration.rs | 117 ++++++++++++++---- .../src/services/group/controller.rs | 20 ++- .../multi_select_controller.rs | 18 ++- .../single_select_controller.rs | 18 ++- .../select_option_controller/util.rs | 45 ++++--- .../flowy-grid/src/services/group/entities.rs | 23 +--- .../src/services/group/group_service.rs | 21 +++- .../tests/grid/group_test/script.rs | 13 +- .../flowy-grid/tests/grid/group_test/test.rs | 23 ++++ .../src/revision/grid_block.rs | 4 +- .../src/revision/group_rev.rs | 6 +- 30 files changed, 562 insertions(+), 215 deletions(-) create mode 100644 frontend/app_flowy/lib/plugins/board/application/board_listener.dart diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index f84c2a2bd1..7c04dec814 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -20,19 +20,19 @@ import 'group_controller.dart'; part 'board_bloc.freezed.dart'; class BoardBloc extends Bloc { - final BoardDataController _dataController; - late final AFBoardDataController afBoardDataController; + final BoardDataController _gridDataController; + late final AFBoardDataController boardController; final MoveRowFFIService _rowService; LinkedHashMap groupControllers = LinkedHashMap.new(); - GridFieldCache get fieldCache => _dataController.fieldCache; - String get gridId => _dataController.gridId; + GridFieldCache get fieldCache => _gridDataController.fieldCache; + String get gridId => _gridDataController.gridId; BoardBloc({required ViewPB view}) : _rowService = MoveRowFFIService(gridId: view.id), - _dataController = BoardDataController(view: view), + _gridDataController = BoardDataController(view: view), super(BoardState.initial(view.id)) { - afBoardDataController = AFBoardDataController( + boardController = AFBoardDataController( onMoveColumn: ( fromColumnId, fromIndex, @@ -70,7 +70,7 @@ class BoardBloc extends Bloc { await _loadGrid(emit); }, createRow: (groupId) async { - final result = await _dataController.createBoardCard(groupId); + final result = await _gridDataController.createBoardCard(groupId); result.fold( (rowPB) { emit(state.copyWith(editingRow: some(rowPB))); @@ -126,7 +126,7 @@ class BoardBloc extends Bloc { @override Future close() async { - await _dataController.dispose(); + await _gridDataController.dispose(); for (final controller in groupControllers.values) { controller.dispose(); } @@ -135,7 +135,7 @@ class BoardBloc extends Bloc { void initializeGroups(List groups) { for (final group in groups) { - final delegate = GroupControllerDelegateImpl(afBoardDataController); + final delegate = GroupControllerDelegateImpl(boardController); final controller = GroupController( gridId: state.gridId, group: group, @@ -147,12 +147,12 @@ class BoardBloc extends Bloc { } GridRowCache? getRowCache(String blockId) { - final GridBlockCache? blockCache = _dataController.blocks[blockId]; + final GridBlockCache? blockCache = _gridDataController.blocks[blockId]; return blockCache?.rowCache; } void _startListening() { - _dataController.addListener( + _gridDataController.addListener( onGridChanged: (grid) { if (!isClosed) { add(BoardEvent.didReceiveGridUpdate(grid)); @@ -162,18 +162,34 @@ class BoardBloc extends Bloc { List columns = groups.map((group) { return AFBoardColumnData( id: group.groupId, - desc: group.desc, + name: group.desc, items: _buildRows(group.rows), customData: group, ); }).toList(); - afBoardDataController.addColumns(columns); + boardController.addColumns(columns); initializeGroups(groups); }, onRowsChanged: (List rowInfos, RowsChangedReason reason) { add(BoardEvent.didReceiveRows(rowInfos)); }, + onDeletedGroup: (groupIds) { + // + }, + onInsertedGroup: (insertedGroups) { + // + }, + onUpdatedGroup: (updatedGroups) { + // + for (final group in updatedGroups) { + final columnController = + boardController.getColumnController(group.groupId); + if (columnController != null) { + columnController.updateColumnName(group.desc); + } + } + }, onError: (err) { Log.error(err); }, @@ -189,7 +205,7 @@ class BoardBloc extends Bloc { } Future _loadGrid(Emitter emit) async { - final result = await _dataController.loadData(); + final result = await _gridDataController.loadData(); result.fold( (grid) => emit( state.copyWith(loadingState: GridLoadingState.finish(left(unit))), @@ -301,6 +317,6 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate { @override void updateRow(String groupId, RowPB row) { - // + controller.updateColumnItem(groupId, BoardColumnItem(row: row)); } } diff --git a/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart b/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart index 1d17431713..31b2594497 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart @@ -10,9 +10,15 @@ import 'dart:async'; import 'package:dartz/dartz.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart'; +import 'board_listener.dart'; + typedef OnFieldsChanged = void Function(UnmodifiableListView); typedef OnGridChanged = void Function(GridPB); typedef DidLoadGroups = void Function(List); +typedef OnUpdatedGroup = void Function(List); +typedef OnDeletedGroup = void Function(List); +typedef OnInsertedGroup = void Function(List); + typedef OnRowsChanged = void Function( List, RowsChangedReason, @@ -23,6 +29,7 @@ class BoardDataController { final String gridId; final GridFFIService _gridFFIService; final GridFieldCache fieldCache; + final BoardListener _listener; // key: the block id final LinkedHashMap _blocks; @@ -44,16 +51,20 @@ class BoardDataController { BoardDataController({required ViewPB view}) : gridId = view.id, + _listener = BoardListener(view.id), _blocks = LinkedHashMap.new(), _gridFFIService = GridFFIService(gridId: view.id), fieldCache = GridFieldCache(gridId: view.id); void addListener({ - OnGridChanged? onGridChanged, + required OnGridChanged onGridChanged, OnFieldsChanged? onFieldsChanged, - DidLoadGroups? didLoadGroups, - OnRowsChanged? onRowsChanged, - OnError? onError, + required DidLoadGroups didLoadGroups, + required OnRowsChanged onRowsChanged, + required OnUpdatedGroup onUpdatedGroup, + required OnDeletedGroup onDeletedGroup, + required OnInsertedGroup onInsertedGroup, + required OnError? onError, }) { _onGridChanged = onGridChanged; _onFieldsChanged = onFieldsChanged; @@ -64,6 +75,25 @@ class BoardDataController { fieldCache.addListener(onFields: (fields) { _onFieldsChanged?.call(UnmodifiableListView(fields)); }); + + _listener.start(onBoardChanged: (result) { + result.fold( + (changeset) { + if (changeset.updateGroups.isNotEmpty) { + onUpdatedGroup.call(changeset.updateGroups); + } + + if (changeset.insertedGroups.isNotEmpty) { + onInsertedGroup.call(changeset.insertedGroups); + } + + if (changeset.deletedGroups.isNotEmpty) { + onDeletedGroup.call(changeset.deletedGroups); + } + }, + (e) => _onError?.call(e), + ); + }); } Future> loadData() async { diff --git a/frontend/app_flowy/lib/plugins/board/application/board_listener.dart b/frontend/app_flowy/lib/plugins/board/application/board_listener.dart new file mode 100644 index 0000000000..a953a993cc --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/application/board_listener.dart @@ -0,0 +1,50 @@ +import 'dart:typed_data'; + +import 'package:app_flowy/core/grid_notification.dart'; +import 'package:flowy_infra/notifier.dart'; +import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/dart_notification.pb.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/group_changeset.pb.dart'; + +typedef UpdateBoardNotifiedValue = Either; + +class BoardListener { + final String viewId; + PublishNotifier? _groupNotifier = PublishNotifier(); + GridNotificationListener? _listener; + BoardListener(this.viewId); + + void start({ + required void Function(UpdateBoardNotifiedValue) onBoardChanged, + }) { + _groupNotifier?.addPublishListener(onBoardChanged); + _listener = GridNotificationListener( + objectId: viewId, + handler: _handler, + ); + } + + void _handler( + GridNotification ty, + Either result, + ) { + switch (ty) { + case GridNotification.DidUpdateGroupView: + result.fold( + (payload) => _groupNotifier?.value = + left(GroupViewChangesetPB.fromBuffer(payload)), + (error) => _groupNotifier?.value = right(error), + ); + break; + default: + break; + } + } + + Future stop() async { + await _listener?.stop(); + _groupNotifier?.dispose(); + _groupNotifier = null; + } +} diff --git a/frontend/app_flowy/lib/plugins/board/application/group_controller.dart b/frontend/app_flowy/lib/plugins/board/application/group_controller.dart index 6fd68b1df8..b0a89baaa3 100644 --- a/frontend/app_flowy/lib/plugins/board/application/group_controller.dart +++ b/frontend/app_flowy/lib/plugins/board/application/group_controller.dart @@ -34,7 +34,12 @@ class GroupController { void startListening() { _listener.start(onGroupChanged: (result) { result.fold( - (GroupRowsChangesetPB changeset) { + (GroupChangesetPB changeset) { + for (final deletedRow in changeset.deletedRows) { + group.rows.removeWhere((rowPB) => rowPB.id == deletedRow); + delegate.removeRow(group.groupId, deletedRow); + } + for (final insertedRow in changeset.insertedRows) { final index = insertedRow.hasIndex() ? insertedRow.index : null; @@ -52,11 +57,6 @@ class GroupController { ); } - for (final deletedRow in changeset.deletedRows) { - group.rows.removeWhere((rowPB) => rowPB.id == deletedRow); - delegate.removeRow(group.groupId, deletedRow); - } - for (final updatedRow in changeset.updatedRows) { final index = group.rows.indexWhere( (rowPB) => rowPB.id == updatedRow.id, diff --git a/frontend/app_flowy/lib/plugins/board/application/group_listener.dart b/frontend/app_flowy/lib/plugins/board/application/group_listener.dart index 797177deca..e3b626af07 100644 --- a/frontend/app_flowy/lib/plugins/board/application/group_listener.dart +++ b/frontend/app_flowy/lib/plugins/board/application/group_listener.dart @@ -8,7 +8,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/group.pb.dart'; import 'package:dartz/dartz.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/group_changeset.pb.dart'; -typedef UpdateGroupNotifiedValue = Either; +typedef UpdateGroupNotifiedValue = Either; class GroupListener { final GroupPB group; @@ -34,7 +34,7 @@ class GroupListener { case GridNotification.DidUpdateGroup: result.fold( (payload) => _groupNotifier?.value = - left(GroupRowsChangesetPB.fromBuffer(payload)), + left(GroupChangesetPB.fromBuffer(payload)), (error) => _groupNotifier?.value = right(error), ); break; 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 e7202e0a6d..eb47e8c134 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -62,9 +62,8 @@ class BoardContent extends StatelessWidget { child: Padding( padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20), child: AFBoard( - // key: UniqueKey(), scrollController: ScrollController(), - dataController: context.read().afBoardDataController, + dataController: context.read().boardController, headerBuilder: _buildHeader, footBuilder: _buildFooter, cardBuilder: (_, data) => _buildCard(context, data), @@ -79,10 +78,11 @@ class BoardContent extends StatelessWidget { ); } - Widget _buildHeader(BuildContext context, AFBoardColumnData columnData) { + Widget _buildHeader( + BuildContext context, AFBoardColumnHeaderData headerData) { return AppFlowyColumnHeader( icon: const Icon(Icons.lightbulb_circle), - title: Text(columnData.desc), + title: Text(headerData.columnName), addIcon: const Icon(Icons.add, size: 20), moreIcon: const Icon(Icons.more_horiz, size: 20), height: 50, diff --git a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart index 218331d198..5df7ce54ff 100644 --- a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart +++ b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart @@ -34,13 +34,18 @@ class _MultiBoardListExampleState extends State { RichTextItem(title: "Card 8", subtitle: 'Aug 1, 2020 4:05 PM'), TextItem("Card 9"), ]; - final column1 = AFBoardColumnData(id: "To Do", items: a); - final column2 = AFBoardColumnData(id: "In Progress", items: [ - RichTextItem(title: "Card 10", subtitle: 'Aug 1, 2020 4:05 PM'), - TextItem("Card 11"), - ]); + final column1 = AFBoardColumnData(id: "To Do", name: "To Do", items: a); + final column2 = AFBoardColumnData( + id: "In Progress", + name: "In Progress", + items: [ + RichTextItem(title: "Card 10", subtitle: 'Aug 1, 2020 4:05 PM'), + TextItem("Card 11"), + ], + ); - final column3 = AFBoardColumnData(id: "Done", items: []); + final column3 = + AFBoardColumnData(id: "Done", name: "Done", items: []); boardDataController.addColumn(column1); boardDataController.addColumn(column2); @@ -68,10 +73,21 @@ class _MultiBoardListExampleState extends State { margin: config.columnItemPadding, ); }, - headerBuilder: (context, columnData) { + headerBuilder: (context, headerData) { return AppFlowyColumnHeader( icon: const Icon(Icons.lightbulb_circle), - title: Text(columnData.id), + title: SizedBox( + width: 60, + child: TextField( + controller: TextEditingController() + ..text = headerData.columnName, + onSubmitted: (val) { + boardDataController + .getColumnController(headerData.columnId)! + .updateColumnName(val); + }, + ), + ), addIcon: const Icon(Icons.add, size: 20), moreIcon: const Icon(Icons.more_horiz, size: 20), height: 50, diff --git a/frontend/app_flowy/packages/appflowy_board/example/lib/single_board_list_example.dart b/frontend/app_flowy/packages/appflowy_board/example/lib/single_board_list_example.dart index 97e83df448..4dda616621 100644 --- a/frontend/app_flowy/packages/appflowy_board/example/lib/single_board_list_example.dart +++ b/frontend/app_flowy/packages/appflowy_board/example/lib/single_board_list_example.dart @@ -13,12 +13,16 @@ class _SingleBoardListExampleState extends State { @override void initState() { - final column = AFBoardColumnData(id: "1", items: [ - TextItem("a"), - TextItem("b"), - TextItem("c"), - TextItem("d"), - ]); + final column = AFBoardColumnData( + id: "1", + name: "1", + items: [ + TextItem("a"), + TextItem("b"), + TextItem("c"), + TextItem("d"), + ], + ); boardData.addColumn(column); super.initState(); diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart index e07ee39d61..dbcf62671a 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart @@ -205,13 +205,13 @@ class _BoardContentState extends State { return ChangeNotifierProvider.value( key: ValueKey(columnData.id), - value: widget.dataController.columnController(columnData.id), + value: widget.dataController.getColumnController(columnData.id), child: Consumer( builder: (context, value, child) { final boardColumn = AFBoardColumnWidget( margin: _marginFromIndex(columnIndex), itemMargin: widget.config.columnItemPadding, - headerBuilder: widget.headerBuilder, + headerBuilder: _buildHeader, footBuilder: widget.footBuilder, cardBuilder: widget.cardBuilder, dataSource: dataSource, @@ -224,7 +224,6 @@ class _BoardContentState extends State { // columnKeys // .removeWhere((element) => element.columnId == columnData.id); - // columnKeys.add( // ColumnKey( // columnId: columnData.id, @@ -245,6 +244,19 @@ class _BoardContentState extends State { return children; } + Widget? _buildHeader( + BuildContext context, AFBoardColumnHeaderData headerData) { + if (widget.headerBuilder == null) { + return null; + } + return Selector( + selector: (context, controller) => controller.columnData.headerData, + builder: (context, headerData, _) { + return widget.headerBuilder!(context, headerData)!; + }, + ); + } + EdgeInsets _marginFromIndex(int index) { if (widget.dataController.columnDatas.isEmpty) { return widget.config.columnPadding; @@ -273,7 +285,7 @@ class _BoardColumnDataSourceImpl extends AFBoardColumnDataDataSource { @override AFBoardColumnData get columnData => - dataController.columnController(columnId).columnData; + dataController.getColumnController(columnId)!.columnData; @override List get acceptedColumnIds => dataController.columnIds; diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart index d4e5ff8800..a5e20055f6 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart @@ -27,9 +27,9 @@ typedef AFBoardColumnCardBuilder = Widget Function( AFColumnItem item, ); -typedef AFBoardColumnHeaderBuilder = Widget Function( +typedef AFBoardColumnHeaderBuilder = Widget? Function( BuildContext context, - AFBoardColumnData columnData, + AFBoardColumnHeaderData headerData, ); typedef AFBoardColumnFooterBuilder = Widget Function( @@ -125,8 +125,8 @@ class _AFBoardColumnWidgetState extends State { .map((item) => _buildWidget(context, item)) .toList(); - final header = - widget.headerBuilder?.call(context, widget.dataSource.columnData); + final header = widget.headerBuilder + ?.call(context, widget.dataSource.columnData.headerData); final footer = widget.footBuilder?.call(context, widget.dataSource.columnData); diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart index f26bd16c50..8cf03d96c1 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart @@ -34,6 +34,13 @@ class AFBoardColumnDataController extends ChangeNotifier with EquatableMixin { UnmodifiableListView get items => UnmodifiableListView(columnData.items); + void updateColumnName(String newName) { + if (columnData.headerData.columnName != newName) { + columnData.headerData.columnName = newName; + notifyListeners(); + } + } + /// Remove the item at [index]. /// * [index] the index of the item you want to remove /// * [notify] the default value of [notify] is true, it will notify the @@ -123,6 +130,20 @@ class AFBoardColumnDataController extends ChangeNotifier with EquatableMixin { notifyListeners(); } + void replaceOrInsertItem(AFColumnItem newItem) { + final index = columnData._items.indexWhere((item) => item.id == newItem.id); + if (index != -1) { + removeAt(index); + + columnData._items.removeAt(index); + columnData._items.insert(index, newItem); + notifyListeners(); + } else { + columnData._items.add(newItem); + notifyListeners(); + } + } + bool _containsItem(AFColumnItem item) { return columnData._items.indexWhere((element) => element.id == item.id) != -1; @@ -133,16 +154,20 @@ class AFBoardColumnDataController extends ChangeNotifier with EquatableMixin { class AFBoardColumnData extends ReoderFlexItem with EquatableMixin { @override final String id; - final String desc; + AFBoardColumnHeaderData headerData; final List _items; final CustomData? customData; AFBoardColumnData({ this.customData, required this.id, - this.desc = "", + required String name, List items = const [], - }) : _items = items; + }) : _items = items, + headerData = AFBoardColumnHeaderData( + columnId: id, + columnName: name, + ); /// Returns the readonly List UnmodifiableListView get items => @@ -156,3 +181,10 @@ class AFBoardColumnData extends ReoderFlexItem with EquatableMixin { return 'Column:[$id]'; } } + +class AFBoardColumnHeaderData { + String columnId; + String columnName; + + AFBoardColumnHeaderData({required this.columnId, required this.columnName}); +} diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart index a08bba378f..2cc853d6a5 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart @@ -89,10 +89,6 @@ class AFBoardDataController extends ChangeNotifier if (columnIds.isNotEmpty && notify) notifyListeners(); } - AFBoardColumnDataController columnController(String columnId) { - return _columnControllers[columnId]!; - } - AFBoardColumnDataController? getColumnController(String columnId) { final columnController = _columnControllers[columnId]; if (columnController == null) { @@ -129,6 +125,10 @@ class AFBoardDataController extends ChangeNotifier getColumnController(columnId)?.removeWhere((item) => item.id == itemId); } + void updateColumnItem(String columnId, AFColumnItem item) { + getColumnController(columnId)?.replaceOrInsertItem(item); + } + @override @protected void swapColumnItem( @@ -137,15 +137,14 @@ class AFBoardDataController extends ChangeNotifier String toColumnId, int toColumnIndex, ) { - final item = columnController(fromColumnId).removeAt(fromColumnIndex); - - if (columnController(toColumnId).items.length > toColumnIndex) { - assert(columnController(toColumnId).items[toColumnIndex] - is PhantomColumnItem); + final fromColumnController = getColumnController(fromColumnId)!; + final toColumnController = getColumnController(toColumnId)!; + final item = fromColumnController.removeAt(fromColumnIndex); + if (toColumnController.items.length > toColumnIndex) { + assert(toColumnController.items[toColumnIndex] is PhantomColumnItem); } - columnController(toColumnId).replace(toColumnIndex, item); - + toColumnController.replace(toColumnIndex, item); onMoveColumnItemToColumn?.call( fromColumnId, fromColumnIndex, @@ -174,9 +173,12 @@ class AFBoardDataController extends ChangeNotifier @override @protected bool removePhantom(String columnId) { - final columnController = this.columnController(columnId); + final columnController = getColumnController(columnId); + if (columnController == null) { + Log.warn('Can not find the column controller with columnId: $columnId'); + return false; + } final index = columnController.items.indexWhere((item) => item.isPhantom); - final isExist = index != -1; if (isExist) { columnController.removeAt(index); @@ -190,7 +192,7 @@ class AFBoardDataController extends ChangeNotifier @override @protected void updatePhantom(String columnId, int newIndex) { - final columnDataController = columnController(columnId); + final columnDataController = getColumnController(columnId)!; final index = columnDataController.items.indexWhere((item) => item.isPhantom); @@ -208,6 +210,6 @@ class AFBoardDataController extends ChangeNotifier @override @protected void insertPhantom(String columnId, int index, PhantomColumnItem item) { - columnController(columnId).insert(index, item); + getColumnController(columnId)!.insert(index, item); } } diff --git a/frontend/app_flowy/pubspec.lock b/frontend/app_flowy/pubspec.lock index 0f9421a563..b08897239c 100644 --- a/frontend/app_flowy/pubspec.lock +++ b/frontend/app_flowy/pubspec.lock @@ -28,7 +28,7 @@ packages: path: "packages/appflowy_board" relative: true source: path - version: "0.0.4" + version: "0.0.5" appflowy_editor: dependency: "direct main" description: diff --git a/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs index bb7eec9032..e691ed1830 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs @@ -30,7 +30,7 @@ impl BlockPB { } /// [RowPB] Describes a row. Has the id of the parent Block. Has the metadata of the row. -#[derive(Debug, Default, Clone, ProtoBuf)] +#[derive(Debug, Default, Clone, ProtoBuf, Eq, PartialEq)] pub struct RowPB { #[pb(index = 1)] pub block_id: String, 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 b769b18154..9cc138bc0f 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 @@ -1,4 +1,5 @@ use crate::entities::{CreateRowParams, FieldType, GridLayout, RowPB}; +use crate::services::group::Group; use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; @@ -82,6 +83,17 @@ pub struct GroupPB { pub rows: Vec, } +impl std::convert::From for GroupPB { + fn from(group: Group) -> Self { + Self { + field_id: group.field_id, + group_id: group.id, + desc: group.name, + rows: group.rows, + } + } +} + #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct RepeatedGridGroupConfigurationPB { #[pb(index = 1)] diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs index b7fdda8b3d..21f39775f6 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs @@ -1,5 +1,4 @@ use crate::entities::{GroupPB, InsertedRowPB, RowPB}; -use diesel::insertable::ColumnInsertValue::Default; use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; @@ -42,7 +41,17 @@ impl std::fmt::Display for GroupChangesetPB { impl GroupChangesetPB { pub fn is_empty(&self) -> bool { - self.inserted_rows.is_empty() && self.deleted_rows.is_empty() && self.updated_rows.is_empty() + self.group_name.is_none() + && self.inserted_rows.is_empty() + && self.deleted_rows.is_empty() + && self.updated_rows.is_empty() + } + + pub fn new(group_id: String) -> Self { + Self { + group_id, + ..Default::default() + } } pub fn name(group_id: String, name: &str) -> Self { @@ -126,9 +135,16 @@ pub struct GroupViewChangesetPB { #[pb(index = 3)] pub deleted_groups: Vec, + + #[pb(index = 4)] + pub update_groups: Vec, } -impl GroupViewChangesetPB {} +impl GroupViewChangesetPB { + pub fn is_empty(&self) -> bool { + self.inserted_groups.is_empty() && self.deleted_groups.is_empty() && self.update_groups.is_empty() + } +} #[derive(Debug, Default, ProtoBuf)] pub struct InsertedGroupPB { diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index 485bf6930c..8974cf0539 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -188,8 +188,13 @@ impl GridRevisionEditor { pub async fn replace_field(&self, field_rev: Arc) -> FlowyResult<()> { let field_id = field_rev.id.clone(); let _ = self - .modify(|grid_pad| Ok(grid_pad.replace_field_rev(field_rev)?)) + .modify(|grid_pad| Ok(grid_pad.replace_field_rev(field_rev.clone())?)) .await?; + + match self.view_manager.did_update_field(&field_rev.id).await { + Ok(_) => {} + Err(e) => tracing::error!("View manager update field failed: {:?}", e), + } let _ = self.notify_did_update_grid_field(&field_id).await?; Ok(()) } @@ -263,59 +268,65 @@ impl GridRevisionEditor { } async fn update_field_rev(&self, params: FieldChangesetParams, field_type: FieldType) -> FlowyResult<()> { - self.modify(|grid| { - let deserializer = TypeOptionJsonDeserializer(field_type); + let _ = self + .modify(|grid| { + let deserializer = TypeOptionJsonDeserializer(field_type); + let changeset = grid.modify_field(¶ms.field_id, |field| { + let mut is_changed = None; + if let Some(name) = params.name { + field.name = name; + is_changed = Some(()) + } - let changeset = grid.modify_field(¶ms.field_id, |field| { - let mut is_changed = None; - if let Some(name) = params.name { - field.name = name; - is_changed = Some(()) - } + if let Some(desc) = params.desc { + field.desc = desc; + is_changed = Some(()) + } - if let Some(desc) = params.desc { - field.desc = desc; - is_changed = Some(()) - } + if let Some(field_type) = params.field_type { + field.ty = field_type; + is_changed = Some(()) + } - if let Some(field_type) = params.field_type { - field.ty = field_type; - is_changed = Some(()) - } + if let Some(frozen) = params.frozen { + field.frozen = frozen; + is_changed = Some(()) + } - if let Some(frozen) = params.frozen { - field.frozen = frozen; - is_changed = Some(()) - } + if let Some(visibility) = params.visibility { + field.visibility = visibility; + is_changed = Some(()) + } - if let Some(visibility) = params.visibility { - field.visibility = visibility; - is_changed = Some(()) - } + if let Some(width) = params.width { + field.width = width; + is_changed = Some(()) + } - if let Some(width) = params.width { - field.width = width; - is_changed = Some(()) - } - - if let Some(type_option_data) = params.type_option_data { - match deserializer.deserialize(type_option_data) { - Ok(json_str) => { - let field_type = field.ty; - field.insert_type_option_str(&field_type, json_str); - is_changed = Some(()) - } - Err(err) => { - tracing::error!("Deserialize data to type option json failed: {}", err); + if let Some(type_option_data) = params.type_option_data { + match deserializer.deserialize(type_option_data) { + Ok(json_str) => { + let field_type = field.ty; + field.insert_type_option_str(&field_type, json_str); + is_changed = Some(()) + } + Err(err) => { + tracing::error!("Deserialize data to type option json failed: {}", err); + } } } - } - Ok(is_changed) - })?; - Ok(changeset) - }) - .await + Ok(is_changed) + })?; + Ok(changeset) + }) + .await?; + + match self.view_manager.did_update_field(¶ms.field_id).await { + Ok(_) => {} + Err(e) => tracing::error!("View manager update field failed: {:?}", e), + } + Ok(()) } pub async fn create_block(&self, block_meta_rev: GridBlockMetaRevision) -> FlowyResult<()> { @@ -585,6 +596,7 @@ impl GridRevisionEditor { .move_group_row(row_rev, to_group_id, to_row_id.clone()) .await { + tracing::trace!("Move group row cause row data changed: {:?}", row_changeset); match self.block_manager.update_row(row_changeset).await { Ok(_) => {} Err(e) => { diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs index 015756954b..6a04e18815 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs @@ -59,7 +59,7 @@ impl GridViewRevisionEditor { rev_manager: rev_manager.clone(), view_pad: pad.clone(), }; - let group_service = GroupService::new(configuration_reader, configuration_writer).await; + let group_service = GroupService::new(view_id.clone(), configuration_reader, configuration_writer).await; let user_id = user_id.to_owned(); let did_load_group = AtomicBool::new(false); Ok(Self { @@ -155,7 +155,7 @@ impl GridViewRevisionEditor { } } } - + /// Only call once after grid view editor initialized #[tracing::instrument(level = "trace", skip(self))] pub(crate) async fn load_groups(&self) -> FlowyResult> { let groups = if !self.did_load_group.load(Ordering::SeqCst) { @@ -198,9 +198,10 @@ impl GridViewRevisionEditor { }; let changeset = GroupViewChangesetPB { - view_id: "".to_string(), + view_id: self.view_id.clone(), inserted_groups: vec![inserted_group], deleted_groups: vec![params.from_group_id.clone()], + update_groups: vec![], }; self.notify_did_update_view(changeset).await; @@ -252,10 +253,15 @@ impl GridViewRevisionEditor { }) .await } - + #[tracing::instrument(level = "trace", skip_all, err)] pub(crate) async fn did_update_field(&self, field_id: &str) -> FlowyResult<()> { - if let Some(field_rev) = self.field_delegate.get_field_rev(&field_id).await { - let _ = self.group_service.write().await.did_update_field(&field_rev).await?; + if let Some(field_rev) = self.field_delegate.get_field_rev(field_id).await { + match self.group_service.write().await.did_update_field(&field_rev).await? { + None => {} + Some(changeset) => { + self.notify_did_update_view(changeset).await; + } + } } Ok(()) } @@ -272,7 +278,6 @@ impl GridViewRevisionEditor { .send(); } - #[allow(dead_code)] async fn modify(&self, f: F) -> FlowyResult<()> where F: for<'a> FnOnce(&'a mut GridViewRevisionPad) -> FlowyResult>, 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 b93ac2871f..657058b31f 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,10 +142,10 @@ impl GridViewManager { .await; } - if row_changeset.has_changed() { - Some(row_changeset) - } else { + if row_changeset.is_empty() { None + } else { + Some(row_changeset) } } 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 a462278d2b..5126c9b2fc 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs @@ -1,12 +1,12 @@ +use crate::entities::{GroupPB, GroupViewChangesetPB, InsertedGroupPB}; use crate::services::group::{default_group_configuration, Group}; use flowy_error::{FlowyError, FlowyResult}; use flowy_grid_data_model::revision::{ FieldRevision, FieldTypeRevision, GroupConfigurationContentSerde, GroupConfigurationRevision, GroupRecordRevision, }; -use std::marker::PhantomData; - use indexmap::IndexMap; use lib_infra::future::AFFuture; +use std::marker::PhantomData; use std::sync::Arc; pub trait GroupConfigurationReader: Send + Sync + 'static { @@ -26,6 +26,7 @@ pub trait GroupConfigurationWriter: Send + Sync + 'static { } pub struct GenericGroupConfiguration { + view_id: String, pub configuration: Arc, configuration_content: PhantomData, field_rev: Arc, @@ -39,6 +40,7 @@ where { #[tracing::instrument(level = "trace", skip_all, err)] pub async fn new( + view_id: String, field_rev: Arc, reader: Arc, writer: Arc, @@ -56,6 +58,7 @@ where // let configuration = C::from_configuration_content(&configuration_rev.content)?; Ok(Self { + view_id, field_rev, groups_map: IndexMap::new(), writer, @@ -72,8 +75,18 @@ where self.groups_map.values().cloned().collect() } - pub(crate) async fn merge_groups(&mut self, groups: Vec) -> FlowyResult<()> { - let (group_revs, groups) = merge_groups(&self.configuration.groups, groups); + pub(crate) fn merge_groups(&mut self, groups: Vec) -> FlowyResult> { + let MergeGroupResult { + groups, + inserted_groups, + updated_groups, + } = merge_groups(&self.configuration.groups, groups); + + let group_revs = groups + .iter() + .map(|group| GroupRecordRevision::new(group.id.clone(), group.name.clone())) + .collect(); + self.mut_configuration(move |configuration| { configuration.groups = group_revs; true @@ -82,7 +95,14 @@ where groups.into_iter().for_each(|group| { self.groups_map.insert(group.id.clone(), group); }); - Ok(()) + + let changeset = make_group_view_changeset(self.view_id.clone(), inserted_groups, updated_groups); + tracing::trace!("Group changeset: {:?}", changeset); + if changeset.is_empty() { + Ok(None) + } else { + Ok(Some(changeset)) + } } #[allow(dead_code)] @@ -101,7 +121,7 @@ where Ok(()) } - pub(crate) fn with_mut_groups(&mut self, mut each: impl FnMut(&mut Group)) { + pub(crate) fn iter_mut_groups(&mut self, mut each: impl FnMut(&mut Group)) { self.groups_map.iter_mut().for_each(|(_, group)| { each(group); }) @@ -189,33 +209,82 @@ where } } -fn merge_groups(old_group_revs: &[GroupRecordRevision], groups: Vec) -> (Vec, Vec) { - if old_group_revs.is_empty() { - let new_groups = groups - .iter() - .map(|group| GroupRecordRevision::new(group.id.clone())) - .collect(); - return (new_groups, groups); +fn merge_groups(old_groups: &[GroupRecordRevision], groups: Vec) -> MergeGroupResult { + let mut merge_result = MergeGroupResult::new(); + if old_groups.is_empty() { + merge_result.groups = groups; + return merge_result; } + // group_map is a helper map is used to filter out the new groups. let mut group_map: IndexMap = IndexMap::new(); groups.into_iter().for_each(|group| { group_map.insert(group.id.clone(), group); }); - // Inert - let mut sorted_groups: Vec = vec![]; - for group_rev in old_group_revs { + // The group is ordered in old groups. Add them before adding the new groups + for group_rev in old_groups { if let Some(group) = group_map.remove(&group_rev.group_id) { - sorted_groups.push(group); + if group.name == group_rev.name { + merge_result.add_group(group); + } else { + merge_result.add_updated_group(group); + } } } - sorted_groups.extend(group_map.into_values().collect::>()); - let new_group_revs = sorted_groups - .iter() - .map(|group| GroupRecordRevision::new(group.id.clone())) - .collect::>(); - tracing::trace!("group revs: {}, groups: {}", new_group_revs.len(), sorted_groups.len()); - (new_group_revs, sorted_groups) + // Find out the new groups + let new_groups = group_map.into_values().collect::>(); + for (index, group) in new_groups.into_iter().enumerate() { + merge_result.add_insert_group(index, group); + } + merge_result +} + +struct MergeGroupResult { + groups: Vec, + inserted_groups: Vec, + updated_groups: Vec, +} + +impl MergeGroupResult { + fn new() -> Self { + Self { + groups: vec![], + inserted_groups: vec![], + updated_groups: vec![], + } + } + + fn add_updated_group(&mut self, group: Group) { + self.groups.push(group.clone()); + self.updated_groups.push(group); + } + + fn add_group(&mut self, group: Group) { + self.groups.push(group.clone()); + } + + fn add_insert_group(&mut self, index: usize, group: Group) { + self.groups.push(group.clone()); + let inserted_group = InsertedGroupPB { + group: GroupPB::from(group), + index: index as i32, + }; + self.inserted_groups.push(inserted_group); + } +} + +fn make_group_view_changeset( + view_id: String, + inserted_groups: Vec, + updated_group: Vec, +) -> GroupViewChangesetPB { + let changeset = 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 7cf7897dfc..1dae47c0db 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, RowPB}; +use crate::entities::{GroupChangesetPB, GroupViewChangesetPB, RowPB}; use crate::services::cell::{decode_any_cell_data, CellBytesParser}; use crate::services::group::action::GroupAction; use crate::services::group::configuration::GenericGroupConfiguration; @@ -61,7 +61,7 @@ pub trait GroupControllerSharedOperation: Send + Sync { fn move_group_row(&mut self, context: MoveGroupRowContext) -> FlowyResult>; - fn did_update_field(&mut self, field_rev: &FieldRevision) -> FlowyResult<()>; + fn did_update_field(&mut self, field_rev: &FieldRevision) -> FlowyResult>; } /// C: represents the group configuration that impl [GroupConfigurationSerde] @@ -91,7 +91,7 @@ where let field_type_rev = field_rev.ty; 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).await?; + let _ = configuration.merge_groups(groups)?; let default_group = Group::new( DEFAULT_GROUP_ID.to_owned(), field_rev.id.clone(), @@ -114,6 +114,9 @@ impl GroupControllerSharedOperation for GenericGroupController, TypeOptionType = T>, + Self: GroupAction, { fn field_id(&self) -> &str { @@ -179,7 +182,8 @@ 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::

()?; - Ok(self.add_row_if_match(row_rev, &cell_data)) + let changesets = self.add_row_if_match(row_rev, &cell_data); + Ok(changesets) } else { Ok(vec![]) } @@ -209,8 +213,12 @@ where } } - fn did_update_field(&mut self, field_rev: &FieldRevision) -> FlowyResult<()> { - todo!() + fn did_update_field(&mut self, field_rev: &FieldRevision) -> FlowyResult> { + let field_type_rev = field_rev.ty; + let type_option = field_rev.get_type_option_entry::(field_type_rev); + let groups = G::generate_groups(&field_rev.id, &self.configuration, &type_option); + let changeset = self.configuration.merge_groups(groups)?; + Ok(changeset) } } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs index 0c1a7117d4..fe90e1b462 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs @@ -27,24 +27,30 @@ impl GroupAction for MultiSelectGroupController { fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec { let mut changesets = vec![]; - self.configuration.with_mut_groups(|group| { - add_row(group, &mut changesets, cell_data, row_rev); + self.configuration.iter_mut_groups(|group| { + if let Some(changeset) = add_row(group, cell_data, row_rev) { + changesets.push(changeset); + } }); changesets } fn remove_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec { let mut changesets = vec![]; - self.configuration.with_mut_groups(|group| { - remove_row(group, &mut changesets, cell_data, row_rev); + self.configuration.iter_mut_groups(|group| { + if let Some(changeset) = remove_row(group, cell_data, row_rev) { + changesets.push(changeset); + } }); changesets } fn move_row(&mut self, cell_data: &Self::CellDataType, mut context: MoveGroupRowContext) -> Vec { let mut group_changeset = vec![]; - self.configuration.with_mut_groups(|group| { - move_select_option_row(group, &mut group_changeset, cell_data, &mut context); + self.configuration.iter_mut_groups(|group| { + if let Some(changeset) = move_select_option_row(group, cell_data, &mut context) { + group_changeset.push(changeset); + } }); group_changeset } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs index ad4d296fab..d774ab083f 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs @@ -27,24 +27,30 @@ impl GroupAction for SingleSelectGroupController { fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec { let mut changesets = vec![]; - self.configuration.with_mut_groups(|group| { - add_row(group, &mut changesets, cell_data, row_rev); + self.configuration.iter_mut_groups(|group| { + if let Some(changeset) = add_row(group, cell_data, row_rev) { + changesets.push(changeset); + } }); changesets } fn remove_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec { let mut changesets = vec![]; - self.configuration.with_mut_groups(|group| { - remove_row(group, &mut changesets, cell_data, row_rev); + self.configuration.iter_mut_groups(|group| { + if let Some(changeset) = remove_row(group, cell_data, row_rev) { + changesets.push(changeset); + } }); changesets } fn move_row(&mut self, cell_data: &Self::CellDataType, mut context: MoveGroupRowContext) -> Vec { let mut group_changeset = vec![]; - self.configuration.with_mut_groups(|group| { - move_select_option_row(group, &mut group_changeset, cell_data, &mut context); + self.configuration.iter_mut_groups(|group| { + if let Some(changeset) = move_select_option_row(group, cell_data, &mut context) { + group_changeset.push(changeset); + } }); group_changeset } 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 9a1658c19f..349f7391a6 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 @@ -11,47 +11,56 @@ pub type SelectOptionGroupConfiguration = GenericGroupConfiguration, cell_data: &SelectOptionCellDataPB, 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); - changesets.push(GroupChangesetPB::insert( - group.id.clone(), - vec![InsertedRowPB::new(row_pb.clone())], - )); + changeset.inserted_rows.push(InsertedRowPB::new(row_pb.clone())); group.add_row(row_pb); } } else if group.contains_row(&row_rev.id) { - changesets.push(GroupChangesetPB::delete(group.id.clone(), vec![row_rev.id.clone()])); + changeset.deleted_rows.push(row_rev.id.clone()); group.remove_row(&row_rev.id); } }); + + if changeset.is_empty() { + None + } else { + Some(changeset) + } } pub fn remove_row( group: &mut Group, - changesets: &mut Vec, cell_data: &SelectOptionCellDataPB, 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 && group.contains_row(&row_rev.id) { - changesets.push(GroupChangesetPB::delete(group.id.clone(), vec![row_rev.id.clone()])); + changeset.deleted_rows.push(row_rev.id.clone()); group.remove_row(&row_rev.id); } }); + + if changeset.is_empty() { + None + } else { + Some(changeset) + } } pub fn move_select_option_row( group: &mut Group, - group_changeset: &mut Vec, _cell_data: &SelectOptionCellDataPB, context: &mut MoveGroupRowContext, -) { +) -> Option { + let mut changeset = GroupChangesetPB::new(group.id.clone()); let MoveGroupRowContext { row_rev, row_changeset, @@ -68,7 +77,7 @@ pub fn move_select_option_row( // Remove the row in which group contains it if from_index.is_some() { - group_changeset.push(GroupChangesetPB::delete(group.id.clone(), vec![row_rev.id.clone()])); + changeset.deleted_rows.push(row_rev.id.clone()); tracing::debug!("Group:{} remove row:{}", group.id, row_rev.id); group.remove_row(&row_rev.id); } @@ -78,7 +87,7 @@ pub fn move_select_option_row( let mut inserted_row = InsertedRowPB::new(row_pb.clone()); match to_index { None => { - group_changeset.push(GroupChangesetPB::insert(group.id.clone(), vec![inserted_row])); + changeset.inserted_rows.push(inserted_row); tracing::debug!("Group:{} append row:{}", group.id, row_rev.id); group.add_row(row_pb); } @@ -91,7 +100,7 @@ pub fn move_select_option_row( tracing::debug!("Group:{} append row:{}", group.id, row_rev.id); group.add_row(row_pb); } - group_changeset.push(GroupChangesetPB::insert(group.id.clone(), vec![inserted_row])); + changeset.inserted_rows.push(inserted_row); } } @@ -100,6 +109,12 @@ pub fn move_select_option_row( tracing::debug!("Mark row:{} belong to group:{}", row_rev.id, group.id); let cell_rev = insert_select_option_cell(group.id.clone(), field_rev); row_changeset.cell_by_field_id.insert(field_rev.id.clone(), cell_rev); + changeset.updated_rows.push(RowPB::from(*row_rev)); } } + if changeset.is_empty() { + None + } else { + Some(changeset) + } } 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 dd4171afb0..d8c4169eaf 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/entities.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/entities.rs @@ -1,33 +1,22 @@ -use crate::entities::{GroupPB, RowPB}; +use crate::entities::RowPB; -#[derive(Clone)] +#[derive(Clone, PartialEq, Eq)] pub struct Group { pub id: String, pub field_id: String, - pub desc: String, - rows: Vec, + pub name: String, + pub(crate) rows: Vec, /// [content] is used to determine which group the cell belongs to. pub content: String, } -impl std::convert::From for GroupPB { - fn from(group: Group) -> Self { - Self { - field_id: group.field_id, - group_id: group.id, - desc: group.desc, - rows: group.rows, - } - } -} - impl Group { - pub fn new(id: String, field_id: String, desc: String, content: String) -> Self { + pub fn new(id: String, field_id: String, name: String, content: String) -> Self { Self { id, field_id, - desc, + name, rows: vec![], content, } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs index a6ae504c05..c7d67f65d6 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs @@ -1,4 +1,4 @@ -use crate::entities::{FieldType, GroupChangesetPB}; +use crate::entities::{FieldType, GroupChangesetPB, GroupViewChangesetPB}; use crate::services::group::configuration::GroupConfigurationReader; use crate::services::group::controller::{GroupController, MoveGroupRowContext}; use crate::services::group::{ @@ -15,18 +15,20 @@ use std::future::Future; use std::sync::Arc; pub(crate) struct GroupService { + view_id: String, configuration_reader: Arc, configuration_writer: Arc, group_controller: Option>, } impl GroupService { - pub(crate) async fn new(configuration_reader: R, configuration_writer: W) -> Self + pub(crate) async fn new(view_id: String, configuration_reader: R, configuration_writer: W) -> Self where R: GroupConfigurationReader, W: GroupConfigurationWriter, { Self { + view_id, configuration_reader: Arc::new(configuration_reader), configuration_writer: Arc::new(configuration_writer), group_controller: None, @@ -36,8 +38,8 @@ impl GroupService { pub(crate) async fn groups(&self) -> Vec { self.group_controller .as_ref() - .and_then(|group_controller| Some(group_controller.groups())) - .unwrap_or(vec![]) + .map(|group_controller| group_controller.groups()) + .unwrap_or_default() } pub(crate) async fn get_group(&self, group_id: &str) -> Option<(usize, Group)> { @@ -170,9 +172,13 @@ impl GroupService { } } - pub(crate) async fn did_update_field(&mut self, field_rev: &FieldRevision) -> FlowyResult<()> { + #[tracing::instrument(level = "trace", name = "group_did_update_field", skip(self, field_rev), err)] + pub(crate) async fn did_update_field( + &mut self, + field_rev: &FieldRevision, + ) -> FlowyResult> { match self.group_controller.as_mut() { - None => Ok(()), + None => Ok(None), Some(group_controller) => group_controller.did_update_field(field_rev), } } @@ -196,6 +202,7 @@ impl GroupService { } FieldType::SingleSelect => { let configuration = SelectOptionGroupConfiguration::new( + self.view_id.clone(), field_rev.clone(), self.configuration_reader.clone(), self.configuration_writer.clone(), @@ -206,6 +213,7 @@ impl GroupService { } FieldType::MultiSelect => { let configuration = SelectOptionGroupConfiguration::new( + self.view_id.clone(), field_rev.clone(), self.configuration_reader.clone(), self.configuration_writer.clone(), @@ -216,6 +224,7 @@ impl GroupService { } FieldType::Checkbox => { let configuration = CheckboxGroupConfiguration::new( + self.view_id.clone(), field_rev.clone(), self.configuration_reader.clone(), self.configuration_writer.clone(), 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 94ef4dff1b..3a5ae9455b 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 @@ -1,9 +1,11 @@ use crate::grid::grid_editor::GridEditorTest; use flowy_grid::entities::{ - CreateRowParams, FieldType, GridLayout, GroupPB, MoveGroupParams, MoveGroupRowParams, RowPB, + CreateRowParams, FieldChangesetParams, FieldType, GridLayout, GroupPB, MoveGroupParams, MoveGroupRowParams, RowPB, }; use flowy_grid::services::cell::insert_select_option_cell; use flowy_grid_data_model::revision::RowChangeset; +use std::time::Duration; +use tokio::time::interval; pub enum GroupScript { AssertGroupRowCount { @@ -42,6 +44,9 @@ pub enum GroupScript { from_group_index: usize, to_group_index: usize, }, + UpdateField { + changeset: FieldChangesetParams, + }, } pub struct GridGroupTest { @@ -156,6 +161,12 @@ impl GridGroupTest { } => { let group = self.group_at_index(group_index).await; assert_eq!(group.group_id, group_pb.group_id); + assert_eq!(group.desc, group_pb.desc); + } + GroupScript::UpdateField { changeset } => { + self.editor.update_field(changeset).await.unwrap(); + let mut interval = interval(Duration::from_millis(130)); + interval.tick().await; } } } 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 bebf499773..d6a9839f77 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 @@ -1,5 +1,6 @@ use crate::grid::group_test::script::GridGroupTest; use crate::grid::group_test::script::GroupScript::*; +use flowy_grid::entities::FieldChangesetParams; #[tokio::test] async fn group_init_test() { @@ -314,3 +315,25 @@ async fn group_move_group_test() { ]; test.run_scripts(scripts).await; } + +#[tokio::test] +async fn group_update_field_test() { + let mut test = GridGroupTest::new().await; + let mut group = test.group_at_index(0).await; + let changeset = FieldChangesetParams { + field_id: group.field_id.clone(), + grid_id: test.grid_id.clone(), + name: Some("ABC".to_string()), + ..Default::default() + }; + + // group.desc = "ABC".to_string(); + let scripts = vec![ + UpdateField { changeset }, + AssertGroup { + group_index: 0, + expected_group: group, + }, + ]; + test.run_scripts(scripts).await; +} diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_block.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_block.rs index 5464d83877..ba113810f6 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/grid_block.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/grid_block.rs @@ -59,8 +59,8 @@ impl RowChangeset { } } - pub fn has_changed(&self) -> bool { - self.height.is_some() || self.visibility.is_some() || !self.cell_by_field_id.is_empty() + pub fn is_empty(&self) -> bool { + self.height.is_none() && self.visibility.is_none() && self.cell_by_field_id.is_empty() } } diff --git a/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs index 97d45295fe..ce378c0ffb 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs @@ -110,6 +110,9 @@ impl GroupConfigurationContentSerde for SelectOptionGroupConfigurationRevision { pub struct GroupRecordRevision { pub group_id: String, + #[serde(default)] + pub name: String, + #[serde(default = "DEFAULT_GROUP_RECORD_VISIBILITY")] pub visible: bool, } @@ -117,9 +120,10 @@ pub struct GroupRecordRevision { const DEFAULT_GROUP_RECORD_VISIBILITY: fn() -> bool = || true; impl GroupRecordRevision { - pub fn new(group_id: String) -> Self { + pub fn new(group_id: String, group_name: String) -> Self { Self { group_id, + name: group_name, visible: true, } } From 2c37bf806e46a14a1f7f009c251c329d19c62672 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 24 Aug 2022 17:23:45 +0800 Subject: [PATCH 66/86] feat: add error tips when loading image fail --- .../example/assets/example.json | 15 +++++----- .../src/render/image/image_node_builder.dart | 5 +++- .../src/render/image/image_node_widget.dart | 30 +++++++++++++++++-- .../src/render/image/image_upload_widget.dart | 12 ++++++-- .../selection_menu/selection_menu_widget.dart | 6 ++-- .../delete_text_handler.dart | 8 +++-- 6 files changed, 58 insertions(+), 18 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json b/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json index c6b27d9ae1..b522a68594 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json +++ b/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json @@ -59,13 +59,6 @@ } ] }, - { - "type": "image", - "attributes": { - "image_src": "https://s1.ax1x.com/2022/08/24/vgAJED.png", - "align": "center" - } - }, { "type": "text", "delta": [ @@ -129,6 +122,14 @@ "heading": "h3" } }, + { + "type": "image", + "attributes": { + "image_src": "https://s1.ax1x.com/2022/08/24/vgAJED.png", + "align": "left", + "width": 300 + } + }, { "type": "text", "delta": [ diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart index f62378fc1a..796e96c250 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart @@ -11,7 +11,10 @@ class ImageNodeBuilder extends NodeWidgetBuilder { Widget build(NodeWidgetContext context) { final src = context.node.attributes['image_src']; final align = context.node.attributes['align']; - final width = context.node.attributes['width']; + double? width; + if (context.node.attributes.containsKey('width')) { + width = context.node.attributes['width'].toDouble(); + } return ImageNodeWidget( key: context.node.key, src: src, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart index 110cff01b3..2cc0916b66 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart @@ -1,4 +1,5 @@ import 'package:appflowy_editor/src/infra/flowy_svg.dart'; +import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; import 'package:flutter/material.dart'; class ImageNodeWidget extends StatefulWidget { @@ -55,7 +56,12 @@ class _ImageNodeWidgetState extends State { @override Widget build(BuildContext context) { // only support network image. - return _buildNetworkImage(context); + + return Container( + width: defaultMaxTextNodeWidth, + padding: const EdgeInsets.only(top: 8, bottom: 8), + child: _buildNetworkImage(context), + ); } Widget _buildNetworkImage(BuildContext context) { @@ -77,8 +83,13 @@ class _ImageNodeWidgetState extends State { final networkImage = Image.network( widget.src, width: _imageWidth == null ? null : _imageWidth! - _distance, + gaplessPlayback: true, loadingBuilder: (context, child, loadingProgress) => loadingProgress == null ? child : _buildLoading(context), + errorBuilder: (context, error, stackTrace) { + _imageWidth ??= defaultMaxTextNodeWidth; + return _buildError(context); + }, ); if (_imageWidth == null) { _imageStream = networkImage.image.resolve(const ImageConfiguration()) @@ -127,8 +138,7 @@ class _ImageNodeWidgetState extends State { Widget _buildLoading(BuildContext context) { return SizedBox( - width: _imageWidth, - height: 300, + height: 150, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -145,6 +155,20 @@ class _ImageNodeWidgetState extends State { ); } + Widget _buildError(BuildContext context) { + return Container( + height: 100, + width: _imageWidth, + alignment: Alignment.center, + padding: const EdgeInsets.only(top: 8.0, bottom: 8.0), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(4.0)), + border: Border.all(width: 1, color: Colors.black), + ), + child: const Text('Could not load the image'), + ); + } + Widget _buildEdgeGesture( BuildContext context, { double? top, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_upload_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_upload_widget.dart index c7353532ab..a8728341df 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_upload_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_upload_widget.dart @@ -8,6 +8,7 @@ import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service import 'package:flutter/material.dart'; OverlayEntry? _imageUploadMenu; +EditorState? _editorState; void showImageUploadMenu( EditorState editorState, SelectionMenuService menuService, @@ -23,11 +24,11 @@ void showImageUploadMenu( child: Material( child: ImageUploadMenu( onSubmitted: (text) { - _dismissImageUploadMenu(); + // _dismissImageUploadMenu(); editorState.insertImageNode(text); }, onUpload: (text) { - _dismissImageUploadMenu(); + // _dismissImageUploadMenu(); editorState.insertImageNode(text); }, ), @@ -36,11 +37,18 @@ void showImageUploadMenu( }); Overlay.of(context)?.insert(_imageUploadMenu!); + + editorState.service.selectionService.currentSelection + .addListener(_dismissImageUploadMenu); } void _dismissImageUploadMenu() { _imageUploadMenu?.remove(); _imageUploadMenu = null; + + _editorState?.service.selectionService.currentSelection + .removeListener(_dismissImageUploadMenu); + _editorState = null; } class ImageUploadMenu extends StatefulWidget { diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart index f73251081f..1553085349 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart @@ -205,8 +205,10 @@ class _SelectionMenuWidgetState extends State { if (event.logicalKey == LogicalKeyboardKey.enter) { if (0 <= _selectedIndex && _selectedIndex < _showingItems.length) { _deleteLastCharacters(length: keyword.length + 1); - _showingItems[_selectedIndex] - .handler(widget.editorState, widget.menuService, context); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + _showingItems[_selectedIndex] + .handler(widget.editorState, widget.menuService, context); + }); return KeyEventResult.handled; } } else if (event.logicalKey == LogicalKeyboardKey.escape) { diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart index 24049dbe1b..d2a3d51e64 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart @@ -34,9 +34,9 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { } else { // 2. non-style // find previous text node. - while (textNode.previous != null) { - if (textNode.previous is TextNode) { - final previous = textNode.previous as TextNode; + var previous = textNode.previous; + while (previous != null) { + if (previous is TextNode) { transactionBuilder ..mergeText(previous, textNode) ..deleteNode(textNode) @@ -47,6 +47,8 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { ), ); break; + } else { + previous = previous.previous; } } } From ba5c1804ce955ed0359a661b5da7a07335a5a678 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 24 Aug 2022 18:02:00 +0800 Subject: [PATCH 67/86] fix: the text enterd after link doesn't need to be linked --- .../src/operation/transaction_builder.dart | 8 ++- .../lib/src/render/rich_text/quoted_text.dart | 53 +++++++++---------- .../lib/src/service/input_service.dart | 4 ++ 3 files changed, 35 insertions(+), 30 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart index 4f6de6e9b0..12c13bf2e5 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart @@ -116,11 +116,17 @@ class TransactionBuilder { /// Optionally, you may specify formatting attributes that are applied to the inserted string. /// By default, the formatting attributes before the insert position will be used. insertText(TextNode node, int index, String content, - [Attributes? attributes]) { + {Attributes? attributes, Attributes? removedAttributes}) { var newAttributes = attributes; if (index != 0 && attributes == null) { newAttributes = node.delta.slice(max(index - 1, 0), index).first.attributes; + if (newAttributes != null) { + newAttributes = Attributes.from(newAttributes); + if (removedAttributes != null) { + newAttributes.addAll(removedAttributes); + } + } } textEdit( node, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart index 3391729779..78c6653904 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart @@ -56,36 +56,31 @@ class _QuotedTextNodeWidgetState extends State @override Widget build(BuildContext context) { return SizedBox( - width: defaultMaxTextNodeWidth, - child: Padding( - padding: EdgeInsets.only(bottom: defaultLinePadding), - child: IntrinsicHeight( - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - FlowySvg( - key: iconKey, - width: _iconWidth, - padding: EdgeInsets.only(right: _iconRightPadding), - name: 'quote', + width: defaultMaxTextNodeWidth, + child: Padding( + padding: EdgeInsets.only(bottom: defaultLinePadding), + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FlowySvg( + key: iconKey, + width: _iconWidth, + padding: EdgeInsets.only(right: _iconRightPadding), + name: 'quote', + ), + Expanded( + child: FlowyRichText( + key: _richTextKey, + placeholderText: 'Quote', + textNode: widget.textNode, + editorState: widget.editorState, ), - Expanded( - child: FlowyRichText( - key: _richTextKey, - placeholderText: 'Quote', - textNode: widget.textNode, - editorState: widget.editorState, - ), - ), - ], - ), + ), + ], ), - )); - } - - double get _quoteHeight { - final lines = - widget.textNode.toRawString().characters.where((c) => c == '\n').length; - return (lines + 1) * _iconWidth; + ), + ), + ); } } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart index a92fae1b95..96f0777544 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart @@ -1,4 +1,5 @@ import 'package:appflowy_editor/src/infra/log.dart'; +import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -149,6 +150,9 @@ class _AppFlowyInputState extends State textNode, delta.insertionOffset, delta.textInserted, + removedAttributes: { + StyleKey.href: null, + }, ) ..commit(); } else { From a896637eab991a9f6c176e78890f5b4cd5bb6016 Mon Sep 17 00:00:00 2001 From: appflowy Date: Wed, 24 Aug 2022 21:06:10 +0800 Subject: [PATCH 68/86] fix: reload card content --- .../plugins/board/application/board_bloc.dart | 5 +++- .../app_flowy/lib/plugins/board/board.dart | 2 +- .../card/board_select_option_cell.dart | 16 +++++------ .../plugins/board/presentation/card/card.dart | 8 +++++- .../cell/cell_service/cell_data_loader.dart | 28 +++++++++++-------- .../cell/cell_service/context_builder.dart | 3 +- .../board_column/board_column_data.dart | 2 -- .../flowy-grid/src/services/grid_editor.rs | 22 +++++++++++---- 8 files changed, 54 insertions(+), 32 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index 7c04dec814..422f1dff2b 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -317,6 +317,9 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate { @override void updateRow(String groupId, RowPB row) { - controller.updateColumnItem(groupId, BoardColumnItem(row: row)); + // workaround: fix the board card reload timing issue. + Future.delayed(const Duration(milliseconds: 300), () { + controller.updateColumnItem(groupId, BoardColumnItem(row: row)); + }); } } diff --git a/frontend/app_flowy/lib/plugins/board/board.dart b/frontend/app_flowy/lib/plugins/board/board.dart index 213cc8bc3c..c55d7f2e17 100644 --- a/frontend/app_flowy/lib/plugins/board/board.dart +++ b/frontend/app_flowy/lib/plugins/board/board.dart @@ -31,7 +31,7 @@ class BoardPluginBuilder implements PluginBuilder { class BoardPluginConfig implements PluginConfig { @override - bool get creatable => true; + bool get creatable => false; } class BoardPlugin extends Plugin { diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart index 373bb3c850..a51a36f99f 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart @@ -35,19 +35,17 @@ class _BoardSelectOptionCellState extends State { child: BlocBuilder( builder: (context, state) { final children = state.selectedOptions - .map((option) => SelectOptionTag.fromOption( - context: context, - option: option, - )) + .map( + (option) => SelectOptionTag.fromOption( + context: context, + option: option, + ), + ) .toList(); return Align( alignment: Alignment.centerLeft, child: AbsorbPointer( - child: Wrap( - children: children, - spacing: 4, - runSpacing: 2, - ), + child: Wrap(children: children, spacing: 4, runSpacing: 2), ), ); }, diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart index a5c7b7ba2c..226a9e8241 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart @@ -42,7 +42,7 @@ class _BoardCardState extends State { _cardBloc = BoardCardBloc( gridId: widget.gridId, dataController: widget.dataController, - ); + )..add(const BoardCardEvent.initial()); super.initState(); } @@ -79,6 +79,12 @@ class _BoardCardState extends State { }, ).toList(); } + + @override + Future dispose() async { + _cardBloc.close(); + super.dispose(); + } } class _CardMoreOption extends StatelessWidget with CardAccessory { diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_data_loader.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_data_loader.dart index c4b3430199..a6a1ba43a9 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_data_loader.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_data_loader.dart @@ -24,18 +24,21 @@ class GridCellDataLoader { Future loadData() { final fut = service.getCell(cellId: cellId); return fut.then( - (result) => result.fold((GridCellPB cell) { - try { - return parser.parserData(cell.data); - } catch (e, s) { - Log.error('$parser parser cellData failed, $e'); - Log.error('Stack trace \n $s'); + (result) => result.fold( + (GridCellPB cell) { + try { + return parser.parserData(cell.data); + } catch (e, s) { + Log.error('$parser parser cellData failed, $e'); + Log.error('Stack trace \n $s'); + return null; + } + }, + (err) { + Log.error(err); return null; - } - }, (err) { - Log.error(err); - return null; - }), + }, + ), ); } } @@ -58,7 +61,8 @@ class DateCellDataParser implements IGridCellDataParser { } } -class SelectOptionCellDataParser implements IGridCellDataParser { +class SelectOptionCellDataParser + implements IGridCellDataParser { @override SelectOptionCellDataPB? parserData(List data) { if (data.isEmpty) { diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart index 1068cbf36b..f3ed73b449 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart @@ -279,8 +279,9 @@ class IGridCellController extends Equatable { _loadDataOperation?.cancel(); _loadDataOperation = Timer(const Duration(milliseconds: 10), () { _cellDataLoader.loadData().then((data) { - _cellDataNotifier?.value = data; + Log.debug('$fieldId CellData: Did Get cell data'); _cellsCache.insert(_cacheKey, GridCell(object: data)); + _cellDataNotifier?.value = data; }); }); } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart index 8cf03d96c1..0015ebd479 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart @@ -133,8 +133,6 @@ class AFBoardColumnDataController extends ChangeNotifier with EquatableMixin { void replaceOrInsertItem(AFColumnItem newItem) { final index = columnData._items.indexWhere((item) => item.id == newItem.id); if (index != -1) { - removeAt(index); - columnData._items.removeAt(index); columnData._items.insert(index, newItem); notifyListeners(); diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index 8974cf0539..cdc6cf6e6b 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -582,7 +582,7 @@ impl GridRevisionEditor { pub async fn move_group_row(&self, params: MoveGroupRowParams) -> FlowyResult<()> { let MoveGroupRowParams { - view_id: _, + view_id, from_row_id, to_group_id, to_row_id, @@ -597,10 +597,22 @@ impl GridRevisionEditor { .await { tracing::trace!("Move group row cause row data changed: {:?}", row_changeset); - match self.block_manager.update_row(row_changeset).await { - Ok(_) => {} - Err(e) => { - tracing::error!("Apply row changeset error:{:?}", e); + + let cell_changesets = row_changeset + .cell_by_field_id + .into_iter() + .map(|(field_id, cell_rev)| CellChangesetPB { + grid_id: view_id.clone(), + row_id: row_changeset.row_id.clone(), + field_id, + content: cell_rev.data, + }) + .collect::>(); + + for cell_changeset in cell_changesets { + match self.block_manager.update_cell(cell_changeset).await { + Ok(_) => {} + Err(e) => tracing::error!("Apply cell changeset error:{:?}", e), } } } From 778b55d44e75fe14bc56a8fbe25a16f2857f3e5d Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 24 Aug 2022 21:08:22 +0800 Subject: [PATCH 69/86] feat: implemnt export editor state --- .../appflowy_editor/example/lib/main.dart | 25 +++++++++++++++++++ .../flutter/generated_plugin_registrant.cc | 4 +++ .../linux/flutter/generated_plugins.cmake | 1 + .../Flutter/GeneratedPluginRegistrant.swift | 4 +++ .../example/macos/Podfile.lock | 12 +++++++++ .../appflowy_editor/example/pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 +++ .../windows/flutter/generated_plugins.cmake | 1 + .../lib/src/document/node.dart | 3 ++- .../lib/src/document/state_tree.dart | 6 +++++ 10 files changed, 59 insertions(+), 1 deletion(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart index 539bb74425..e5aae9432d 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -8,6 +9,7 @@ import 'expandable_floating_action_button.dart'; import 'plugin/youtube_link_node_widget.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:path_provider/path_provider.dart'; void main() { runApp(const MyApp()); @@ -58,6 +60,7 @@ class MyHomePage extends StatefulWidget { class _MyHomePageState extends State { final editorKey = GlobalKey(); int page = 0; + EditorState? _editorState; @override Widget build(BuildContext context) { @@ -103,6 +106,7 @@ class _MyHomePageState extends State { ..handler = (message) { debugPrint(message); }; + _editorState = editorState; return _buildAppFlowyEditor(editorState); } else { return const Center( @@ -146,6 +150,21 @@ class _MyHomePageState extends State { ); } + void _exportDocument(EditorState editorState) async { + // await FileSaver.instance.saveAs(String name, Uint8List bytes, String ext, MimeType); + final document = editorState.document.toJson(); + debugPrint(document.toString()); + final json = jsonEncode(document); + debugPrint(json); + + final directory = await getTemporaryDirectory(); + final path = directory.path; + debugPrint(path); + + final file = File('$path/temp.json'); + await file.writeAsString(json); + } + Widget _buildExpandableFab() { return ExpandableFab( distance: 112.0, @@ -177,6 +196,12 @@ class _MyHomePageState extends State { }, icon: const Icon(Icons.text_fields), ), + ActionButton( + onPressed: () { + _exportDocument(_editorState!); + }, + icon: const Icon(Icons.print), + ), ], ); } diff --git a/frontend/app_flowy/packages/appflowy_editor/example/linux/flutter/generated_plugin_registrant.cc b/frontend/app_flowy/packages/appflowy_editor/example/linux/flutter/generated_plugin_registrant.cc index 00fd3bc03f..45a19e066d 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/linux/flutter/generated_plugin_registrant.cc +++ b/frontend/app_flowy/packages/appflowy_editor/example/linux/flutter/generated_plugin_registrant.cc @@ -6,10 +6,14 @@ #include "generated_plugin_registrant.h" +#include #include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_saver_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSaverPlugin"); + file_saver_plugin_register_with_registrar(file_saver_registrar); g_autoptr(FlPluginRegistrar) rich_clipboard_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "RichClipboardPlugin"); rich_clipboard_plugin_register_with_registrar(rich_clipboard_linux_registrar); diff --git a/frontend/app_flowy/packages/appflowy_editor/example/linux/flutter/generated_plugins.cmake b/frontend/app_flowy/packages/appflowy_editor/example/linux/flutter/generated_plugins.cmake index 0342e3868a..10140a75b4 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/linux/flutter/generated_plugins.cmake +++ b/frontend/app_flowy/packages/appflowy_editor/example/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_saver rich_clipboard_linux url_launcher_linux ) diff --git a/frontend/app_flowy/packages/appflowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift b/frontend/app_flowy/packages/appflowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift index cc167443dc..ac60ac6ec9 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/frontend/app_flowy/packages/appflowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,11 +5,15 @@ import FlutterMacOS import Foundation +import file_saver +import path_provider_macos import rich_clipboard_macos import url_launcher_macos import wakelock_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) RichClipboardPlugin.register(with: registry.registrar(forPlugin: "RichClipboardPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin")) diff --git a/frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock b/frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock index d3a1dd3611..c56e297a68 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock +++ b/frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock @@ -1,5 +1,9 @@ PODS: + - file_saver (0.0.1): + - FlutterMacOS - FlutterMacOS (1.0.0) + - path_provider_macos (0.0.1): + - FlutterMacOS - rich_clipboard_macos (0.0.1): - FlutterMacOS - url_launcher_macos (0.0.1): @@ -8,14 +12,20 @@ PODS: - FlutterMacOS DEPENDENCIES: + - file_saver (from `Flutter/ephemeral/.symlinks/plugins/file_saver/macos`) - FlutterMacOS (from `Flutter/ephemeral`) + - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`) - rich_clipboard_macos (from `Flutter/ephemeral/.symlinks/plugins/rich_clipboard_macos/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - wakelock_macos (from `Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos`) EXTERNAL SOURCES: + file_saver: + :path: Flutter/ephemeral/.symlinks/plugins/file_saver/macos FlutterMacOS: :path: Flutter/ephemeral + path_provider_macos: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos rich_clipboard_macos: :path: Flutter/ephemeral/.symlinks/plugins/rich_clipboard_macos/macos url_launcher_macos: @@ -24,7 +34,9 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos SPEC CHECKSUMS: + file_saver: 44e6fbf666677faf097302460e214e977fdd977b FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 + path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 rich_clipboard_macos: 43364b66b9dc69d203eb8dd6d758e2d12e02723c url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3 wakelock_macos: bc3f2a9bd8d2e6c89fee1e1822e7ddac3bd004a9 diff --git a/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml b/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml index 4e2e2d68ab..593d9ff45f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml +++ b/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml @@ -40,6 +40,7 @@ dependencies: video_player: ^2.4.5 pod_player: 0.0.8 flutter_inappwebview: ^5.4.3+7 + file_saver: ^0.1.1 dev_dependencies: flutter_test: diff --git a/frontend/app_flowy/packages/appflowy_editor/example/windows/flutter/generated_plugin_registrant.cc b/frontend/app_flowy/packages/appflowy_editor/example/windows/flutter/generated_plugin_registrant.cc index 4f7884874d..391ef56268 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/windows/flutter/generated_plugin_registrant.cc +++ b/frontend/app_flowy/packages/appflowy_editor/example/windows/flutter/generated_plugin_registrant.cc @@ -6,9 +6,12 @@ #include "generated_plugin_registrant.h" +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + FileSaverPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSaverPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/frontend/app_flowy/packages/appflowy_editor/example/windows/flutter/generated_plugins.cmake b/frontend/app_flowy/packages/appflowy_editor/example/windows/flutter/generated_plugins.cmake index 88b22e5c77..047111654d 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/windows/flutter/generated_plugins.cmake +++ b/frontend/app_flowy/packages/appflowy_editor/example/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_saver url_launcher_windows ) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/node.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/node.dart index a0c4e33a70..909e7dd494 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/node.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/node.dart @@ -163,7 +163,8 @@ class Node extends ChangeNotifier with LinkedListEntry { 'type': type, }; if (children.isNotEmpty) { - map['children'] = children.map((node) => node.toJson()); + map['children'] = + (children.map((node) => node.toJson())).toList(growable: false); } if (_attributes.isNotEmpty) { map['attributes'] = _attributes; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/state_tree.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/state_tree.dart index 5bf49c0048..a17b2fbf98 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/state_tree.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/state_tree.dart @@ -33,6 +33,12 @@ class StateTree { return StateTree(root: root); } + Map toJson() { + return { + 'document': root.toJson(), + }; + } + Node? nodeAtPath(Path path) { return root.childAtPath(path); } From 8a8791b880ad1ad57213b878feb972ad30ea873a Mon Sep 17 00:00:00 2001 From: appflowy Date: Wed, 24 Aug 2022 23:08:57 +0800 Subject: [PATCH 70/86] chore: adjust ui --- .../card/board_checkbox_cell.dart | 19 +++++++++----- .../presentation/card/board_date_cell.dart | 15 ++++++++--- .../presentation/card/board_number_cell.dart | 16 ++++++++---- .../card/board_select_option_cell.dart | 13 +++++++--- .../presentation/card/board_text_cell.dart | 11 ++++---- .../presentation/card/board_url_cell.dart | 26 ++++++++++++------- .../plugins/board/presentation/card/card.dart | 2 +- .../board/presentation/card/define.dart | 3 +++ .../cell/cell_service/context_builder.dart | 1 - .../example/lib/multi_board_list_example.dart | 4 +-- 10 files changed, 73 insertions(+), 37 deletions(-) create mode 100644 frontend/app_flowy/lib/plugins/board/presentation/card/define.dart diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart index c816964d3c..25aacb5678 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart @@ -5,6 +5,8 @@ import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'define.dart'; + class BoardCheckboxCell extends StatefulWidget { final GridCellControllerBuilder cellControllerBuilder; @@ -38,12 +40,17 @@ class _BoardCheckboxCellState extends State { final icon = state.isSelected ? svgWidget('editor/editor_check') : svgWidget('editor/editor_uncheck'); - return Align( - alignment: Alignment.centerLeft, - child: FlowyIconButton( - iconPadding: EdgeInsets.zero, - icon: icon, - width: 20, + return Padding( + padding: EdgeInsets.symmetric( + vertical: BoardSizes.cardCellVPading, + ), + child: Align( + alignment: Alignment.centerLeft, + child: FlowyIconButton( + iconPadding: EdgeInsets.zero, + icon: icon, + width: 20, + ), ), ); }, diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart index 4a52d82116..7c7386696d 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart @@ -1,9 +1,12 @@ import 'package:app_flowy/plugins/board/application/card/board_date_cell_bloc.dart'; import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'define.dart'; + class BoardDateCell extends StatefulWidget { final GridCellControllerBuilder cellControllerBuilder; @@ -40,9 +43,15 @@ class _BoardDateCellState extends State { } else { return Align( alignment: Alignment.centerLeft, - child: FlowyText.regular( - state.dateStr, - fontSize: 14, + child: Padding( + padding: EdgeInsets.symmetric( + vertical: BoardSizes.cardCellVPading, + ), + child: FlowyText.regular( + state.dateStr, + fontSize: 13, + color: context.read().shader3, + ), ), ); } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart index 096592583e..965c06e643 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart @@ -4,6 +4,8 @@ import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'define.dart'; + class BoardNumberCell extends StatefulWidget { final GridCellControllerBuilder cellControllerBuilder; @@ -38,11 +40,15 @@ class _BoardNumberCellState extends State { if (state.content.isEmpty) { return const SizedBox(); } else { - return Align( - alignment: Alignment.centerLeft, - child: FlowyText.regular( - state.content, - fontSize: 14, + return Padding( + padding: + EdgeInsets.symmetric(vertical: BoardSizes.cardCellVPading), + child: Align( + alignment: Alignment.centerLeft, + child: FlowyText.medium( + state.content, + fontSize: 14, + ), ), ); } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart index a51a36f99f..9865875e44 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart @@ -4,6 +4,8 @@ import 'package:app_flowy/plugins/grid/presentation/widgets/cell/select_option_c import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'define.dart'; + class BoardSelectOptionCell extends StatefulWidget { final GridCellControllerBuilder cellControllerBuilder; @@ -42,10 +44,13 @@ class _BoardSelectOptionCellState extends State { ), ) .toList(); - return Align( - alignment: Alignment.centerLeft, - child: AbsorbPointer( - child: Wrap(children: children, spacing: 4, runSpacing: 2), + return Padding( + padding: EdgeInsets.symmetric(vertical: BoardSizes.cardCellVPading), + child: Align( + alignment: Alignment.centerLeft, + child: AbsorbPointer( + child: Wrap(children: children, spacing: 4, runSpacing: 2), + ), ), ); }, diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart index 2da156ded8..89b193edc5 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart @@ -4,6 +4,8 @@ import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'define.dart'; + class BoardTextCell extends StatefulWidget { final GridCellControllerBuilder cellControllerBuilder; const BoardTextCell({required this.cellControllerBuilder, Key? key}) @@ -37,11 +39,10 @@ class _BoardTextCellState extends State { } else { return Align( alignment: Alignment.centerLeft, - child: ConstrainedBox( - constraints: BoxConstraints.loose( - const Size(double.infinity, 100), - ), - child: FlowyText.regular( + child: Padding( + padding: + EdgeInsets.symmetric(vertical: BoardSizes.cardCellVPading), + child: FlowyText.medium( state.content, fontSize: 14, ), diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart index 31cca41e6a..5940e71788 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart @@ -4,6 +4,8 @@ import 'package:flowy_infra/theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'define.dart'; + class BoardUrlCell extends StatefulWidget { final GridCellControllerBuilder cellControllerBuilder; @@ -38,16 +40,20 @@ class _BoardUrlCellState extends State { if (state.content.isEmpty) { return const SizedBox(); } else { - return Align( - alignment: Alignment.centerLeft, - child: RichText( - textAlign: TextAlign.left, - text: TextSpan( - text: state.content, - style: TextStyle( - color: theme.main2, - fontSize: 14, - decoration: TextDecoration.underline, + return Padding( + padding: + EdgeInsets.symmetric(vertical: BoardSizes.cardCellVPading), + child: Align( + alignment: Alignment.centerLeft, + child: RichText( + textAlign: TextAlign.left, + text: TextSpan( + text: state.content, + style: TextStyle( + color: theme.main2, + fontSize: 14, + decoration: TextDecoration.underline, + ), ), ), ), diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart index 226a9e8241..ac65d5e5f7 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart @@ -73,7 +73,7 @@ class _BoardCardState extends State { (cellId) { final child = widget.cellBuilder.buildCell(cellId); return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5), + padding: const EdgeInsets.symmetric(horizontal: 6), child: child, ); }, diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/define.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/define.dart new file mode 100644 index 0000000000..ced0fefb83 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/define.dart @@ -0,0 +1,3 @@ +class BoardSizes { + static double get cardCellVPading => 4; +} diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart index f3ed73b449..95c708e0e2 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart @@ -279,7 +279,6 @@ class IGridCellController extends Equatable { _loadDataOperation?.cancel(); _loadDataOperation = Timer(const Duration(milliseconds: 10), () { _cellDataLoader.loadData().then((data) { - Log.debug('$fieldId CellData: Did Get cell data'); _cellsCache.insert(_cacheKey, GridCell(object: data)); _cellDataNotifier?.value = data; }); diff --git a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart index 5df7ce54ff..854936ea29 100644 --- a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart +++ b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart @@ -114,7 +114,7 @@ class _MultiBoardListExampleState extends State { return Align( alignment: Alignment.centerLeft, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 40), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 60), child: Text(item.s), ), ); @@ -124,7 +124,7 @@ class _MultiBoardListExampleState extends State { return Align( alignment: Alignment.centerLeft, child: Padding( - padding: const EdgeInsets.all(20), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 60), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ From 50e772edff961ad5f07bc81ac2ad5dd85e67de31 Mon Sep 17 00:00:00 2001 From: appflowy Date: Thu, 25 Aug 2022 09:46:26 +0800 Subject: [PATCH 71/86] chore: adjust header, card padding ui --- .../board/presentation/board_page.dart | 57 ++++++++++++++++--- .../plugins/board/presentation/card/card.dart | 2 +- .../presentation/card/card_container.dart | 2 +- .../appflowy_board/lib/src/widgets/board.dart | 8 ++- .../lib/src/widgets/styled_widgets/card.dart | 16 +++--- .../src/widgets/styled_widgets/footer.dart | 5 +- .../src/widgets/styled_widgets/header.dart | 20 +++++-- .../flowy_infra_ui/lib/style_widget/text.dart | 21 +++++-- .../flowy-grid/tests/grid/group_test/test.rs | 2 +- 9 files changed, 100 insertions(+), 33 deletions(-) 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 eb47e8c134..26c44e52bf 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -9,6 +9,9 @@ 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: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'; @@ -81,21 +84,47 @@ class BoardContent extends StatelessWidget { Widget _buildHeader( BuildContext context, AFBoardColumnHeaderData headerData) { return AppFlowyColumnHeader( - icon: const Icon(Icons.lightbulb_circle), - title: Text(headerData.columnName), - addIcon: const Icon(Icons.add, size: 20), - moreIcon: const Icon(Icons.more_horiz, size: 20), + // icon: const Icon(Icons.lightbulb_circle), + title: Flexible( + fit: FlexFit.tight, + child: FlowyText.medium( + headerData.columnName, + fontSize: 14, + overflow: TextOverflow.clip, + color: context.read().textColor, + ), + ), + // addIcon: const Icon(Icons.add, size: 20), + moreIcon: SizedBox( + width: 20, + height: 20, + child: svgWidget( + 'grid/details', + color: context.read().iconColor, + ), + ), height: 50, - margin: config.columnItemPadding, + margin: config.headerPadding, ); } Widget _buildFooter(BuildContext context, AFBoardColumnData columnData) { return AppFlowyColumnFooter( - icon: const Icon(Icons.add, size: 20), - title: const Text('New'), + icon: SizedBox( + height: 20, + width: 20, + child: svgWidget( + "home/add", + color: context.read().iconColor, + ), + ), + title: FlowyText.medium( + "New", + fontSize: 14, + color: context.read().textColor, + ), height: 50, - margin: config.columnItemPadding, + margin: config.footerPadding, onAddButtonClick: () { context.read().add(BoardEvent.createRow(columnData.id)); }); @@ -124,6 +153,8 @@ class BoardContent extends StatelessWidget { return AppFlowyColumnItemCard( key: ObjectKey(item), + margin: config.cardPadding, + decoration: _makeBoxDecoration(context), child: BoardCard( gridId: gridId, isEditing: isEditing, @@ -143,6 +174,16 @@ class BoardContent extends StatelessWidget { ); } + BoxDecoration _makeBoxDecoration(BuildContext context) { + final theme = context.read(); + final borderSide = BorderSide(color: theme.shader6, width: 1.0); + return BoxDecoration( + color: theme.surface, + border: Border.fromBorderSide(borderSide), + borderRadius: const BorderRadius.all(Radius.circular(6)), + ); + } + void _openCard(String gridId, GridFieldCache fieldCache, RowPB rowPB, GridRowCache rowCache, BuildContext context) { final rowInfo = RowInfo( diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart index ac65d5e5f7..7966f9e212 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart @@ -92,7 +92,7 @@ class _CardMoreOption extends StatelessWidget with CardAccessory { @override Widget build(BuildContext context) { - return svgWidget('home/details', color: context.read().iconColor); + return svgWidget('grid/details', color: context.read().iconColor); } @override 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 abca27e5c5..4b8c0548d5 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 @@ -116,7 +116,7 @@ class _CardEnterRegion extends StatelessWidget { .onEnter = false, child: IntrinsicHeight( child: Stack( - alignment: AlignmentDirectional.center, + alignment: AlignmentDirectional.topEnd, fit: StackFit.expand, children: children, )), diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart index dbcf62671a..dc7ae1c02f 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart @@ -12,12 +12,18 @@ class AFBoardConfig { final double cornerRadius; final EdgeInsets columnPadding; final EdgeInsets columnItemPadding; + final EdgeInsets footerPadding; + final EdgeInsets headerPadding; + final EdgeInsets cardPadding; final Color columnBackgroundColor; const AFBoardConfig({ this.cornerRadius = 6.0, this.columnPadding = const EdgeInsets.symmetric(horizontal: 8), - this.columnItemPadding = const EdgeInsets.symmetric(horizontal: 10), + this.columnItemPadding = const EdgeInsets.symmetric(horizontal: 12), + this.footerPadding = const EdgeInsets.symmetric(horizontal: 12), + this.headerPadding = const EdgeInsets.symmetric(horizontal: 16), + this.cardPadding = const EdgeInsets.symmetric(horizontal: 3, vertical: 4), this.columnBackgroundColor = Colors.transparent, }); } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/card.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/card.dart index 323964c75f..77cfc1cb13 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/card.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/card.dart @@ -2,16 +2,17 @@ import 'package:flutter/material.dart'; class AppFlowyColumnItemCard extends StatefulWidget { final Widget? child; - final Color backgroundColor; - final double cornerRadius; final EdgeInsets margin; final BoxConstraints boxConstraints; + final BoxDecoration decoration; const AppFlowyColumnItemCard({ this.child, - this.cornerRadius = 0.0, this.margin = const EdgeInsets.all(4), - this.backgroundColor = Colors.white, + this.decoration = const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.zero, + ), this.boxConstraints = const BoxConstraints(minHeight: 40), Key? key, }) : super(key: key); @@ -24,14 +25,11 @@ class _AppFlowyColumnItemCardState extends State { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.all(4), + padding: widget.margin, child: Container( clipBehavior: Clip.hardEdge, constraints: widget.boxConstraints, - decoration: BoxDecoration( - color: widget.backgroundColor, - borderRadius: BorderRadius.circular(widget.cornerRadius), - ), + decoration: widget.decoration, child: widget.child, ), ); diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/footer.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/footer.dart index 7f5655fe60..c877e4fe4d 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/footer.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/footer.dart @@ -12,7 +12,7 @@ class AppFlowyColumnFooter extends StatefulWidget { const AppFlowyColumnFooter({ this.icon, this.title, - this.margin = EdgeInsets.zero, + this.margin = const EdgeInsets.symmetric(horizontal: 12), required this.height, this.onAddButtonClick, Key? key, @@ -30,12 +30,13 @@ class _AppFlowyColumnFooterState extends State { child: SizedBox( height: widget.height, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), + padding: widget.margin, child: Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ if (widget.icon != null) widget.icon!, + const SizedBox(width: 8), if (widget.title != null) widget.title!, ], ), diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/header.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/header.dart index fdebc7ef21..88f52c9134 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/header.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/header.dart @@ -45,15 +45,25 @@ class _AppFlowyColumnHeaderState extends State { } if (widget.moreIcon != null) { - children.add(const Spacer()); + // children.add(const Spacer()); children.add( - IconButton(onPressed: widget.onMoreButtonClick, icon: widget.moreIcon!), + IconButton( + onPressed: widget.onMoreButtonClick, + icon: widget.moreIcon!, + padding: const EdgeInsets.all(4), + constraints: const BoxConstraints(), + ), ); } if (widget.addIcon != null) { children.add( - IconButton(onPressed: widget.onAddButtonClick, icon: widget.addIcon!), + IconButton( + onPressed: widget.onAddButtonClick, + icon: widget.addIcon!, + padding: const EdgeInsets.all(4), + constraints: const BoxConstraints(), + ), ); } @@ -61,9 +71,7 @@ class _AppFlowyColumnHeaderState extends State { height: widget.height, child: Padding( padding: widget.margin, - child: Row( - children: children, - ), + child: Row(children: children), ), ); } diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/text.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/text.dart index 74cf7e4c31..bad452cbe4 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/text.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/text.dart @@ -20,18 +20,31 @@ class FlowyText extends StatelessWidget { }) : super(key: key); const FlowyText.semibold(this.title, - {Key? key, this.fontSize = 16, TextOverflow? overflow, this.color, this.textAlign}) + {Key? key, + this.fontSize = 16, + TextOverflow? overflow, + this.color, + this.textAlign}) : fontWeight = FontWeight.w600, overflow = overflow ?? TextOverflow.ellipsis, super(key: key); - const FlowyText.medium(this.title, {Key? key, this.fontSize = 16, TextOverflow? overflow, this.color, this.textAlign}) + const FlowyText.medium(this.title, + {Key? key, + this.fontSize = 16, + TextOverflow? overflow, + this.color, + this.textAlign}) : fontWeight = FontWeight.w500, overflow = overflow ?? TextOverflow.ellipsis, super(key: key); const FlowyText.regular(this.title, - {Key? key, this.fontSize = 16, TextOverflow? overflow, this.color, this.textAlign}) + {Key? key, + this.fontSize = 16, + TextOverflow? overflow, + this.color, + this.textAlign}) : fontWeight = FontWeight.w400, overflow = overflow ?? TextOverflow.ellipsis, super(key: key); @@ -40,9 +53,9 @@ class FlowyText extends StatelessWidget { Widget build(BuildContext context) { final theme = context.watch(); return Text(title, - overflow: overflow, softWrap: false, textAlign: textAlign, + overflow: overflow, style: TextStyle( color: color ?? theme.textColor, fontWeight: fontWeight, 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 d6a9839f77..4a4d45f952 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 @@ -319,7 +319,7 @@ async fn group_move_group_test() { #[tokio::test] async fn group_update_field_test() { let mut test = GridGroupTest::new().await; - let mut group = test.group_at_index(0).await; + let group = test.group_at_index(0).await; let changeset = FieldChangesetParams { field_id: group.field_id.clone(), grid_id: test.grid_id.clone(), From befc40ba71b3096aa57a2ba337ff251592d40dbb Mon Sep 17 00:00:00 2001 From: appflowy Date: Thu, 25 Aug 2022 10:00:54 +0800 Subject: [PATCH 72/86] fix: clip the text when out of bound --- .../widgets/cell/select_option_cell/extension.dart | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart index 6fdd8bf6f8..6b937a3c9b 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart @@ -91,8 +91,13 @@ class SelectOptionTag extends StatelessWidget { Widget build(BuildContext context) { return ChoiceChip( pressElevation: 1, - label: - FlowyText.medium(name, fontSize: 12, overflow: TextOverflow.ellipsis), + label: Flexible( + child: FlowyText.medium( + name, + fontSize: 12, + overflow: TextOverflow.clip, + ), + ), selectedColor: color, backgroundColor: color, labelPadding: const EdgeInsets.symmetric(horizontal: 6), From 311c6ae94afbe941c1e68bd61a00465b0e608acf Mon Sep 17 00:00:00 2001 From: appflowy Date: Thu, 25 Aug 2022 11:12:58 +0800 Subject: [PATCH 73/86] chore: hide more button on card title --- .../plugins/board/presentation/board_page.dart | 17 ++++++++--------- .../presentation/card/board_checkbox_cell.dart | 4 +--- .../presentation/card/board_date_cell.dart | 2 +- .../presentation/card/board_number_cell.dart | 2 +- .../card/board_select_option_cell.dart | 3 ++- .../presentation/card/board_text_cell.dart | 2 +- .../presentation/card/board_url_cell.dart | 2 +- .../presentation/card/card_container.dart | 18 ++++++++++++++++++ .../board/presentation/card/define.dart | 2 +- .../cell/select_option_cell/extension.dart | 10 ++++------ 10 files changed, 38 insertions(+), 24 deletions(-) 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 26c44e52bf..3d109309d7 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -84,7 +84,6 @@ class BoardContent extends StatelessWidget { Widget _buildHeader( BuildContext context, AFBoardColumnHeaderData headerData) { return AppFlowyColumnHeader( - // icon: const Icon(Icons.lightbulb_circle), title: Flexible( fit: FlexFit.tight, child: FlowyText.medium( @@ -95,14 +94,14 @@ class BoardContent extends StatelessWidget { ), ), // addIcon: const Icon(Icons.add, size: 20), - moreIcon: SizedBox( - width: 20, - height: 20, - child: svgWidget( - 'grid/details', - color: context.read().iconColor, - ), - ), + // moreIcon: SizedBox( + // width: 20, + // height: 20, + // child: svgWidget( + // 'grid/details', + // color: context.read().iconColor, + // ), + // ), height: 50, margin: config.headerPadding, ); diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart index 25aacb5678..8578f956d6 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart @@ -41,9 +41,7 @@ class _BoardCheckboxCellState extends State { ? svgWidget('editor/editor_check') : svgWidget('editor/editor_uncheck'); return Padding( - padding: EdgeInsets.symmetric( - vertical: BoardSizes.cardCellVPading, - ), + padding: EdgeInsets.zero, child: Align( alignment: Alignment.centerLeft, child: FlowyIconButton( diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart index 7c7386696d..8896a32732 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart @@ -45,7 +45,7 @@ class _BoardDateCellState extends State { alignment: Alignment.centerLeft, child: Padding( padding: EdgeInsets.symmetric( - vertical: BoardSizes.cardCellVPading, + vertical: BoardSizes.cardCellVPadding, ), child: FlowyText.regular( state.dateStr, diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart index 965c06e643..33fb4a9b4b 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart @@ -42,7 +42,7 @@ class _BoardNumberCellState extends State { } else { return Padding( padding: - EdgeInsets.symmetric(vertical: BoardSizes.cardCellVPading), + EdgeInsets.symmetric(vertical: BoardSizes.cardCellVPadding), child: Align( alignment: Alignment.centerLeft, child: FlowyText.medium( diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart index 9865875e44..6cba08c636 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart @@ -45,7 +45,8 @@ class _BoardSelectOptionCellState extends State { ) .toList(); return Padding( - padding: EdgeInsets.symmetric(vertical: BoardSizes.cardCellVPading), + padding: + EdgeInsets.symmetric(vertical: BoardSizes.cardCellVPadding), child: Align( alignment: Alignment.centerLeft, child: AbsorbPointer( diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart index 89b193edc5..6f6e35429a 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart @@ -41,7 +41,7 @@ class _BoardTextCellState extends State { alignment: Alignment.centerLeft, child: Padding( padding: - EdgeInsets.symmetric(vertical: BoardSizes.cardCellVPading), + EdgeInsets.symmetric(vertical: BoardSizes.cardCellVPadding), child: FlowyText.medium( state.content, fontSize: 14, diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart index 5940e71788..f687e7efeb 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart @@ -42,7 +42,7 @@ class _BoardUrlCellState extends State { } else { return Padding( padding: - EdgeInsets.symmetric(vertical: BoardSizes.cardCellVPading), + EdgeInsets.symmetric(vertical: BoardSizes.cardCellVPadding), child: Align( alignment: Alignment.centerLeft, child: RichText( 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 4b8c0548d5..0e0a7287ae 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 @@ -74,6 +74,7 @@ class CardAccessoryContainer extends StatelessWidget { width: 26, height: 26, padding: const EdgeInsets.all(3), + decoration: _makeBoxDecoration(context), child: accessory, ), ); @@ -88,6 +89,23 @@ class CardAccessoryContainer extends StatelessWidget { } } +BoxDecoration _makeBoxDecoration(BuildContext context) { + final theme = context.read(); + final borderSide = BorderSide(color: theme.shader6, width: 1.0); + return BoxDecoration( + color: theme.surface, + border: Border.fromBorderSide(borderSide), + boxShadow: [ + BoxShadow( + color: theme.shader6, + spreadRadius: 0, + blurRadius: 2, + offset: Offset.zero) + ], + borderRadius: const BorderRadius.all(Radius.circular(6)), + ); +} + class _CardEnterRegion extends StatelessWidget { final Widget child; final List accessories; diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/define.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/define.dart index ced0fefb83..c2cff2ee0f 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/define.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/define.dart @@ -1,3 +1,3 @@ class BoardSizes { - static double get cardCellVPading => 4; + static double get cardCellVPadding => 4; } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart index 6b937a3c9b..f045984e66 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart @@ -91,12 +91,10 @@ class SelectOptionTag extends StatelessWidget { Widget build(BuildContext context) { return ChoiceChip( pressElevation: 1, - label: Flexible( - child: FlowyText.medium( - name, - fontSize: 12, - overflow: TextOverflow.clip, - ), + label: FlowyText.medium( + name, + fontSize: 12, + overflow: TextOverflow.clip, ), selectedColor: color, backgroundColor: color, From c827f9b15645d6790e4befbe449434760301a882 Mon Sep 17 00:00:00 2001 From: appflowy Date: Thu, 25 Aug 2022 12:36:56 +0800 Subject: [PATCH 74/86] chore: fix card refresh issue --- .../app_flowy/lib/plugins/board/application/board_bloc.dart | 5 +---- .../application/card/board_select_option_cell_bloc.dart | 1 - .../grid/application/cell/cell_service/context_builder.dart | 5 ++++- frontend/rust-lib/flowy-grid/src/event_handler.rs | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index 422f1dff2b..7c04dec814 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -317,9 +317,6 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate { @override void updateRow(String groupId, RowPB row) { - // workaround: fix the board card reload timing issue. - Future.delayed(const Duration(milliseconds: 300), () { - controller.updateColumnItem(groupId, BoardColumnItem(row: row)); - }); + controller.updateColumnItem(groupId, BoardColumnItem(row: row)); } } diff --git a/frontend/app_flowy/lib/plugins/board/application/card/board_select_option_cell_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/card/board_select_option_cell_bloc.dart index df36033cfa..1b70710a35 100644 --- a/frontend/app_flowy/lib/plugins/board/application/card/board_select_option_cell_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/card/board_select_option_cell_bloc.dart @@ -68,7 +68,6 @@ class BoardSelectOptionCellState with _$BoardSelectOptionCellState { factory BoardSelectOptionCellState.initial( GridSelectOptionCellController context) { final data = context.getCellData(); - return BoardSelectOptionCellState( selectedOptions: data?.selectOptions ?? [], ); diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart index 95c708e0e2..8ab486a48c 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart @@ -190,7 +190,10 @@ class IGridCellController extends Equatable { /// cell display: $12 _cellListener?.start(onCellChanged: (result) { result.fold( - (_) => _loadData(), + (_) { + _cellsCache.remove(fieldId); + _loadData(); + }, (err) => Log.error(err), ); }); diff --git a/frontend/rust-lib/flowy-grid/src/event_handler.rs b/frontend/rust-lib/flowy-grid/src/event_handler.rs index a9060aa87e..4b525c233f 100644 --- a/frontend/rust-lib/flowy-grid/src/event_handler.rs +++ b/frontend/rust-lib/flowy-grid/src/event_handler.rs @@ -269,7 +269,7 @@ pub(crate) async fn create_table_row_handler( data_result(row) } -// #[tracing::instrument(level = "debug", skip_all, err)] +#[tracing::instrument(level = "trace", skip_all, err)] pub(crate) async fn get_cell_handler( data: Data, manager: AppData>, From 14874772b18ee4758e6030585ae0cfc3136bedfb Mon Sep 17 00:00:00 2001 From: appflowy Date: Thu, 25 Aug 2022 13:44:58 +0800 Subject: [PATCH 75/86] chore: insert default group at index 0 --- .../card/board_checkbox_cell.dart | 2 - .../entities/group_entities/configuration.rs | 8 +-- .../src/services/group/configuration.rs | 51 +++++++++++++++---- .../src/services/group/controller.rs | 45 +++++++++++----- .../flowy-grid/src/services/group/entities.rs | 4 ++ .../src/revision/group_rev.rs | 32 ++++++++---- 6 files changed, 104 insertions(+), 38 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart index 8578f956d6..522a23fe78 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart @@ -5,8 +5,6 @@ import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'define.dart'; - class BoardCheckboxCell extends StatefulWidget { final GridCellControllerBuilder cellControllerBuilder; diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/configuration.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/configuration.rs index cb5503727b..19f5b27a9b 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/configuration.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/configuration.rs @@ -1,5 +1,5 @@ use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; -use flowy_grid_data_model::revision::{GroupRecordRevision, SelectOptionGroupConfigurationRevision}; +use flowy_grid_data_model::revision::{GroupRevision, SelectOptionGroupConfigurationRevision}; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct UrlGroupConfigurationPB { @@ -36,10 +36,10 @@ pub struct GroupRecordPB { visible: bool, } -impl std::convert::From for GroupRecordPB { - fn from(rev: GroupRecordRevision) -> Self { +impl std::convert::From for GroupRecordPB { + fn from(rev: GroupRevision) -> Self { Self { - group_id: rev.group_id, + group_id: rev.id, visible: rev.visible, } } 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 5126c9b2fc..546afb8a32 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs @@ -2,10 +2,11 @@ use crate::entities::{GroupPB, GroupViewChangesetPB, InsertedGroupPB}; use crate::services::group::{default_group_configuration, Group}; use flowy_error::{FlowyError, FlowyResult}; use flowy_grid_data_model::revision::{ - FieldRevision, FieldTypeRevision, GroupConfigurationContentSerde, GroupConfigurationRevision, GroupRecordRevision, + FieldRevision, FieldTypeRevision, GroupConfigurationContentSerde, GroupConfigurationRevision, GroupRevision, }; use indexmap::IndexMap; use lib_infra::future::AFFuture; +use std::fmt::Formatter; use std::marker::PhantomData; use std::sync::Arc; @@ -25,6 +26,15 @@ pub trait GroupConfigurationWriter: Send + Sync + 'static { ) -> AFFuture>; } +impl std::fmt::Display for GenericGroupConfiguration { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.groups_map.iter().for_each(|(_, group)| { + let _ = f.write_fmt(format_args!("Group:{} has {} rows \n", group.id, group.rows.len())); + }); + Ok(()) + } +} + pub struct GenericGroupConfiguration { view_id: String, pub configuration: Arc, @@ -84,12 +94,31 @@ where let group_revs = groups .iter() - .map(|group| GroupRecordRevision::new(group.id.clone(), group.name.clone())) - .collect(); + .map(|group| GroupRevision::new(group.id.clone(), group.name.clone())) + .collect::>(); self.mut_configuration(move |configuration| { - configuration.groups = group_revs; - true + let mut is_changed = false; + for new_group_rev in group_revs { + match configuration + .groups + .iter() + .position(|group_rev| group_rev.id == new_group_rev.id) + { + None => { + configuration.groups.push(new_group_rev); + is_changed = true; + } + Some(pos) => { + let removed_group = configuration.groups.remove(pos); + if removed_group != new_group_rev { + is_changed = true; + } + configuration.groups.insert(pos, new_group_rev); + } + } + } + is_changed })?; groups.into_iter().for_each(|group| { @@ -139,8 +168,8 @@ where self.groups_map.swap_indices(from_index, to_index); self.mut_configuration(|configuration| { - let from_index = configuration.groups.iter().position(|group| group.group_id == from_id); - let to_index = configuration.groups.iter().position(|group| group.group_id == to_id); + let from_index = configuration.groups.iter().position(|group| group.id == from_id); + let to_index = configuration.groups.iter().position(|group| group.id == to_id); if let (Some(from), Some(to)) = (from_index, to_index) { configuration.groups.swap(from, to); } @@ -183,10 +212,10 @@ where fn mut_configuration_group( &mut self, group_id: &str, - mut_groups_fn: impl Fn(&mut GroupRecordRevision), + mut_groups_fn: impl Fn(&mut GroupRevision), ) -> FlowyResult<()> { self.mut_configuration(|configuration| { - match configuration.groups.iter_mut().find(|group| group.group_id == group_id) { + match configuration.groups.iter_mut().find(|group| group.id == group_id) { None => false, Some(group_rev) => { mut_groups_fn(group_rev); @@ -209,7 +238,7 @@ where } } -fn merge_groups(old_groups: &[GroupRecordRevision], groups: Vec) -> MergeGroupResult { +fn merge_groups(old_groups: &[GroupRevision], groups: Vec) -> MergeGroupResult { let mut merge_result = MergeGroupResult::new(); if old_groups.is_empty() { merge_result.groups = groups; @@ -224,7 +253,7 @@ fn merge_groups(old_groups: &[GroupRecordRevision], groups: Vec) -> Merge // The group is ordered in old groups. Add them before adding the new groups for group_rev in old_groups { - if let Some(group) = group_map.remove(&group_rev.group_id) { + if let Some(group) = group_map.remove(&group_rev.id) { if group.name == group_rev.name { merge_result.add_group(group); } else { 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 1dae47c0db..fac034f6f5 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller.rs @@ -124,7 +124,11 @@ where } fn groups(&self) -> Vec { - self.configuration.clone_groups() + let mut groups = self.configuration.clone_groups(); + if self.default_group.is_empty() == false { + groups.insert(0, self.default_group.clone()); + } + groups } fn get_group(&self, group_id: &str) -> Option<(usize, Group)> { @@ -132,25 +136,28 @@ where Some((group.0, group.1.clone())) } + #[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 group_rows: Vec = vec![]; + let mut grouped_rows: Vec = vec![]; let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), field_rev); let cell_data = cell_bytes.parser::

()?; for group in self.configuration.groups() { if self.can_group(&group.content, &cell_data) { - group_rows.push(GroupRow { + grouped_rows.push(GroupedRow { row: row_rev.into(), group_id: group.id.clone(), }); } } - if group_rows.is_empty() { + if grouped_rows.is_empty() { + // ungrouped_rows.push(RowPB::from(row_rev)); self.default_group.add_row(row_rev.into()); } else { - for group_row in group_rows { + for group_row in grouped_rows { if let Some(group) = self.configuration.get_mut_group(&group_row.group_id) { group.add_row(group_row.row); } @@ -161,13 +168,27 @@ where } } - let default_group = self.default_group.clone(); - let mut groups: Vec = self.configuration.clone_groups(); - if !default_group.number_of_row() == 0 { - groups.push(default_group); - } + // 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(), + // ); + // } - Ok(groups) + tracing::Span::current().record( + "group_result", + &format!( + "{}, default_group has {} rows", + self.configuration, + self.default_group.rows.len() + ) + .as_str(), + ); + + Ok(self.groups()) } fn move_group(&mut self, from_group_id: &str, to_group_id: &str) -> FlowyResult<()> { @@ -222,7 +243,7 @@ where } } -struct GroupRow { +struct GroupedRow { row: RowPB, group_id: String, } 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 d8c4169eaf..f4d0ee1652 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/entities.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/entities.rs @@ -59,4 +59,8 @@ impl Group { pub fn number_of_row(&self) -> usize { self.rows.len() } + + pub fn is_empty(&self) -> bool { + self.rows.is_empty() + } } diff --git a/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs index ce378c0ffb..04b571fd76 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs @@ -14,7 +14,7 @@ pub struct GroupConfigurationRevision { pub id: String, pub field_id: String, pub field_type_rev: FieldTypeRevision, - pub groups: Vec, + pub groups: Vec, pub content: String, } @@ -106,24 +106,38 @@ impl GroupConfigurationContentSerde for SelectOptionGroupConfigurationRevision { } } -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub struct GroupRecordRevision { - pub group_id: String, +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +pub struct GroupRevision { + pub id: String, #[serde(default)] pub name: String, - #[serde(default = "DEFAULT_GROUP_RECORD_VISIBILITY")] + #[serde(skip, default = "IS_DEFAULT_GROUP")] + pub is_default: bool, + + #[serde(default = "GROUP_REV_VISIBILITY")] pub visible: bool, } -const DEFAULT_GROUP_RECORD_VISIBILITY: fn() -> bool = || true; +const GROUP_REV_VISIBILITY: fn() -> bool = || true; +const IS_DEFAULT_GROUP: fn() -> bool = || false; -impl GroupRecordRevision { - pub fn new(group_id: String, group_name: String) -> Self { +impl GroupRevision { + pub fn new(id: String, group_name: String) -> Self { Self { - group_id, + id, name: group_name, + is_default: false, + visible: true, + } + } + + pub fn default_group(id: String, group_name: String) -> Self { + Self { + id, + name: group_name, + is_default: true, visible: true, } } From 90db76811c64557f81ec542de667f2d0cb84d4fb Mon Sep 17 00:00:00 2001 From: appflowy Date: Thu, 25 Aug 2022 14:08:49 +0800 Subject: [PATCH 76/86] chore: update board template --- frontend/rust-lib/flowy-grid/src/util.rs | 112 +++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/frontend/rust-lib/flowy-grid/src/util.rs b/frontend/rust-lib/flowy-grid/src/util.rs index 90bf2f2a26..128c42ae04 100644 --- a/frontend/rust-lib/flowy-grid/src/util.rs +++ b/frontend/rust-lib/flowy-grid/src/util.rs @@ -34,6 +34,118 @@ pub fn make_default_grid() -> BuildGridContext { } pub fn make_default_board() -> BuildGridContext { + let mut grid_builder = GridBuilder::new(); + // text + let text_field = FieldBuilder::new(RichTextTypeOptionBuilder::default()) + .name("Description") + .visibility(true) + .primary(true) + .build(); + let text_field_id = text_field.id.clone(); + grid_builder.add_field(text_field); + + // single select + let to_do_option = SelectOptionPB::with_color("To Do", SelectOptionColorPB::Purple); + let doing_option = SelectOptionPB::with_color("Doing", SelectOptionColorPB::Orange); + let done_option = SelectOptionPB::with_color("Done", SelectOptionColorPB::Yellow); + let single_select_type_option = SingleSelectTypeOptionBuilder::default() + .add_option(to_do_option.clone()) + .add_option(doing_option.clone()) + .add_option(done_option.clone()); + let single_select_field = FieldBuilder::new(single_select_type_option) + .name("Status") + .visibility(true) + .build(); + let single_select_field_id = single_select_field.id.clone(); + grid_builder.add_field(single_select_field); + + // MultiSelect + let work_option = SelectOptionPB::with_color("Work", SelectOptionColorPB::Aqua); + let travel_option = SelectOptionPB::with_color("Travel", SelectOptionColorPB::Green); + let fun_option = SelectOptionPB::with_color("Fun", SelectOptionColorPB::Lime); + let health_option = SelectOptionPB::with_color("Health", SelectOptionColorPB::Pink); + let multi_select_type_option = MultiSelectTypeOptionBuilder::default() + .add_option(travel_option.clone()) + .add_option(work_option.clone()) + .add_option(fun_option.clone()) + .add_option(health_option.clone()); + let multi_select_field = FieldBuilder::new(multi_select_type_option) + .name("Tags") + .visibility(true) + .build(); + let multi_select_field_id = multi_select_field.id.clone(); + grid_builder.add_field(multi_select_field); + + for i in 0..3 { + let mut row_builder = RowRevisionBuilder::new(grid_builder.block_id(), grid_builder.field_revs()); + row_builder.insert_select_option_cell(&single_select_field_id, to_do_option.id.clone()); + match i { + 0 => { + row_builder.insert_text_cell(&text_field_id, "Update AppFlowy Website".to_string()); + row_builder.insert_select_option_cell(&multi_select_field_id, work_option.id.clone()); + } + 1 => { + row_builder.insert_text_cell(&text_field_id, "Learn French".to_string()); + row_builder.insert_select_option_cell(&multi_select_field_id, travel_option.id.clone()); + } + + 2 => { + row_builder.insert_text_cell(&text_field_id, "Exercise 4x/week".to_string()); + row_builder.insert_select_option_cell(&multi_select_field_id, fun_option.id.clone()); + } + _ => {} + } + let row = row_builder.build(); + grid_builder.add_row(row); + } + + for i in 0..3 { + let mut row_builder = RowRevisionBuilder::new(grid_builder.block_id(), grid_builder.field_revs()); + row_builder.insert_select_option_cell(&single_select_field_id, doing_option.id.clone()); + match i { + 0 => { + row_builder.insert_text_cell(&text_field_id, "Learn how to swim".to_string()); + row_builder.insert_select_option_cell(&multi_select_field_id, fun_option.id.clone()); + } + 1 => { + row_builder.insert_text_cell(&text_field_id, "Meditate 10 mins each day".to_string()); + row_builder.insert_select_option_cell(&multi_select_field_id, health_option.id.clone()); + } + + 2 => { + row_builder.insert_text_cell(&text_field_id, "Write atomic essays ".to_string()); + row_builder.insert_select_option_cell(&multi_select_field_id, fun_option.id.clone()); + } + _ => {} + } + let row = row_builder.build(); + grid_builder.add_row(row); + } + + for i in 0..2 { + let mut row_builder = RowRevisionBuilder::new(grid_builder.block_id(), grid_builder.field_revs()); + row_builder.insert_select_option_cell(&single_select_field_id, done_option.id.clone()); + match i { + 0 => { + row_builder.insert_text_cell(&text_field_id, "Publish an article".to_string()); + row_builder.insert_select_option_cell(&multi_select_field_id, work_option.id.clone()); + } + 1 => { + row_builder.insert_text_cell(&text_field_id, "Visit Chicago".to_string()); + row_builder.insert_select_option_cell(&multi_select_field_id, travel_option.id.clone()); + } + + _ => {} + } + let row = row_builder.build(); + grid_builder.add_row(row); + } + + grid_builder.build() +} + +#[allow(dead_code)] +pub fn make_default_board2() -> BuildGridContext { let mut grid_builder = GridBuilder::new(); // text let text_field = FieldBuilder::new(RichTextTypeOptionBuilder::default()) From d0f476dc72fd984eb661df0f40e70493497b4643 Mon Sep 17 00:00:00 2001 From: appflowy Date: Thu, 25 Aug 2022 14:38:13 +0800 Subject: [PATCH 77/86] chore: add max height constraint for board text cell --- .../presentation/card/board_text_cell.dart | 11 +++-- .../flowy_infra_ui/lib/style_widget/text.dart | 47 +++++++------------ 2 files changed, 23 insertions(+), 35 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart index 6f6e35429a..8f481dd996 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart @@ -40,11 +40,12 @@ class _BoardTextCellState extends State { return Align( alignment: Alignment.centerLeft, child: Padding( - padding: - EdgeInsets.symmetric(vertical: BoardSizes.cardCellVPadding), - child: FlowyText.medium( - state.content, - fontSize: 14, + padding: EdgeInsets.symmetric( + vertical: BoardSizes.cardCellVPadding, + ), + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 120), + child: FlowyText.medium(state.content, fontSize: 14), ), ), ); diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/text.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/text.dart index bad452cbe4..4b285a9137 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/text.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/text.dart @@ -4,15 +4,16 @@ import 'package:provider/provider.dart'; class FlowyText extends StatelessWidget { final String title; - final TextOverflow overflow; + final TextOverflow? overflow; final double fontSize; final FontWeight fontWeight; final TextAlign? textAlign; final Color? color; + const FlowyText( this.title, { Key? key, - this.overflow = TextOverflow.ellipsis, + this.overflow = TextOverflow.clip, this.fontSize = 16, this.fontWeight = FontWeight.w400, this.textAlign, @@ -20,47 +21,33 @@ class FlowyText extends StatelessWidget { }) : super(key: key); const FlowyText.semibold(this.title, - {Key? key, - this.fontSize = 16, - TextOverflow? overflow, - this.color, - this.textAlign}) + {Key? key, this.fontSize = 16, this.overflow, this.color, this.textAlign}) : fontWeight = FontWeight.w600, - overflow = overflow ?? TextOverflow.ellipsis, super(key: key); const FlowyText.medium(this.title, - {Key? key, - this.fontSize = 16, - TextOverflow? overflow, - this.color, - this.textAlign}) + {Key? key, this.fontSize = 16, this.overflow, this.color, this.textAlign}) : fontWeight = FontWeight.w500, - overflow = overflow ?? TextOverflow.ellipsis, super(key: key); const FlowyText.regular(this.title, - {Key? key, - this.fontSize = 16, - TextOverflow? overflow, - this.color, - this.textAlign}) + {Key? key, this.fontSize = 16, this.overflow, this.color, this.textAlign}) : fontWeight = FontWeight.w400, - overflow = overflow ?? TextOverflow.ellipsis, super(key: key); @override Widget build(BuildContext context) { final theme = context.watch(); - return Text(title, - softWrap: false, - textAlign: textAlign, - overflow: overflow, - style: TextStyle( - color: color ?? theme.textColor, - fontWeight: fontWeight, - fontSize: fontSize, - fontFamily: 'Mulish', - )); + return Text( + title, + textAlign: textAlign, + overflow: overflow ?? TextOverflow.clip, + style: TextStyle( + color: color ?? theme.textColor, + fontWeight: fontWeight, + fontSize: fontSize, + fontFamily: 'Mulish', + ), + ); } } From c43d0790dcc8a1797f5176289c268c63f99935c3 Mon Sep 17 00:00:00 2001 From: appflowy Date: Thu, 25 Aug 2022 14:58:08 +0800 Subject: [PATCH 78/86] chore: enable create board --- CHANGELOG.md | 10 ++++++++++ frontend/app_flowy/lib/plugins/board/board.dart | 2 +- .../card/board_select_option_cell.dart | 3 +-- .../plugins/board/presentation/card/define.dart | 2 +- .../selection_type_option/select_option.rs | 9 +++++++++ frontend/rust-lib/flowy-grid/src/util.rs | 15 ++++++++++++--- 6 files changed, 34 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ee49d36f3..94b4766806 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Release Notes +## Version 0.0.5 - beta.1 - 2022-08-25 + +New features +- Board-view database + - Group by single select + - drag and drop cards + - insert / delete cards + + + ## Version 0.0.4 - 2022-06-06 - Drag to adjust the width of a column - Upgrade to Flutter 3.0 diff --git a/frontend/app_flowy/lib/plugins/board/board.dart b/frontend/app_flowy/lib/plugins/board/board.dart index c55d7f2e17..213cc8bc3c 100644 --- a/frontend/app_flowy/lib/plugins/board/board.dart +++ b/frontend/app_flowy/lib/plugins/board/board.dart @@ -31,7 +31,7 @@ class BoardPluginBuilder implements PluginBuilder { class BoardPluginConfig implements PluginConfig { @override - bool get creatable => false; + bool get creatable => true; } class BoardPlugin extends Plugin { diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart index 6cba08c636..e10eda4cec 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart @@ -45,8 +45,7 @@ class _BoardSelectOptionCellState extends State { ) .toList(); return Padding( - padding: - EdgeInsets.symmetric(vertical: BoardSizes.cardCellVPadding), + padding: EdgeInsets.only(top: BoardSizes.cardCellVPadding), child: Align( alignment: Alignment.centerLeft, child: AbsorbPointer( diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/define.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/define.dart index c2cff2ee0f..5fc55743db 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/define.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/define.dart @@ -1,3 +1,3 @@ class BoardSizes { - static double get cardCellVPadding => 4; + static double get cardCellVPadding => 6; } diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs index 13da2e8359..9270f73684 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs @@ -157,6 +157,9 @@ pub fn select_option_color_from_index(index: usize) -> SelectOptionColorPB { pub struct SelectOptionIds(Vec); impl SelectOptionIds { + pub fn new() -> Self { + Self(vec![]) + } pub fn into_inner(self) -> Vec { self.0 } @@ -181,6 +184,12 @@ impl std::convert::From for SelectOptionIds { } } +impl ToString for SelectOptionIds { + fn to_string(&self) -> String { + self.0.join(SELECTION_IDS_SEPARATOR) + } +} + impl std::convert::From> for SelectOptionIds { fn from(s: Option) -> Self { match s { diff --git a/frontend/rust-lib/flowy-grid/src/util.rs b/frontend/rust-lib/flowy-grid/src/util.rs index 128c42ae04..65f6440c1f 100644 --- a/frontend/rust-lib/flowy-grid/src/util.rs +++ b/frontend/rust-lib/flowy-grid/src/util.rs @@ -86,7 +86,10 @@ pub fn make_default_board() -> BuildGridContext { } 1 => { row_builder.insert_text_cell(&text_field_id, "Learn French".to_string()); - row_builder.insert_select_option_cell(&multi_select_field_id, travel_option.id.clone()); + let mut options = SelectOptionIds::new(); + options.push(fun_option.id.clone()); + options.push(travel_option.id.clone()); + row_builder.insert_select_option_cell(&multi_select_field_id, options.to_string()); } 2 => { @@ -114,7 +117,10 @@ pub fn make_default_board() -> BuildGridContext { 2 => { row_builder.insert_text_cell(&text_field_id, "Write atomic essays ".to_string()); - row_builder.insert_select_option_cell(&multi_select_field_id, fun_option.id.clone()); + let mut options = SelectOptionIds::new(); + options.push(fun_option.id.clone()); + options.push(work_option.id.clone()); + row_builder.insert_select_option_cell(&multi_select_field_id, options.to_string()); } _ => {} } @@ -132,7 +138,10 @@ pub fn make_default_board() -> BuildGridContext { } 1 => { row_builder.insert_text_cell(&text_field_id, "Visit Chicago".to_string()); - row_builder.insert_select_option_cell(&multi_select_field_id, travel_option.id.clone()); + let mut options = SelectOptionIds::new(); + options.push(travel_option.id.clone()); + options.push(fun_option.id.clone()); + row_builder.insert_select_option_cell(&multi_select_field_id, options.to_string()); } _ => {} From 02283038d5ec1906cd57aa683b84687c7c383745 Mon Sep 17 00:00:00 2001 From: appflowy Date: Thu, 25 Aug 2022 16:17:19 +0800 Subject: [PATCH 79/86] chore: hide cell --- .../board/presentation/board_page.dart | 19 +++++++--- .../card/board_checkbox_cell.dart | 2 + .../presentation/card/board_date_cell.dart | 2 + .../presentation/card/board_number_cell.dart | 2 + .../card/board_select_option_cell.dart | 38 +++++++++++-------- .../presentation/card/board_text_cell.dart | 8 +++- .../presentation/card/board_url_cell.dart | 2 + .../plugins/board/presentation/card/card.dart | 4 +- .../presentation/card/card_cell_builder.dart | 9 ++++- .../example/lib/multi_board_list_example.dart | 6 +-- .../lib/single_board_list_example.dart | 5 ++- .../widgets/board_column/board_column.dart | 3 +- 12 files changed, 70 insertions(+), 30 deletions(-) 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 3d109309d7..45e9b574df 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -69,7 +69,11 @@ class BoardContent extends StatelessWidget { dataController: context.read().boardController, headerBuilder: _buildHeader, footBuilder: _buildFooter, - cardBuilder: (_, data) => _buildCard(context, data), + cardBuilder: (_, column, columnItem) => _buildCard( + context, + column, + columnItem, + ), columnConstraints: const BoxConstraints.tightFor(width: 240), config: AFBoardConfig( columnBackgroundColor: HexColor.fromHex('#F7F8FC'), @@ -129,12 +133,16 @@ class BoardContent extends StatelessWidget { }); } - Widget _buildCard(BuildContext context, AFColumnItem item) { - final rowPB = (item as BoardColumnItem).row; + Widget _buildCard( + BuildContext context, + AFBoardColumnData column, + AFColumnItem columnItem, + ) { + final rowPB = (columnItem as BoardColumnItem).row; final rowCache = context.read().getRowCache(rowPB.blockId); /// Return placeholder widget if the rowCache is null. - if (rowCache == null) return SizedBox(key: ObjectKey(item)); + if (rowCache == null) return SizedBox(key: ObjectKey(columnItem)); final fieldCache = context.read().fieldCache; final gridId = context.read().gridId; @@ -151,11 +159,12 @@ class BoardContent extends StatelessWidget { ); return AppFlowyColumnItemCard( - key: ObjectKey(item), + key: ObjectKey(columnItem), margin: config.cardPadding, decoration: _makeBoxDecoration(context), child: BoardCard( gridId: gridId, + groupId: column.id, isEditing: isEditing, cellBuilder: cellBuilder, dataController: cardController, diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart index 522a23fe78..ab78a46765 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart @@ -6,9 +6,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class BoardCheckboxCell extends StatefulWidget { + final String groupId; final GridCellControllerBuilder cellControllerBuilder; const BoardCheckboxCell({ + required this.groupId, required this.cellControllerBuilder, Key? key, }) : super(key: key); diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart index 8896a32732..dd6fe621df 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart @@ -8,9 +8,11 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'define.dart'; class BoardDateCell extends StatefulWidget { + final String groupId; final GridCellControllerBuilder cellControllerBuilder; const BoardDateCell({ + required this.groupId, required this.cellControllerBuilder, Key? key, }) : super(key: key); diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart index 33fb4a9b4b..a651f9f3aa 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart @@ -7,9 +7,11 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'define.dart'; class BoardNumberCell extends StatefulWidget { + final String groupId; final GridCellControllerBuilder cellControllerBuilder; const BoardNumberCell({ + required this.groupId, required this.cellControllerBuilder, Key? key, }) : super(key: key); diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart index e10eda4cec..f1da64579e 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart @@ -7,9 +7,11 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'define.dart'; class BoardSelectOptionCell extends StatefulWidget { + final String groupId; final GridCellControllerBuilder cellControllerBuilder; const BoardSelectOptionCell({ + required this.groupId, required this.cellControllerBuilder, Key? key, }) : super(key: key); @@ -36,23 +38,29 @@ class _BoardSelectOptionCellState extends State { value: _cellBloc, child: BlocBuilder( builder: (context, state) { - final children = state.selectedOptions - .map( - (option) => SelectOptionTag.fromOption( - context: context, - option: option, + if (state.selectedOptions + .where((element) => element.id == widget.groupId) + .isNotEmpty) { + return const SizedBox(); + } else { + final children = state.selectedOptions + .map( + (option) => SelectOptionTag.fromOption( + context: context, + option: option, + ), + ) + .toList(); + return Padding( + padding: EdgeInsets.only(top: BoardSizes.cardCellVPadding), + child: Align( + alignment: Alignment.centerLeft, + child: AbsorbPointer( + child: Wrap(children: children, spacing: 4, runSpacing: 2), ), - ) - .toList(); - return Padding( - padding: EdgeInsets.only(top: BoardSizes.cardCellVPadding), - child: Align( - alignment: Alignment.centerLeft, - child: AbsorbPointer( - child: Wrap(children: children, spacing: 4, runSpacing: 2), ), - ), - ); + ); + } }, ), ); diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart index 8f481dd996..d7c4e67bc1 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart @@ -7,9 +7,13 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'define.dart'; class BoardTextCell extends StatefulWidget { + final String groupId; final GridCellControllerBuilder cellControllerBuilder; - const BoardTextCell({required this.cellControllerBuilder, Key? key}) - : super(key: key); + const BoardTextCell({ + required this.groupId, + required this.cellControllerBuilder, + Key? key, + }) : super(key: key); @override State createState() => _BoardTextCellState(); diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart index f687e7efeb..a70414baf4 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart @@ -7,9 +7,11 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'define.dart'; class BoardUrlCell extends StatefulWidget { + final String groupId; final GridCellControllerBuilder cellControllerBuilder; const BoardUrlCell({ + required this.groupId, required this.cellControllerBuilder, Key? key, }) : super(key: key); diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart index 7966f9e212..0fcce45fa9 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart @@ -14,6 +14,7 @@ typedef OnEndEditing = void Function(String rowId); class BoardCard extends StatefulWidget { final String gridId; + final String groupId; final bool isEditing; final CardDataController dataController; final BoardCellBuilder cellBuilder; @@ -22,6 +23,7 @@ class BoardCard extends StatefulWidget { const BoardCard({ required this.gridId, + required this.groupId, required this.isEditing, required this.dataController, required this.cellBuilder, @@ -71,7 +73,7 @@ class _BoardCardState extends State { List _makeCells(BuildContext context, GridCellMap cellMap) { return cellMap.values.map( (cellId) { - final child = widget.cellBuilder.buildCell(cellId); + final child = widget.cellBuilder.buildCell(widget.groupId, cellId); return Padding( padding: const EdgeInsets.symmetric(horizontal: 6), child: child, diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card_cell_builder.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card_cell_builder.dart index 10ae0db680..83dbf584e8 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card_cell_builder.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card_cell_builder.dart @@ -19,7 +19,7 @@ class BoardCellBuilder { BoardCellBuilder(this.delegate); - Widget buildCell(GridCellIdentifier cellId) { + Widget buildCell(String groupId, GridCellIdentifier cellId) { final cellControllerBuilder = GridCellControllerBuilder( delegate: delegate, cellId: cellId, @@ -30,36 +30,43 @@ class BoardCellBuilder { switch (cellId.fieldType) { case FieldType.Checkbox: return BoardCheckboxCell( + groupId: groupId, cellControllerBuilder: cellControllerBuilder, key: key, ); case FieldType.DateTime: return BoardDateCell( + groupId: groupId, cellControllerBuilder: cellControllerBuilder, key: key, ); case FieldType.SingleSelect: return BoardSelectOptionCell( + groupId: groupId, cellControllerBuilder: cellControllerBuilder, key: key, ); case FieldType.MultiSelect: return BoardSelectOptionCell( + groupId: groupId, cellControllerBuilder: cellControllerBuilder, key: key, ); case FieldType.Number: return BoardNumberCell( + groupId: groupId, cellControllerBuilder: cellControllerBuilder, key: key, ); case FieldType.RichText: return BoardTextCell( + groupId: groupId, cellControllerBuilder: cellControllerBuilder, key: key, ); case FieldType.URL: return BoardUrlCell( + groupId: groupId, cellControllerBuilder: cellControllerBuilder, key: key, ); diff --git a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart index 854936ea29..e571a0559b 100644 --- a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart +++ b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart @@ -94,10 +94,10 @@ class _MultiBoardListExampleState extends State { margin: config.columnItemPadding, ); }, - cardBuilder: (context, item) { + cardBuilder: (context, column, columnItem) { return AppFlowyColumnItemCard( - key: ObjectKey(item), - child: _buildCard(item), + key: ObjectKey(columnItem), + child: _buildCard(columnItem), ); }, columnConstraints: const BoxConstraints.tightFor(width: 240), diff --git a/frontend/app_flowy/packages/appflowy_board/example/lib/single_board_list_example.dart b/frontend/app_flowy/packages/appflowy_board/example/lib/single_board_list_example.dart index 4dda616621..f22c562343 100644 --- a/frontend/app_flowy/packages/appflowy_board/example/lib/single_board_list_example.dart +++ b/frontend/app_flowy/packages/appflowy_board/example/lib/single_board_list_example.dart @@ -32,8 +32,9 @@ class _SingleBoardListExampleState extends State { Widget build(BuildContext context) { return AFBoard( dataController: boardData, - cardBuilder: (context, item) { - return _RowWidget(item: item as TextItem, key: ObjectKey(item)); + cardBuilder: (context, column, columnItem) { + return _RowWidget( + item: columnItem as TextItem, key: ObjectKey(columnItem)); }, ); } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart index a5e20055f6..ce053b5c79 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart @@ -24,6 +24,7 @@ typedef OnColumnInserted = void Function(String listId, int insertedIndex); typedef AFBoardColumnCardBuilder = Widget Function( BuildContext context, + AFBoardColumnData columnData, AFColumnItem item, ); @@ -207,7 +208,7 @@ class _AFBoardColumnWidgetState extends State { passthroughPhantomContext: item.phantomContext, ); } else { - return widget.cardBuilder(context, item); + return widget.cardBuilder(context, widget.dataSource.columnData, item); } } } From 00351158080f09c2011e4a5d9fac81cedeb2f95f Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Thu, 25 Aug 2022 16:24:57 +0800 Subject: [PATCH 80/86] Update CHANGELOG.md --- CHANGELOG.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94b4766806..9f5b42c8b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,11 @@ New features - Board-view database - - Group by single select - - drag and drop cards - - insert / delete cards - + - Group by single select + - drag and drop cards + - insert / delete cards + +![Aug-25-2022 16-22-38](https://user-images.githubusercontent.com/86001920/186614248-23186dfe-410e-427a-8cc6-865b1f79e074.gif) ## Version 0.0.4 - 2022-06-06 From 8cf1fee81a0ada5270649b97f0240977a5b813f0 Mon Sep 17 00:00:00 2001 From: appflowy Date: Thu, 25 Aug 2022 16:50:15 +0800 Subject: [PATCH 81/86] chore: update version --- frontend/Makefile.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml index 337b9efd76..f6e8194efa 100644 --- a/frontend/Makefile.toml +++ b/frontend/Makefile.toml @@ -22,7 +22,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true CARGO_MAKE_CRATE_FS_NAME = "dart_ffi" CARGO_MAKE_CRATE_NAME = "dart-ffi" LIB_NAME = "dart_ffi" -CURRENT_APP_VERSION = "0.0.4" +CURRENT_APP_VERSION = "0.0.5" FEATURES = "flutter" PRODUCT_NAME = "AppFlowy" # CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html From 96fcedf7717d3f93748d3075cb0ef87bef61ca87 Mon Sep 17 00:00:00 2001 From: Annie Date: Thu, 25 Aug 2022 19:38:16 +0800 Subject: [PATCH 82/86] chore: update changelog --- CHANGELOG.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f5b42c8b9..6522dc971f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Release Notes -## Version 0.0.5 - beta.1 - 2022-08-25 +## Version 0.0.5 - beta.1 - 08/25/2022 New features - Board-view database @@ -11,7 +11,7 @@ New features ![Aug-25-2022 16-22-38](https://user-images.githubusercontent.com/86001920/186614248-23186dfe-410e-427a-8cc6-865b1f79e074.gif) -## Version 0.0.4 - 2022-06-06 +## Version 0.0.4 - 06/06/2022 - Drag to adjust the width of a column - Upgrade to Flutter 3.0 - Native support for M1 chip @@ -23,7 +23,7 @@ New features - Fixed some bugs -## Version 0.0.4 - beta.3 - 2022-05-02 +## Version 0.0.4 - beta.3 - 05/02/2022 - Drag to reorder app/ view/ field - Row record open as a page - Auto resize the height of the row in the grid @@ -38,7 +38,7 @@ New features - Fixed some bugs -## Version 0.0.4 - beta.2 - 2022-04-11 +## Version 0.0.4 - beta.2 - 04/11/2022 - Support properties: Text, Number, Date, Checkbox, Select, Multi-select - Insert / delete rows @@ -46,7 +46,7 @@ New features - Edit property ![](https://user-images.githubusercontent.com/12026239/162753644-bf2f4e7a-2367-4d48-87e6-35e244e83a5b.png) -## Version 0.0.4 - beta.1 - 2022-04-08 +## Version 0.0.4 - beta.1 - 04/08/2022 v0.0.4 - beta.1 is pre-release New features @@ -55,7 +55,7 @@ New features - hide / delete columns - insert rows -## Version 0.0.3 - 2022-02-23 +## Version 0.0.3 - 02/23/2022 v0.0.3 is production ready, available on Linux, macOS, and Windows New features From b7c21df3b20f3c0f9ef1054e9d87d1b7006b64b2 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 25 Aug 2022 20:21:33 +0800 Subject: [PATCH 83/86] feat: implement simple export function --- .../appflowy_editor/example/lib/main.dart | 207 +++++++----------- .../appflowy_editor/example/pubspec.yaml | 1 + 2 files changed, 85 insertions(+), 123 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart index e5aae9432d..35dd8161db 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart @@ -4,12 +4,11 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'expandable_floating_action_button.dart'; -// import 'plugin/image_node_widget.dart'; -import 'plugin/youtube_link_node_widget.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:path_provider/path_provider.dart'; + +import 'expandable_floating_action_button.dart'; void main() { runApp(const MyApp()); @@ -18,20 +17,10 @@ void main() { class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); - // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( theme: ThemeData( - // This is the theme of your application. - // - // Try running your application with "flutter run". You'll see the - // application has a blue toolbar. Then, without quitting the app, try - // changing the primarySwatch below to Colors.green and then invoke - // "hot reload" (press "r" in the console where you ran "flutter run", - // or simply save your changes to "hot reload" in a Flutter IDE). - // Notice that the counter didn't reset back to zero; the application - // is not restarted. primarySwatch: Colors.blue, ), home: const MyHomePage(title: 'AppFlowyEditor Example'), @@ -41,16 +30,6 @@ class MyApp extends StatelessWidget { class MyHomePage extends StatefulWidget { const MyHomePage({Key? key, required this.title}) : super(key: key); - - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - final String title; @override @@ -58,56 +37,60 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - final editorKey = GlobalKey(); - int page = 0; - EditorState? _editorState; + int _pageIndex = 0; + late EditorState _editorState; + Future? _jsonString; @override Widget build(BuildContext context) { return Scaffold( body: Container( alignment: Alignment.topCenter, - child: _buildBody(), + child: _buildEditor(context), ), floatingActionButton: _buildExpandableFab(), ); } - Widget _buildBody() { - if (page == 0) { - return _buildAppFlowyEditorWithExample(); - } else if (page == 1) { - return _buildAppFlowyEditorWithEmptyDocument(); - } else if (page == 2) { - return _buildAppFlowyEditorWithBigDocument(); + Widget _buildEditor(BuildContext context) { + if (_jsonString != null) { + return _buildEditorWithJsonPath(_jsonString!); } - return Container(); + if (_pageIndex == 0) { + return _buildEditorWithJsonPath( + rootBundle.loadString('assets/example.json')); + } else if (_pageIndex == 1) { + return _buildEditorWithJsonPath( + rootBundle.loadString('assets/big_document.json')); + } else if (_pageIndex == 2) { + return _buildEditorWithEmptyDocument(); + } + throw UnimplementedError(); } - Widget _buildAppFlowyEditorWithEmptyDocument() { - final editorState = EditorState.empty(); - final editor = AppFlowyEditor( - editorState: editorState, - keyEventHandlers: const [], - customBuilders: const {}, - ); - return editor; - } - - Widget _buildAppFlowyEditorWithExample() { + Widget _buildEditorWithJsonPath(Future jsonString) { return FutureBuilder( - future: rootBundle.loadString('assets/example.json'), - builder: (context, snapshot) { + future: jsonString, + builder: (_, snapshot) { if (snapshot.hasData) { - final data = Map.from(json.decode(snapshot.data!)); - final editorState = EditorState(document: StateTree.fromJson(data)); - editorState.logConfiguration + _editorState = EditorState( + document: StateTree.fromJson( + Map.from( + json.decode(snapshot.data!), + ), + ), + ); + _editorState.logConfiguration ..level = LogLevel.all ..handler = (message) { debugPrint(message); }; - _editorState = editorState; - return _buildAppFlowyEditor(editorState); + return Container( + padding: const EdgeInsets.only(left: 20, right: 20), + child: AppFlowyEditor( + editorState: _editorState, + ), + ); } else { return const Center( child: CircularProgressIndicator(), @@ -117,52 +100,17 @@ class _MyHomePageState extends State { ); } - Widget _buildAppFlowyEditorWithBigDocument() { - return FutureBuilder( - future: rootBundle.loadString('assets/big_document.json'), - builder: (context, snapshot) { - if (snapshot.hasData) { - final data = Map.from(json.decode(snapshot.data!)); - return _buildAppFlowyEditor(EditorState( - document: StateTree.fromJson(data), - )); - } else { - return const Center( - child: CircularProgressIndicator(), - ); - } - }, + Widget _buildEditorWithEmptyDocument() { + _editorState = EditorState.empty(); + _editorState.logConfiguration + ..level = LogLevel.all + ..handler = (message) { + debugPrint(message); + }; + final editor = AppFlowyEditor( + editorState: _editorState, ); - } - - Widget _buildAppFlowyEditor(EditorState editorState) { - return Container( - padding: const EdgeInsets.only(left: 20, right: 20), - child: AppFlowyEditor( - key: editorKey, - editorState: editorState, - keyEventHandlers: const [], - customBuilders: { - // 'image': ImageNodeBuilder(), - 'youtube_link': YouTubeLinkNodeBuilder() - }, - ), - ); - } - - void _exportDocument(EditorState editorState) async { - // await FileSaver.instance.saveAs(String name, Uint8List bytes, String ext, MimeType); - final document = editorState.document.toJson(); - debugPrint(document.toString()); - final json = jsonEncode(document); - debugPrint(json); - - final directory = await getTemporaryDirectory(); - final path = directory.path; - debugPrint(path); - - final file = File('$path/temp.json'); - await file.writeAsString(json); + return editor; } Widget _buildExpandableFab() { @@ -170,39 +118,52 @@ class _MyHomePageState extends State { distance: 112.0, children: [ ActionButton( - onPressed: () { - if (page == 0) return; - setState(() { - page = 0; - }); - }, - icon: const Icon(Icons.note_add), + icon: const Icon(Icons.abc), + onPressed: () => _switchToPage(0), ), ActionButton( - icon: const Icon(Icons.document_scanner), - onPressed: () { - if (page == 1) return; - setState(() { - page = 1; - }); - }, + icon: const Icon(Icons.abc), + onPressed: () => _switchToPage(1), ), ActionButton( - onPressed: () { - if (page == 2) return; - setState(() { - page = 2; - }); - }, - icon: const Icon(Icons.text_fields), + icon: const Icon(Icons.abc), + onPressed: () => _switchToPage(2), ), ActionButton( - onPressed: () { - _exportDocument(_editorState!); - }, icon: const Icon(Icons.print), + onPressed: () => _exportDocument(_editorState), + ), + ActionButton( + icon: const Icon(Icons.import_export), + onPressed: () => _importDocument(), ), ], ); } + + void _exportDocument(EditorState editorState) async { + final document = editorState.document.toJson(); + final json = jsonEncode(document); + final directory = await getTemporaryDirectory(); + final path = directory.path; + final file = File('$path/editor.json'); + await file.writeAsString(json); + } + + void _importDocument() async { + final directory = await getTemporaryDirectory(); + final path = directory.path; + final file = File('$path/editor.json'); + setState(() { + _jsonString = file.readAsString(); + }); + } + + void _switchToPage(int pageIndex) { + if (pageIndex != _pageIndex) { + setState(() { + _pageIndex = pageIndex; + }); + } + } } diff --git a/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml b/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml index 593d9ff45f..9f2fae24bd 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml +++ b/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml @@ -41,6 +41,7 @@ dependencies: pod_player: 0.0.8 flutter_inappwebview: ^5.4.3+7 file_saver: ^0.1.1 + path_provider: ^2.0.11 dev_dependencies: flutter_test: From 2b725f8f7110872b0f6adb67a5172f814e2dc478 Mon Sep 17 00:00:00 2001 From: appflowy Date: Fri, 26 Aug 2022 08:32:25 +0800 Subject: [PATCH 84/86] chore: add remove out of bound guard --- .../board/presentation/card/board_select_option_cell.dart | 3 +++ .../lib/src/widgets/board_column/board_column_data.dart | 4 ++++ .../lib/src/widgets/reorder_flex/drag_target.dart | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart index f1da64579e..faed598d09 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart @@ -37,6 +37,9 @@ class _BoardSelectOptionCellState extends State { return BlocProvider.value( value: _cellBloc, child: BlocBuilder( + // buildWhen: (previous, current) { + // return previous.selectedOptions != current.selectedOptions; + // }, builder: (context, state) { if (state.selectedOptions .where((element) => element.id == widget.groupId) diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart index 0015ebd479..bc442acd2a 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart @@ -121,6 +121,10 @@ class AFBoardColumnDataController extends ChangeNotifier with EquatableMixin { columnData._items.add(newItem); Log.debug('[$AFBoardColumnDataController] $columnData add $newItem'); } else { + if (index >= columnData._items.length) { + return; + } + final removedItem = columnData._items.removeAt(index); columnData._items.insert(index, newItem); Log.debug( diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart index 8217d2736f..a091e9711a 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart @@ -140,7 +140,7 @@ class _ReorderDragTargetState widget.insertAnimationController, widget.deleteAnimationController, ) ?? - LongPressDraggable( + Draggable( maxSimultaneousDrags: 1, data: widget.dragTargetData, ignoringFeedbackSemantics: false, From d3c13d325ee8c4336db576cc6d8c0c466c433f8f Mon Sep 17 00:00:00 2001 From: appflowy Date: Fri, 26 Aug 2022 08:47:41 +0800 Subject: [PATCH 85/86] chore: fix card animation issue --- .../card/board_checkbox_cell.dart | 17 ++++++------ .../presentation/card/board_date_cell.dart | 16 ++++------- .../presentation/card/board_number_cell.dart | 17 +++++------- .../card/board_select_option_cell.dart | 18 +++++-------- .../presentation/card/board_text_cell.dart | 14 +++------- .../presentation/card/board_url_cell.dart | 27 ++++++++----------- .../plugins/board/presentation/card/card.dart | 2 +- 7 files changed, 41 insertions(+), 70 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart index ab78a46765..f832d3749d 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart @@ -36,19 +36,18 @@ class _BoardCheckboxCellState extends State { return BlocProvider.value( value: _cellBloc, child: BlocBuilder( + buildWhen: (previous, current) => + previous.isSelected != current.isSelected, builder: (context, state) { final icon = state.isSelected ? svgWidget('editor/editor_check') : svgWidget('editor/editor_uncheck'); - return Padding( - padding: EdgeInsets.zero, - child: Align( - alignment: Alignment.centerLeft, - child: FlowyIconButton( - iconPadding: EdgeInsets.zero, - icon: icon, - width: 20, - ), + return Align( + alignment: Alignment.centerLeft, + child: FlowyIconButton( + iconPadding: EdgeInsets.zero, + icon: icon, + width: 20, ), ); }, diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart index dd6fe621df..47472a0f9f 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart @@ -5,8 +5,6 @@ import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'define.dart'; - class BoardDateCell extends StatefulWidget { final String groupId; final GridCellControllerBuilder cellControllerBuilder; @@ -39,21 +37,17 @@ class _BoardDateCellState extends State { return BlocProvider.value( value: _cellBloc, child: BlocBuilder( + buildWhen: (previous, current) => previous.dateStr != current.dateStr, builder: (context, state) { if (state.dateStr.isEmpty) { return const SizedBox(); } else { return Align( alignment: Alignment.centerLeft, - child: Padding( - padding: EdgeInsets.symmetric( - vertical: BoardSizes.cardCellVPadding, - ), - child: FlowyText.regular( - state.dateStr, - fontSize: 13, - color: context.read().shader3, - ), + child: FlowyText.regular( + state.dateStr, + fontSize: 13, + color: context.read().shader3, ), ); } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart index a651f9f3aa..0f4aca6b61 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart @@ -4,8 +4,6 @@ import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'define.dart'; - class BoardNumberCell extends StatefulWidget { final String groupId; final GridCellControllerBuilder cellControllerBuilder; @@ -38,19 +36,16 @@ class _BoardNumberCellState extends State { return BlocProvider.value( value: _cellBloc, child: BlocBuilder( + buildWhen: (previous, current) => previous.content != current.content, builder: (context, state) { if (state.content.isEmpty) { return const SizedBox(); } else { - return Padding( - padding: - EdgeInsets.symmetric(vertical: BoardSizes.cardCellVPadding), - child: Align( - alignment: Alignment.centerLeft, - child: FlowyText.medium( - state.content, - fontSize: 14, - ), + return Align( + alignment: Alignment.centerLeft, + child: FlowyText.medium( + state.content, + fontSize: 14, ), ); } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart index faed598d09..f75de47651 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart @@ -4,8 +4,6 @@ import 'package:app_flowy/plugins/grid/presentation/widgets/cell/select_option_c import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'define.dart'; - class BoardSelectOptionCell extends StatefulWidget { final String groupId; final GridCellControllerBuilder cellControllerBuilder; @@ -37,9 +35,8 @@ class _BoardSelectOptionCellState extends State { return BlocProvider.value( value: _cellBloc, child: BlocBuilder( - // buildWhen: (previous, current) { - // return previous.selectedOptions != current.selectedOptions; - // }, + buildWhen: (previous, current) => + previous.selectedOptions != current.selectedOptions, builder: (context, state) { if (state.selectedOptions .where((element) => element.id == widget.groupId) @@ -54,13 +51,10 @@ class _BoardSelectOptionCellState extends State { ), ) .toList(); - return Padding( - padding: EdgeInsets.only(top: BoardSizes.cardCellVPadding), - child: Align( - alignment: Alignment.centerLeft, - child: AbsorbPointer( - child: Wrap(children: children, spacing: 4, runSpacing: 2), - ), + return Align( + alignment: Alignment.centerLeft, + child: AbsorbPointer( + child: Wrap(children: children, spacing: 4, runSpacing: 2), ), ); } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart index d7c4e67bc1..deea60e793 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart @@ -4,8 +4,6 @@ import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'define.dart'; - class BoardTextCell extends StatefulWidget { final String groupId; final GridCellControllerBuilder cellControllerBuilder; @@ -37,20 +35,16 @@ class _BoardTextCellState extends State { return BlocProvider.value( value: _cellBloc, child: BlocBuilder( + buildWhen: (previous, current) => previous.content != current.content, builder: (context, state) { if (state.content.isEmpty) { return const SizedBox(); } else { return Align( alignment: Alignment.centerLeft, - child: Padding( - padding: EdgeInsets.symmetric( - vertical: BoardSizes.cardCellVPadding, - ), - child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 120), - child: FlowyText.medium(state.content, fontSize: 14), - ), + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 120), + child: FlowyText.medium(state.content, fontSize: 14), ), ); } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart index a70414baf4..40cdec7c2f 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart @@ -4,8 +4,6 @@ import 'package:flowy_infra/theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'define.dart'; - class BoardUrlCell extends StatefulWidget { final String groupId; final GridCellControllerBuilder cellControllerBuilder; @@ -38,24 +36,21 @@ class _BoardUrlCellState extends State { return BlocProvider.value( value: _cellBloc, child: BlocBuilder( + buildWhen: (previous, current) => previous.content != current.content, builder: (context, state) { if (state.content.isEmpty) { return const SizedBox(); } else { - return Padding( - padding: - EdgeInsets.symmetric(vertical: BoardSizes.cardCellVPadding), - child: Align( - alignment: Alignment.centerLeft, - child: RichText( - textAlign: TextAlign.left, - text: TextSpan( - text: state.content, - style: TextStyle( - color: theme.main2, - fontSize: 14, - decoration: TextDecoration.underline, - ), + return Align( + alignment: Alignment.centerLeft, + child: RichText( + textAlign: TextAlign.left, + text: TextSpan( + text: state.content, + style: TextStyle( + color: theme.main2, + fontSize: 14, + decoration: TextDecoration.underline, ), ), ), diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart index 0fcce45fa9..65c7d3dade 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart @@ -75,7 +75,7 @@ class _BoardCardState extends State { (cellId) { final child = widget.cellBuilder.buildCell(widget.groupId, cellId); return Padding( - padding: const EdgeInsets.symmetric(horizontal: 6), + padding: const EdgeInsets.only(left: 4, right: 4, top: 6), child: child, ); }, From 2b2bae80ef0d2cc964b178a2bd965bfc6251b438 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 26 Aug 2022 10:53:20 +0800 Subject: [PATCH 86/86] chore: update example project and example.json --- .../example/assets/example.json | 318 ++++-------------- .../appflowy_editor/example/lib/main.dart | 49 +-- .../flutter/generated_plugin_registrant.cc | 4 - .../linux/flutter/generated_plugins.cmake | 1 - .../Flutter/GeneratedPluginRegistrant.swift | 2 - .../example/macos/Podfile.lock | 6 - .../appflowy_editor/example/pubspec.yaml | 1 - .../flutter/generated_plugin_registrant.cc | 3 - .../windows/flutter/generated_plugins.cmake | 1 - 9 files changed, 97 insertions(+), 288 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json b/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json index b522a68594..48184a6511 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json +++ b/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json @@ -1,276 +1,102 @@ { "document": { "type": "editor", - "attributes": {}, "children": [ { "type": "image", "attributes": { - "image_src": "https://images.squarespace-cdn.com/content/v1/617f6f16b877c06711e87373/c3f23723-37f4-44d7-9c5d-6e2a53064ae7/Asset+10.png", + "image_src": "https://s1.ax1x.com/2022/08/26/v2sSbR.jpg", "align": "center" } }, { "type": "text", + "attributes": { "subtype": "heading", "heading": "h1" }, "delta": [ + { "insert": "👋 " }, + { "insert": "Welcome to ", "attributes": { "bold": true } }, { - "insert": "🌶 Read Me" - } - ], - "attributes": { - "subtype": "heading", - "heading": "h1" - } - }, - { - "type": "text", - "delta": [ - { - "insert": "👋 Welcome to FlowyEditor" - } - ], - "attributes": { - "subtype": "heading", - "heading": "h2" - } - }, - { - "type": "text", - "delta": [ - { - "insert": "To be honest, we are still in the alpha stage. There are still many functions that need to be completed. And we are developing more features. Please give us a star if the " - }, - { - "insert": "FlowyEditor", + "insert": "AppFlowy Editor", "attributes": { - "href": "https://github.com/AppFlowy-IO/AppFlowy" + "href": "appflowy.io", + "italic": true, + "bold": true + } + } + ] + }, + { "type": "text", "delta": [] }, + { + "type": "text", + "delta": [ + { "insert": "AppFlowy Editor is a " }, + { "insert": "highly customizable", "attributes": { "bold": true } }, + { "insert": " " }, + { "insert": "rich-text editor", "attributes": { "italic": true } }, + { "insert": " for " }, + { "insert": "Flutter", "attributes": { "underline": true } } + ] + }, + { + "type": "text", + "attributes": { "checkbox": true, "subtype": "checkbox" }, + "delta": [{ "insert": "Customizable" }] + }, + { + "type": "text", + "attributes": { "checkbox": true, "subtype": "checkbox" }, + "delta": [{ "insert": "Test-covered" }] + }, + { + "type": "text", + "attributes": { "checkbox": false, "subtype": "checkbox" }, + "delta": [{ "insert": "more to come!" }] + }, + { "type": "text", "delta": [] }, + { + "type": "text", + "attributes": { "subtype": "quote" }, + "delta": [{ "insert": "Here is an exmaple you can give it a try" }] + }, + { "type": "text", "delta": [] }, + { + "type": "text", + "delta": [ + { "insert": "You can also use " }, + { + "insert": "AppFlowy Editor", + "attributes": { + "italic": true, + "bold": true, + "backgroundColor": "0x6000BCF0" } }, + { "insert": " as a component to build your own app." } + ] + }, + { "type": "text", "delta": [] }, + { + "type": "text", + "attributes": { "subtype": "bulleted-list" }, + "delta": [{ "insert": "Use / to insert blocks" }] + }, + { + "type": "text", + "attributes": { "subtype": "bulleted-list" }, + "delta": [ { - "insert": " helps you. 😊😊😊" + "insert": "Select text to trigger to the toolbar to format your notes." } ] }, + { "type": "text", "delta": [] }, { "type": "text", "delta": [ { - "insert": "Since the FlowyEditor are a community-driven open source editor, we very welcome and appreciate every pull request submissions from everyone.😄😄😄" + "insert": "If you have questions or feedback, please submit an issue on Github or join the community along with 1000+ builders!" } ] - }, - { - "type": "text", - "delta": [ - { - "insert": "Here are the basics:" - } - ], - "attributes": { - "subtype": "heading", - "heading": "h3" - } - }, - { - "type": "text", - "delta": [ - { "insert": "Click " }, - { "insert": "anywhere", "attributes": { "underline": true } }, - { "insert": " and just typing." } - ] - }, - { - "type": "text", - "delta": [ - { - "insert": "Hit" - }, - { - "insert": " / ", - "attributes": { "backgroundColor": "0xFFFFFF00" } - }, - { - "insert": "to see all the types of content you can add - headers, bulleted lists, checkboxes, etc." - } - ] - }, - { - "type": "text", - "delta": [ - { - "insert": "Highlight", - "attributes": { "backgroundColor": "0xFF00BCFB" } - }, - { - "insert": " any text, and use the menu that pops up to " - }, - { "insert": "style", "attributes": { "bold": true } }, - { "insert": " your ", "attributes": { "italic": true } }, - { "insert": "writing", "attributes": { "strikethrough": true } }, - { "insert": "." } - ] - }, - { - "type": "text", - "delta": [ - { - "insert": "Here are the plugins:" - } - ], - "attributes": { - "subtype": "heading", - "heading": "h3" - } - }, - { - "type": "image", - "attributes": { - "image_src": "https://s1.ax1x.com/2022/08/24/vgAJED.png", - "align": "left", - "width": 300 - } - }, - { - "type": "text", - "delta": [ - { - "insert": "Hello world" - } - ], - "attributes": { - "subtype": "checkbox", - "checkbox": false - } - }, - { - "type": "text", - "delta": [ - { - "insert": "Hello world" - } - ], - "attributes": { - "subtype": "checkbox", - "checkbox": false - } - }, - { - "type": "text", - "delta": [ - { - "insert": "Hello world" - } - ], - "attributes": { - "subtype": "checkbox", - "checkbox": false - } - }, - { - "type": "text", - "delta": [ - { - "insert": "Hello world" - } - ], - "attributes": { - "subtype": "bulleted-list" - } - }, - { - "type": "text", - "delta": [ - { - "insert": "Hello world" - } - ], - "attributes": { - "subtype": "bulleted-list" - } - }, - { - "type": "text", - "delta": [ - { - "insert": "Hello " - }, - { - "insert": "world", - "attributes": { "bold": true } - } - ], - "attributes": { - "subtype": "bulleted-list" - } - }, - { - "type": "text", - "delta": [ - { - "insert": "Hello world" - } - ], - "attributes": { - "subtype": "quote" - } - }, - { - "type": "text", - "delta": [ - { - "insert": "Hello world" - } - ], - "attributes": { - "subtype": "quote" - } - }, - { - "type": "text", - "delta": [ - { - "insert": "Hello world" - } - ], - "attributes": { - "subtype": "quote" - } - }, - { - "type": "text", - "delta": [ - { - "insert": "Hello world" - } - ], - "attributes": { - "subtype": "number-list", - "number": 1 - } - }, - { - "type": "text", - "delta": [ - { - "insert": "Hello world" - } - ], - "attributes": { - "subtype": "number-list", - "number": 2 - } - }, - { - "type": "text", - "delta": [ - { - "insert": "Hello world" - } - ], - "attributes": { - "subtype": "number-list", - "number": 3 - } } ] } diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart index 35dd8161db..e72739e246 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart @@ -20,6 +20,7 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( + debugShowCheckedModeBanner: false, theme: ThemeData( primarySwatch: Colors.blue, ), @@ -54,21 +55,27 @@ class _MyHomePageState extends State { Widget _buildEditor(BuildContext context) { if (_jsonString != null) { - return _buildEditorWithJsonPath(_jsonString!); + return _buildEditorWithJsonString(_jsonString!); } if (_pageIndex == 0) { - return _buildEditorWithJsonPath( - rootBundle.loadString('assets/example.json')); + return _buildEditorWithJsonString( + rootBundle.loadString('assets/example.json'), + ); } else if (_pageIndex == 1) { - return _buildEditorWithJsonPath( - rootBundle.loadString('assets/big_document.json')); + return _buildEditorWithJsonString( + rootBundle.loadString('assets/big_document.json'), + ); } else if (_pageIndex == 2) { - return _buildEditorWithEmptyDocument(); + return _buildEditorWithJsonString( + Future.value( + jsonEncode(EditorState.empty().document.toJson()), + ), + ); } throw UnimplementedError(); } - Widget _buildEditorWithJsonPath(Future jsonString) { + Widget _buildEditorWithJsonString(Future jsonString) { return FutureBuilder( future: jsonString, builder: (_, snapshot) { @@ -86,7 +93,7 @@ class _MyHomePageState extends State { debugPrint(message); }; return Container( - padding: const EdgeInsets.only(left: 20, right: 20), + padding: const EdgeInsets.all(20), child: AppFlowyEditor( editorState: _editorState, ), @@ -100,19 +107,6 @@ class _MyHomePageState extends State { ); } - Widget _buildEditorWithEmptyDocument() { - _editorState = EditorState.empty(); - _editorState.logConfiguration - ..level = LogLevel.all - ..handler = (message) { - debugPrint(message); - }; - final editor = AppFlowyEditor( - editorState: _editorState, - ); - return editor; - } - Widget _buildExpandableFab() { return ExpandableFab( distance: 112.0, @@ -130,9 +124,8 @@ class _MyHomePageState extends State { onPressed: () => _switchToPage(2), ), ActionButton( - icon: const Icon(Icons.print), - onPressed: () => _exportDocument(_editorState), - ), + icon: const Icon(Icons.print), + onPressed: () => {_exportDocument(_editorState)}), ActionButton( icon: const Icon(Icons.import_export), onPressed: () => _importDocument(), @@ -148,6 +141,14 @@ class _MyHomePageState extends State { final path = directory.path; final file = File('$path/editor.json'); await file.writeAsString(json); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('The document is saved to the ${file.path}'), + ), + ); + } } void _importDocument() async { diff --git a/frontend/app_flowy/packages/appflowy_editor/example/linux/flutter/generated_plugin_registrant.cc b/frontend/app_flowy/packages/appflowy_editor/example/linux/flutter/generated_plugin_registrant.cc index 45a19e066d..00fd3bc03f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/linux/flutter/generated_plugin_registrant.cc +++ b/frontend/app_flowy/packages/appflowy_editor/example/linux/flutter/generated_plugin_registrant.cc @@ -6,14 +6,10 @@ #include "generated_plugin_registrant.h" -#include #include #include void fl_register_plugins(FlPluginRegistry* registry) { - g_autoptr(FlPluginRegistrar) file_saver_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "FileSaverPlugin"); - file_saver_plugin_register_with_registrar(file_saver_registrar); g_autoptr(FlPluginRegistrar) rich_clipboard_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "RichClipboardPlugin"); rich_clipboard_plugin_register_with_registrar(rich_clipboard_linux_registrar); diff --git a/frontend/app_flowy/packages/appflowy_editor/example/linux/flutter/generated_plugins.cmake b/frontend/app_flowy/packages/appflowy_editor/example/linux/flutter/generated_plugins.cmake index 10140a75b4..0342e3868a 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/linux/flutter/generated_plugins.cmake +++ b/frontend/app_flowy/packages/appflowy_editor/example/linux/flutter/generated_plugins.cmake @@ -3,7 +3,6 @@ # list(APPEND FLUTTER_PLUGIN_LIST - file_saver rich_clipboard_linux url_launcher_linux ) diff --git a/frontend/app_flowy/packages/appflowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift b/frontend/app_flowy/packages/appflowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift index ac60ac6ec9..08b7c3b866 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/frontend/app_flowy/packages/appflowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,14 +5,12 @@ import FlutterMacOS import Foundation -import file_saver import path_provider_macos import rich_clipboard_macos import url_launcher_macos import wakelock_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) RichClipboardPlugin.register(with: registry.registrar(forPlugin: "RichClipboardPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) diff --git a/frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock b/frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock index c56e297a68..1fcb47735c 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock +++ b/frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock @@ -1,6 +1,4 @@ PODS: - - file_saver (0.0.1): - - FlutterMacOS - FlutterMacOS (1.0.0) - path_provider_macos (0.0.1): - FlutterMacOS @@ -12,7 +10,6 @@ PODS: - FlutterMacOS DEPENDENCIES: - - file_saver (from `Flutter/ephemeral/.symlinks/plugins/file_saver/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`) - rich_clipboard_macos (from `Flutter/ephemeral/.symlinks/plugins/rich_clipboard_macos/macos`) @@ -20,8 +17,6 @@ DEPENDENCIES: - wakelock_macos (from `Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos`) EXTERNAL SOURCES: - file_saver: - :path: Flutter/ephemeral/.symlinks/plugins/file_saver/macos FlutterMacOS: :path: Flutter/ephemeral path_provider_macos: @@ -34,7 +29,6 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos SPEC CHECKSUMS: - file_saver: 44e6fbf666677faf097302460e214e977fdd977b FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 rich_clipboard_macos: 43364b66b9dc69d203eb8dd6d758e2d12e02723c diff --git a/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml b/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml index 9f2fae24bd..5ba51433d6 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml +++ b/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml @@ -40,7 +40,6 @@ dependencies: video_player: ^2.4.5 pod_player: 0.0.8 flutter_inappwebview: ^5.4.3+7 - file_saver: ^0.1.1 path_provider: ^2.0.11 dev_dependencies: diff --git a/frontend/app_flowy/packages/appflowy_editor/example/windows/flutter/generated_plugin_registrant.cc b/frontend/app_flowy/packages/appflowy_editor/example/windows/flutter/generated_plugin_registrant.cc index 391ef56268..4f7884874d 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/windows/flutter/generated_plugin_registrant.cc +++ b/frontend/app_flowy/packages/appflowy_editor/example/windows/flutter/generated_plugin_registrant.cc @@ -6,12 +6,9 @@ #include "generated_plugin_registrant.h" -#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { - FileSaverPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("FileSaverPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/frontend/app_flowy/packages/appflowy_editor/example/windows/flutter/generated_plugins.cmake b/frontend/app_flowy/packages/appflowy_editor/example/windows/flutter/generated_plugins.cmake index 047111654d..88b22e5c77 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/windows/flutter/generated_plugins.cmake +++ b/frontend/app_flowy/packages/appflowy_editor/example/windows/flutter/generated_plugins.cmake @@ -3,7 +3,6 @@ # list(APPEND FLUTTER_PLUGIN_LIST - file_saver url_launcher_windows )