From e28e5a0649ebee0f2d816f9373aef423bdd2ebe7 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Sat, 28 Oct 2023 11:48:28 +0800 Subject: [PATCH 01/56] refactor: rename group (#3815) * chore: add group operation interceptor * refactor: impl interceptor trait * chore: update type option when group change * test: fix test --- frontend/appflowy_tauri/src-tauri/Cargo.lock | 21 +- frontend/appflowy_tauri/src-tauri/Cargo.toml | 16 +- frontend/rust-lib/Cargo.lock | 21 +- frontend/rust-lib/Cargo.toml | 16 +- .../tests/database/local_test/test.rs | 2 +- .../flowy-database2/src/event_handler.rs | 24 +- .../src/services/database/database_editor.rs | 244 ++++++++++-------- .../src/services/database_view/mod.rs | 2 + .../src/services/database_view/view_editor.rs | 217 +++++----------- .../src/services/database_view/view_filter.rs | 8 +- .../src/services/database_view/view_group.rs | 48 +++- .../services/database_view/view_operation.rs | 126 +++++++++ .../src/services/database_view/view_sort.rs | 8 +- .../src/services/database_view/views.rs | 66 ++--- .../selection_type_option/select_ids.rs | 1 - .../field/type_options/type_option.rs | 10 +- .../field/type_options/type_option_cell.rs | 9 +- .../src/services/filter/controller.rs | 2 +- .../src/services/group/action.rs | 40 ++- .../src/services/group/configuration.rs | 19 +- .../src/services/group/controller.rs | 155 +++++------ .../controller_impls/checkbox_controller.rs | 63 +++-- .../group/controller_impls/date_controller.rs | 128 ++++----- .../controller_impls/default_controller.rs | 13 +- .../multi_select_controller.rs | 116 +++++---- .../single_select_controller.rs | 115 +++++---- .../select_option_controller/util.rs | 11 +- .../group/controller_impls/url_controller.rs | 81 ++++-- .../src/services/group/entities.rs | 8 +- .../src/services/group/group_builder.rs | 102 +++++++- .../src/services/sort/controller.rs | 2 +- .../tests/database/group_test/script.rs | 26 -- frontend/rust-lib/flowy-error/Cargo.toml | 1 + frontend/rust-lib/flowy-error/src/errors.rs | 6 + 34 files changed, 982 insertions(+), 745 deletions(-) create mode 100644 frontend/rust-lib/flowy-database2/src/services/database_view/view_operation.rs diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index c43f9d217cd3..545b6438e437 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -454,7 +454,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" dependencies = [ "borsh-derive", - "hashbrown 0.13.2", + "hashbrown 0.12.3", ] [[package]] @@ -854,7 +854,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" dependencies = [ "anyhow", "async-trait", @@ -873,7 +873,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" dependencies = [ "anyhow", "async-trait", @@ -903,7 +903,7 @@ dependencies = [ [[package]] name = "collab-derive" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" dependencies = [ "proc-macro2", "quote", @@ -915,7 +915,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" dependencies = [ "anyhow", "collab", @@ -935,7 +935,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" dependencies = [ "anyhow", "bytes", @@ -949,7 +949,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" dependencies = [ "anyhow", "chrono", @@ -991,7 +991,7 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" dependencies = [ "async-trait", "bincode", @@ -1012,7 +1012,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" dependencies = [ "anyhow", "async-trait", @@ -1039,7 +1039,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" dependencies = [ "anyhow", "collab", @@ -2115,6 +2115,7 @@ dependencies = [ "serde_json", "serde_repr", "thiserror", + "tokio", "tokio-postgres", "url", "validator", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index 3ee84542e581..980d4cb0936a 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -48,14 +48,14 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c87 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ff10abd5" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ff10abd5" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ff10abd5" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ff10abd5" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ff10abd5" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ff10abd5" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ff10abd5" } -collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ff10abd5" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "8861d7a" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "8861d7a" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "8861d7a" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "8861d7a" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "8861d7a" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "8861d7a" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "8861d7a" } +collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "8861d7a" } diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 041ccd56c116..e94ab9c83239 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -461,7 +461,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" dependencies = [ "borsh-derive", - "hashbrown 0.13.2", + "hashbrown 0.12.3", ] [[package]] @@ -721,7 +721,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5e#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" dependencies = [ "anyhow", "async-trait", @@ -740,7 +740,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5e#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" dependencies = [ "anyhow", "async-trait", @@ -770,7 +770,7 @@ dependencies = [ [[package]] name = "collab-derive" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5e#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" dependencies = [ "proc-macro2", "quote", @@ -782,7 +782,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5e#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" dependencies = [ "anyhow", "collab", @@ -802,7 +802,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5e#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" dependencies = [ "anyhow", "bytes", @@ -816,7 +816,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5e#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" dependencies = [ "anyhow", "chrono", @@ -858,7 +858,7 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5e#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" dependencies = [ "async-trait", "bincode", @@ -879,7 +879,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5e#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" dependencies = [ "anyhow", "async-trait", @@ -906,7 +906,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ff10abd5e#ff10abd5e41b9a7d118cbb79f8c2bf9ac27c0ded" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" dependencies = [ "anyhow", "collab", @@ -1938,6 +1938,7 @@ dependencies = [ "serde_json", "serde_repr", "thiserror", + "tokio", "tokio-postgres", "url", "validator", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 1aaf419f157c..1c81d09ff4fa 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -92,11 +92,11 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c87 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ff10abd5e" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ff10abd5e" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ff10abd5e" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ff10abd5e" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ff10abd5e" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ff10abd5e" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ff10abd5e" } -collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ff10abd5e" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "8861d7a" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "8861d7a" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "8861d7a" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "8861d7a" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "8861d7a" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "8861d7a" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "8861d7a" } +collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "8861d7a" } diff --git a/frontend/rust-lib/event-integration/tests/database/local_test/test.rs b/frontend/rust-lib/event-integration/tests/database/local_test/test.rs index 3b3cdd171e32..5ed18809fc8d 100644 --- a/frontend/rust-lib/event-integration/tests/database/local_test/test.rs +++ b/frontend/rust-lib/event-integration/tests/database/local_test/test.rs @@ -761,7 +761,7 @@ async fn rename_group_event_test() { } #[tokio::test] -async fn hide_group_event_test2() { +async fn hide_group_event_test() { let test = EventIntegrationTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let board_view = test diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index b72e2e73c516..310311181244 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -2,6 +2,7 @@ use std::sync::{Arc, Weak}; use collab_database::database::gen_row_id; use collab_database::rows::RowId; +use tokio::sync::oneshot; use flowy_error::{FlowyError, FlowyResult}; use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; @@ -15,7 +16,7 @@ use crate::services::field::{ type_option_data_from_pb_or_default, DateCellChangeset, SelectOptionCellChangeset, }; use crate::services::field_settings::FieldSettingsChangesetParams; -use crate::services::group::{GroupChangeset, GroupChangesets}; +use crate::services::group::GroupChangeset; use crate::services::share::csv::CSVFormat; fn upgrade_manager( @@ -725,18 +726,15 @@ pub(crate) async fn update_group_handler( let view_id = params.view_id.clone(); let database_editor = manager.get_database_with_view_id(&view_id).await?; let group_changeset = GroupChangeset::from(params); - database_editor - .update_group(&view_id, group_changeset.clone()) - .await?; - database_editor - .update_group_setting( - &view_id, - GroupChangesets { - update_groups: vec![group_changeset], - }, - ) - .await?; - + let (tx, rx) = oneshot::channel(); + tokio::spawn(async move { + let result = database_editor + .update_group(&view_id, vec![group_changeset].into()) + .await; + let _ = tx.send(result); + }); + + let _ = rx.await?; Ok(()) } diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index 6bafa9361a2d..b51252a54d18 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -8,10 +8,11 @@ use collab_database::rows::{Cell, Cells, CreateRowParams, Row, RowCell, RowDetai use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting}; use futures::StreamExt; use tokio::sync::{broadcast, RwLock}; +use tracing::{event, warn}; use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult}; use flowy_task::TaskDispatcher; -use lib_infra::future::{to_fut, Fut}; +use lib_infra::future::{to_fut, Fut, FutureResult}; use crate::entities::*; use crate::notification::{send_notification, DatabaseNotification}; @@ -20,7 +21,9 @@ use crate::services::cell::{ }; use crate::services::database::util::database_view_setting_pb_from_view; use crate::services::database::UpdatedRow; -use crate::services::database_view::{DatabaseViewChanged, DatabaseViewData, DatabaseViews}; +use crate::services::database_view::{ + DatabaseViewChanged, DatabaseViewEditor, DatabaseViewOperation, DatabaseViews, EditorByViewId, +}; use crate::services::field::checklist_type_option::ChecklistCellChangeset; use crate::services::field::{ default_type_option_data_from_type, select_type_option_from_field, transform_type_option, @@ -32,8 +35,7 @@ use crate::services::field_settings::{ }; use crate::services::filter::Filter; use crate::services::group::{ - default_group_setting, GroupChangeset, GroupChangesets, GroupSetting, GroupSettingChangeset, - RowChangeset, + default_group_setting, GroupChangesets, GroupSetting, GroupSettingChangeset, RowChangeset, }; use crate::services::share::csv::{CSVExport, CSVFormat}; use crate::services::sort::Sort; @@ -51,12 +53,6 @@ impl DatabaseEditor { task_scheduler: Arc>, ) -> FlowyResult { let cell_cache = AnyTypeCache::::new(); - let database_view_data = Arc::new(DatabaseViewDataImpl { - database: database.clone(), - task_scheduler: task_scheduler.clone(), - cell_cache: cell_cache.clone(), - }); - let database_id = database.lock().get_database_id(); // Receive database sync state and send to frontend via the notification @@ -93,8 +89,24 @@ impl DatabaseEditor { } }); - let database_views = - Arc::new(DatabaseViews::new(database.clone(), cell_cache.clone(), database_view_data).await?); + // Used to cache the view of the database for fast access. + let editor_by_view_id = Arc::new(RwLock::new(EditorByViewId::default())); + let view_operation = Arc::new(DatabaseViewOperationImpl { + database: database.clone(), + task_scheduler: task_scheduler.clone(), + cell_cache: cell_cache.clone(), + editor_by_view_id: editor_by_view_id.clone(), + }); + + let database_views = Arc::new( + DatabaseViews::new( + database.clone(), + cell_cache.clone(), + view_operation, + editor_by_view_id, + ) + .await?, + ); Ok(Self { database, cell_cache, @@ -177,40 +189,9 @@ impl DatabaseEditor { Ok(self.database.lock().delete_view(view_id)) } - pub async fn update_group_setting( - &self, - view_id: &str, - group_setting_changeset: GroupChangesets, - ) -> FlowyResult<()> { + pub async fn update_group(&self, view_id: &str, changesets: GroupChangesets) -> FlowyResult<()> { let view_editor = self.database_views.get_view_editor(view_id).await?; - view_editor - .v_update_group_setting(group_setting_changeset) - .await?; - Ok(()) - } - - pub async fn update_group( - &self, - view_id: &str, - group_changeset: GroupChangeset, - ) -> FlowyResult<()> { - let view_editor = self.database_views.get_view_editor(view_id).await?; - let type_option = view_editor.update_group(group_changeset.clone()).await?; - - if let Some(type_option_data) = type_option { - let field = self.get_field(&group_changeset.field_id); - if field.is_some() { - let _ = self - .update_field_type_option( - view_id, - &group_changeset.field_id, - type_option_data, - field.unwrap(), - ) - .await; - } - } - + view_editor.v_update_group(changesets).await?; Ok(()) } @@ -295,9 +276,7 @@ impl DatabaseEditor { .set_width_at_if_not_none(params.width.map(|value| value as i64)) .set_visibility_if_not_none(params.visibility); }); - self - .notify_did_update_database_field(¶ms.field_id) - .await?; + notify_did_update_database_field(&self.database, ¶ms.field_id)?; Ok(()) } @@ -331,30 +310,13 @@ impl DatabaseEditor { pub async fn update_field_type_option( &self, view_id: &str, - field_id: &str, + _field_id: &str, type_option_data: TypeOptionData, old_field: Field, ) -> FlowyResult<()> { - let field_type = FieldType::from(old_field.field_type); - self - .database - .lock() - .fields - .update_field(field_id, |update| { - if old_field.is_primary { - tracing::warn!("Cannot update primary field type"); - } else { - update.update_type_options(|type_options_update| { - type_options_update.insert(&field_type.to_string(), type_option_data); - }); - } - }); + let view_editor = self.database_views.get_view_editor(view_id).await?; + update_field_type_option_fn(&self.database, &view_editor, type_option_data, old_field).await?; - self - .database_views - .did_update_field_type_option(view_id, field_id, &old_field) - .await?; - let _ = self.notify_did_update_database_field(field_id).await; Ok(()) } @@ -398,7 +360,7 @@ impl DatabaseEditor { }, } - self.notify_did_update_database_field(field_id).await?; + notify_did_update_database_field(&self.database, field_id)?; Ok(()) } @@ -435,7 +397,7 @@ impl DatabaseEditor { let params = self.database.lock().duplicate_row(row_id); match params { None => { - tracing::warn!("Failed to duplicate row: {}", row_id); + warn!("Failed to duplicate row: {}", row_id); }, Some(params) => { let _ = self.create_row(view_id, group_id, params).await; @@ -585,7 +547,7 @@ impl DatabaseEditor { cover: row_meta.cover_url, }) } else { - tracing::warn!("the row:{} is exist in view:{}", row_id.as_str(), view_id); + warn!("the row:{} is exist in view:{}", row_id.as_str(), view_id); None } } @@ -594,7 +556,7 @@ impl DatabaseEditor { if self.database.lock().views.is_row_exist(view_id, row_id) { self.database.lock().get_row_detail(row_id) } else { - tracing::warn!("the row:{} is exist in view:{}", row_id.as_str(), view_id); + warn!("the row:{} is exist in view:{}", row_id.as_str(), view_id); None } } @@ -984,7 +946,7 @@ impl DatabaseEditor { let row_detail = self.get_row_detail(view_id, &from_row); match row_detail { None => { - tracing::warn!( + warn!( "Move row between group failed, can not find the row:{}", from_row ) @@ -1054,7 +1016,7 @@ impl DatabaseEditor { match self.database_views.get_view_editor(view_id).await { Ok(view) => view.v_get_all_calendar_events().await.unwrap_or_default(), Err(_) => { - tracing::warn!("Can not find the view: {}", view_id); + warn!("Can not find the view: {}", view_id); vec![] }, } @@ -1066,7 +1028,7 @@ impl DatabaseEditor { view_id: &str, ) -> FlowyResult> { let _database_view = self.database_views.get_view_editor(view_id).await?; - todo!() + Ok(vec![]) } #[tracing::instrument(level = "trace", skip_all)] @@ -1084,28 +1046,6 @@ impl DatabaseEditor { Ok(()) } - #[tracing::instrument(level = "trace", skip_all, err)] - async fn notify_did_update_database_field(&self, field_id: &str) -> FlowyResult<()> { - let (database_id, field) = { - let database = self.database.lock(); - let database_id = database.get_database_id(); - let field = database.fields.get_field(field_id); - (database_id, field) - }; - - if let Some(field) = field { - let updated_field = FieldPB::from(field); - let notified_changeset = - DatabaseFieldChangesetPB::update(&database_id, vec![updated_field.clone()]); - self.notify_did_update_database(notified_changeset).await?; - send_notification(field_id, DatabaseNotification::DidUpdateField) - .payload(updated_field) - .send(); - } - - Ok(()) - } - async fn notify_did_update_database( &self, changeset: DatabaseFieldChangesetPB, @@ -1134,7 +1074,7 @@ impl DatabaseEditor { pub async fn get_database_data(&self, view_id: &str) -> FlowyResult { let database_view = self.database_views.get_view_editor(view_id).await?; let view = database_view - .get_view() + .v_get_view() .await .ok_or_else(FlowyError::record_not_found)?; let rows = database_view.v_get_rows().await; @@ -1284,13 +1224,14 @@ fn cell_changesets_from_cell_by_field_id( .collect() } -struct DatabaseViewDataImpl { +struct DatabaseViewOperationImpl { database: Arc, task_scheduler: Arc>, cell_cache: CellCache, + editor_by_view_id: Arc>, } -impl DatabaseViewData for DatabaseViewDataImpl { +impl DatabaseViewOperation for DatabaseViewOperationImpl { fn get_database(&self) -> Arc { self.database.clone() } @@ -1305,14 +1246,8 @@ impl DatabaseViewData for DatabaseViewDataImpl { to_fut(async move { fields.into_iter().map(Arc::new).collect() }) } - fn get_field(&self, field_id: &str) -> Fut>> { - let field = self - .database - .lock() - .fields - .get_field(field_id) - .map(Arc::new); - to_fut(async move { field }) + fn get_field(&self, field_id: &str) -> Option { + self.database.lock().fields.get_field(field_id) } fn create_field( @@ -1336,6 +1271,29 @@ impl DatabaseViewData for DatabaseViewDataImpl { to_fut(async move { field }) } + fn update_field( + &self, + view_id: &str, + type_option_data: TypeOptionData, + old_field: Field, + ) -> FutureResult<(), FlowyError> { + let view_id = view_id.to_string(); + let weak_editor_by_view_id = Arc::downgrade(&self.editor_by_view_id); + let weak_database = Arc::downgrade(&self.database); + FutureResult::new(async move { + if let (Some(database), Some(editor_by_view_id)) = + (weak_database.upgrade(), weak_editor_by_view_id.upgrade()) + { + let view_editor = editor_by_view_id.read().await.get(&view_id).cloned(); + if let Some(view_editor) = view_editor { + let _ = + update_field_type_option_fn(&database, &view_editor, type_option_data, old_field).await; + } + } + Ok(()) + }) + } + fn get_primary_field(&self) -> Fut>> { let field = self .database @@ -1559,3 +1517,73 @@ impl DatabaseViewData for DatabaseViewDataImpl { .send() } } + +#[tracing::instrument(level = "trace", skip_all, err)] +pub async fn update_field_type_option_fn( + database: &Arc, + view_editor: &Arc, + type_option_data: TypeOptionData, + old_field: Field, +) -> FlowyResult<()> { + if type_option_data.is_empty() { + warn!("Update type option with empty data"); + return Ok(()); + } + let field_type = FieldType::from(old_field.field_type); + database + .lock() + .fields + .update_field(&old_field.id, |update| { + if old_field.is_primary { + warn!("Cannot update primary field type"); + } else { + update.update_type_options(|type_options_update| { + event!( + tracing::Level::TRACE, + "insert type option to field type: {:?}", + field_type + ); + type_options_update.insert(&field_type.to_string(), type_option_data); + }); + } + }); + + let _ = notify_did_update_database_field(database, &old_field.id); + view_editor + .v_did_update_field_type_option(&old_field) + .await?; + Ok(()) +} + +#[tracing::instrument(level = "trace", skip_all, err)] +fn notify_did_update_database_field( + database: &Arc, + field_id: &str, +) -> FlowyResult<()> { + let (database_id, field, views) = { + let database = database + .try_lock() + .ok_or(FlowyError::internal().with_context("fail to acquire the lock of database"))?; + let database_id = database.get_database_id(); + let field = database.fields.get_field(field_id); + let views = database.get_all_views_description(); + (database_id, field, views) + }; + + if let Some(field) = field { + let updated_field = FieldPB::from(field); + let notified_changeset = + DatabaseFieldChangesetPB::update(&database_id, vec![updated_field.clone()]); + + for view in views { + send_notification(&view.id, DatabaseNotification::DidUpdateFields) + .payload(notified_changeset.clone()) + .send(); + } + + send_notification(field_id, DatabaseNotification::DidUpdateField) + .payload(updated_field) + .send(); + } + Ok(()) +} diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/mod.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/mod.rs index da4ad5bc522c..6522c8e917e1 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/mod.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/mod.rs @@ -1,6 +1,7 @@ pub use layout_deps::*; pub use notifier::*; pub use view_editor::*; +pub use view_operation::*; pub use views::*; mod layout_deps; @@ -8,6 +9,7 @@ mod notifier; mod view_editor; mod view_filter; mod view_group; +mod view_operation; mod view_sort; mod views; // mod trait_impl; diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs index 4c5266c708db..66d976047660 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs @@ -2,15 +2,13 @@ use std::borrow::Cow; use std::collections::HashMap; use std::sync::Arc; -use collab_database::database::{gen_database_filter_id, gen_database_sort_id, MutexDatabase}; +use collab_database::database::{gen_database_filter_id, gen_database_sort_id}; use collab_database::fields::{Field, TypeOptionData}; -use collab_database::rows::{Cells, Row, RowCell, RowDetail, RowId}; -use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting}; +use collab_database::rows::{Cells, Row, RowDetail, RowId}; +use collab_database::views::{DatabaseLayout, DatabaseView}; use tokio::sync::{broadcast, RwLock}; use flowy_error::{FlowyError, FlowyResult}; -use flowy_task::TaskDispatcher; -use lib_infra::future::Fut; use crate::entities::{ CalendarEventPB, DatabaseLayoutMetaPB, DatabaseLayoutSettingPB, DeleteFilterParams, @@ -25,124 +23,27 @@ use crate::services::database_view::view_filter::make_filter_controller; use crate::services::database_view::view_group::{ get_cell_for_row, get_cells_for_field, new_group_controller, new_group_controller_with_field, }; +use crate::services::database_view::view_operation::DatabaseViewOperation; use crate::services::database_view::view_sort::make_sort_controller; use crate::services::database_view::{ notify_did_update_filter, notify_did_update_group_rows, notify_did_update_num_of_groups, notify_did_update_setting, notify_did_update_sort, DatabaseLayoutDepsResolver, DatabaseViewChangedNotifier, DatabaseViewChangedReceiverRunner, }; -use crate::services::field::TypeOptionCellDataHandler; use crate::services::field_settings::FieldSettings; use crate::services::filter::{ Filter, FilterChangeset, FilterController, FilterType, UpdatedFilterType, }; use crate::services::group::{ - GroupChangeset, GroupChangesets, GroupController, GroupSetting, GroupSettingChangeset, - MoveGroupRowContext, RowChangeset, + GroupChangesets, GroupController, GroupSetting, GroupSettingChangeset, MoveGroupRowContext, + RowChangeset, }; use crate::services::setting::CalendarLayoutSetting; use crate::services::sort::{DeletedSortType, Sort, SortChangeset, SortController, SortType}; -pub trait DatabaseViewData: Send + Sync + 'static { - fn get_database(&self) -> Arc; - - fn get_view(&self, view_id: &str) -> Fut>; - /// If the field_ids is None, then it will return all the field revisions - fn get_fields(&self, view_id: &str, field_ids: Option>) -> Fut>>; - - /// Returns the field with the field_id - fn get_field(&self, field_id: &str) -> Fut>>; - - fn create_field( - &self, - view_id: &str, - name: &str, - field_type: FieldType, - type_option_data: TypeOptionData, - ) -> Fut; - - fn get_primary_field(&self) -> Fut>>; - - /// Returns the index of the row with row_id - fn index_of_row(&self, view_id: &str, row_id: &RowId) -> Fut>; - - /// Returns the `index` and `RowRevision` with row_id - fn get_row(&self, view_id: &str, row_id: &RowId) -> Fut)>>; - - /// Returns all the rows in the view - fn get_rows(&self, view_id: &str) -> Fut>>; - - fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Fut>>; - - fn get_cell_in_row(&self, field_id: &str, row_id: &RowId) -> Fut>; - - /// Return the database layout type for the view with given view_id - /// The default layout type is [DatabaseLayout::Grid] - fn get_layout_for_view(&self, view_id: &str) -> DatabaseLayout; - - fn get_group_setting(&self, view_id: &str) -> Vec; - - fn insert_group_setting(&self, view_id: &str, setting: GroupSetting); - - fn get_sort(&self, view_id: &str, sort_id: &str) -> Option; - - fn insert_sort(&self, view_id: &str, sort: Sort); - - fn remove_sort(&self, view_id: &str, sort_id: &str); - - fn get_all_sorts(&self, view_id: &str) -> Vec; - - fn remove_all_sorts(&self, view_id: &str); - - fn get_all_filters(&self, view_id: &str) -> Vec>; - - fn delete_filter(&self, view_id: &str, filter_id: &str); - - fn insert_filter(&self, view_id: &str, filter: Filter); - - fn get_filter(&self, view_id: &str, filter_id: &str) -> Option; - - fn get_filter_by_field_id(&self, view_id: &str, field_id: &str) -> Option; - - fn get_layout_setting(&self, view_id: &str, layout_ty: &DatabaseLayout) -> Option; - - fn insert_layout_setting( - &self, - view_id: &str, - layout_ty: &DatabaseLayout, - layout_setting: LayoutSetting, - ); - - fn update_layout_type(&self, view_id: &str, layout_type: &DatabaseLayout); - - /// Returns a `TaskDispatcher` used to poll a `Task` - fn get_task_scheduler(&self) -> Arc>; - - fn get_type_option_cell_handler( - &self, - field: &Field, - field_type: &FieldType, - ) -> Option>; - - fn get_field_settings( - &self, - view_id: &str, - field_ids: &[String], - ) -> HashMap; - - fn get_all_field_settings(&self, view_id: &str) -> HashMap; - - fn update_field_settings( - &self, - view_id: &str, - field_id: &str, - visibility: Option, - ); -} - pub struct DatabaseViewEditor { pub view_id: String, - delegate: Arc, + delegate: Arc, group_controller: Arc>>>, filter_controller: Arc, sort_controller: Arc>, @@ -158,14 +59,17 @@ impl Drop for DatabaseViewEditor { impl DatabaseViewEditor { pub async fn new( view_id: String, - delegate: Arc, + delegate: Arc, cell_cache: CellCache, ) -> FlowyResult { let (notifier, _) = broadcast::channel(100); tokio::spawn(DatabaseViewChangedReceiverRunner(Some(notifier.subscribe())).run()); - let group_controller = new_group_controller(view_id.clone(), delegate.clone()).await?; - let group_controller = Arc::new(RwLock::new(group_controller)); + // Group + let group_controller = Arc::new(RwLock::new( + new_group_controller(view_id.clone(), delegate.clone()).await?, + )); + // Filter let filter_controller = make_filter_controller( &view_id, delegate.clone(), @@ -174,6 +78,7 @@ impl DatabaseViewEditor { ) .await; + // Sort let sort_controller = make_sort_controller( &view_id, delegate.clone(), @@ -198,7 +103,7 @@ impl DatabaseViewEditor { self.filter_controller.close().await; } - pub async fn get_view(&self) -> Option { + pub async fn v_get_view(&self) -> Option { self.delegate.get_view(&self.view_id).await } @@ -383,7 +288,7 @@ impl DatabaseViewEditor { let move_row_context = MoveGroupRowContext { row_detail, row_changeset, - field: field.as_ref(), + field: &field, to_group_id, to_row_id, }; @@ -485,40 +390,31 @@ impl DatabaseViewEditor { Ok(result.flatten()) } - pub async fn v_update_group_setting(&self, changeset: GroupChangesets) -> FlowyResult<()> { - self - .mut_group_controller(|group_controller, _| { - group_controller.apply_group_setting_changeset(changeset) - }) - .await; + pub async fn v_update_group(&self, changeset: GroupChangesets) -> FlowyResult<()> { + let mut type_option_data = TypeOptionData::new(); + let old_field = if let Some(controller) = self.group_controller.write().await.as_mut() { + let old_field = self.delegate.get_field(controller.field_id()); + type_option_data.extend(controller.apply_group_changeset(&changeset).await?); + old_field + } else { + None + }; + + if let Some(old_field) = old_field { + if !type_option_data.is_empty() { + self + .delegate + .update_field(&self.view_id, type_option_data, old_field) + .await?; + } + } + Ok(()) } pub async fn v_get_group_configuration_settings(&self) -> Vec { self.delegate.get_group_setting(&self.view_id) } - - pub async fn update_group( - &self, - changeset: GroupChangeset, - ) -> FlowyResult> { - match changeset.name { - Some(group_name) => { - let result = self - .mut_group_controller(|controller, _| { - Ok(controller.update_group_name(&changeset.group_id, &group_name)) - }) - .await; - - match result { - Some(r) => Ok(r), - None => Ok(None), - } - }, - None => Ok(None), - } - } - pub async fn v_get_all_sorts(&self) -> Vec { self.delegate.get_all_sorts(&self.view_id) } @@ -659,7 +555,7 @@ impl DatabaseViewEditor { if let Some(value) = self.delegate.get_layout_setting(&self.view_id, layout_ty) { let calendar_setting = CalendarLayoutSetting::from(value); // Check the field exist or not - if let Some(field) = self.delegate.get_field(&calendar_setting.field_id).await { + if let Some(field) = self.delegate.get_field(&calendar_setting.field_id) { let field_type = FieldType::from(field.field_type); // Check the type of field is Datetime or not @@ -682,11 +578,7 @@ impl DatabaseViewEditor { pub async fn v_set_layout_settings(&self, params: LayoutSettingParams) -> FlowyResult<()> { // Maybe it needs no send notification to refresh the UI if let Some(new_calendar_setting) = params.calendar { - if let Some(field) = self - .delegate - .get_field(&new_calendar_setting.field_id) - .await - { + if let Some(field) = self.delegate.get_field(&new_calendar_setting.field_id) { let field_type = FieldType::from(field.field_type); if field_type != FieldType::DateTime { return Err(FlowyError::unexpect_calendar_field_type()); @@ -729,13 +621,19 @@ impl DatabaseViewEditor { Ok(()) } + /// Notifies the view's field type-option data is changed + /// For the moment, only the groups will be generated after the type-option data changed. A + /// [Field] has a property named type_options contains a list of type-option data. #[tracing::instrument(level = "trace", skip_all, err)] - pub async fn v_did_update_field_type_option( - &self, - field_id: &str, - old_field: &Field, - ) -> FlowyResult<()> { - if let Some(field) = self.delegate.get_field(field_id).await { + pub async fn v_did_update_field_type_option(&self, old_field: &Field) -> FlowyResult<()> { + let field_id = &old_field.id; + // If the id of the grouping field is equal to the updated field's id, then we need to + // update the group setting + if self.is_grouping_field(field_id).await { + self.v_grouping_by_field(field_id).await?; + } + + if let Some(field) = self.delegate.get_field(field_id) { self .sort_controller .read() @@ -776,9 +674,13 @@ impl DatabaseViewEditor { /// Called when a grouping field is updated. #[tracing::instrument(level = "debug", skip_all, err)] pub async fn v_grouping_by_field(&self, field_id: &str) -> FlowyResult<()> { - if let Some(field) = self.delegate.get_field(field_id).await { - let new_group_controller = - new_group_controller_with_field(self.view_id.clone(), self.delegate.clone(), field).await?; + if let Some(field) = self.delegate.get_field(field_id) { + let new_group_controller = new_group_controller_with_field( + self.view_id.clone(), + self.delegate.clone(), + Arc::new(field), + ) + .await?; let new_groups = new_group_controller .groups() @@ -812,7 +714,7 @@ impl DatabaseViewEditor { let text_cell = get_cell_for_row(self.delegate.clone(), &primary_field.id, &row_id).await?; // Date - let date_field = self.delegate.get_field(&calendar_setting.field_id).await?; + let date_field = self.delegate.get_field(&calendar_setting.field_id)?; let date_cell = get_cell_for_row(self.delegate.clone(), &date_field.id, &row_id).await?; let title = text_cell @@ -973,7 +875,7 @@ impl DatabaseViewEditor { async fn mut_group_controller(&self, f: F) -> Option where - F: FnOnce(&mut Box, Arc) -> FlowyResult, + F: FnOnce(&mut Box, Field) -> FlowyResult, { let group_field_id = self .group_controller @@ -981,8 +883,7 @@ impl DatabaseViewEditor { .await .as_ref() .map(|group| group.field_id().to_owned())?; - let field = self.delegate.get_field(&group_field_id).await?; - + let field = self.delegate.get_field(&group_field_id)?; let mut write_guard = self.group_controller.write().await; if let Some(group_controller) = &mut *write_guard { f(group_controller, field).ok() diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs index eec723e6a0bf..75f31212d961 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs @@ -7,13 +7,13 @@ use lib_infra::future::{to_fut, Fut}; use crate::services::cell::CellCache; use crate::services::database_view::{ - gen_handler_id, DatabaseViewChangedNotifier, DatabaseViewData, + gen_handler_id, DatabaseViewChangedNotifier, DatabaseViewOperation, }; use crate::services::filter::{Filter, FilterController, FilterDelegate, FilterTaskHandler}; pub async fn make_filter_controller( view_id: &str, - delegate: Arc, + delegate: Arc, notifier: DatabaseViewChangedNotifier, cell_cache: CellCache, ) -> Arc { @@ -43,7 +43,7 @@ pub async fn make_filter_controller( filter_controller } -struct DatabaseViewFilterDelegateImpl(Arc); +struct DatabaseViewFilterDelegateImpl(Arc); impl FilterDelegate for DatabaseViewFilterDelegateImpl { fn get_filter(&self, view_id: &str, filter_id: &str) -> Fut>> { @@ -51,7 +51,7 @@ impl FilterDelegate for DatabaseViewFilterDelegateImpl { to_fut(async move { filter }) } - fn get_field(&self, field_id: &str) -> Fut>> { + fn get_field(&self, field_id: &str) -> Option { self.0.get_field(field_id) } diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_group.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_group.rs index b79ea3eb3e18..2fb903b0612a 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_group.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_group.rs @@ -1,40 +1,43 @@ use std::sync::Arc; +use async_trait::async_trait; use collab_database::fields::Field; -use collab_database::rows::RowId; +use collab_database::rows::{Cell, RowId}; use flowy_error::FlowyResult; use lib_infra::future::{to_fut, Fut}; use crate::entities::FieldType; -use crate::services::database_view::DatabaseViewData; +use crate::services::database_view::DatabaseViewOperation; use crate::services::field::RowSingleCellData; use crate::services::group::{ find_new_grouping_field, make_group_controller, GroupController, GroupSetting, - GroupSettingReader, GroupSettingWriter, + GroupSettingReader, GroupSettingWriter, GroupTypeOptionCellOperation, }; pub async fn new_group_controller_with_field( view_id: String, - delegate: Arc, + delegate: Arc, grouping_field: Arc, ) -> FlowyResult> { let setting_reader = GroupSettingReaderImpl(delegate.clone()); let rows = delegate.get_rows(&view_id).await; let setting_writer = GroupSettingWriterImpl(delegate.clone()); + let type_option_writer = GroupTypeOptionCellWriterImpl(delegate.clone()); make_group_controller( view_id, grouping_field, rows, setting_reader, setting_writer, + type_option_writer, ) .await } pub async fn new_group_controller( view_id: String, - delegate: Arc, + delegate: Arc, ) -> FlowyResult>> { let fields = delegate.get_fields(&view_id, None).await; let setting_reader = GroupSettingReaderImpl(delegate.clone()); @@ -59,6 +62,7 @@ pub async fn new_group_controller( if let Some(grouping_field) = grouping_field { let rows = delegate.get_rows(&view_id).await; let setting_writer = GroupSettingWriterImpl(delegate.clone()); + let type_option_writer = GroupTypeOptionCellWriterImpl(delegate.clone()); Ok(Some( make_group_controller( view_id, @@ -66,6 +70,7 @@ pub async fn new_group_controller( rows, setting_reader, setting_writer, + type_option_writer, ) .await?, )) @@ -74,7 +79,7 @@ pub async fn new_group_controller( } } -pub(crate) struct GroupSettingReaderImpl(pub Arc); +pub(crate) struct GroupSettingReaderImpl(pub Arc); impl GroupSettingReader for GroupSettingReaderImpl { fn get_group_setting(&self, view_id: &str) -> Fut>> { @@ -97,11 +102,11 @@ impl GroupSettingReader for GroupSettingReaderImpl { } pub(crate) async fn get_cell_for_row( - delegate: Arc, + delegate: Arc, field_id: &str, row_id: &RowId, ) -> Option { - let field = delegate.get_field(field_id).await?; + let field = delegate.get_field(field_id)?; let row_cell = delegate.get_cell_in_row(field_id, row_id).await; let field_type = FieldType::from(field.field_type); let handler = delegate.get_type_option_cell_handler(&field, &field_type)?; @@ -120,11 +125,11 @@ pub(crate) async fn get_cell_for_row( // Returns the list of cells corresponding to the given field. pub(crate) async fn get_cells_for_field( - delegate: Arc, + delegate: Arc, view_id: &str, field_id: &str, ) -> Vec { - if let Some(field) = delegate.get_field(field_id).await { + if let Some(field) = delegate.get_field(field_id) { let field_type = FieldType::from(field.field_type); if let Some(handler) = delegate.get_type_option_cell_handler(&field, &field_type) { let cells = delegate.get_cells_for_field(view_id, field_id).await; @@ -149,11 +154,30 @@ pub(crate) async fn get_cells_for_field( vec![] } -struct GroupSettingWriterImpl(Arc); - +struct GroupSettingWriterImpl(Arc); impl GroupSettingWriter for GroupSettingWriterImpl { fn save_configuration(&self, view_id: &str, group_setting: GroupSetting) -> Fut> { self.0.insert_group_setting(view_id, group_setting); to_fut(async move { Ok(()) }) } } + +struct GroupTypeOptionCellWriterImpl(Arc); + +#[async_trait] +impl GroupTypeOptionCellOperation for GroupTypeOptionCellWriterImpl { + async fn get_cell(&self, _row_id: &RowId, _field_id: &str) -> FlowyResult> { + todo!() + } + + #[tracing::instrument(level = "trace", skip_all, err)] + async fn update_cell( + &self, + _view_id: &str, + _row_id: &RowId, + _field_id: &str, + _cell: Cell, + ) -> FlowyResult<()> { + todo!() + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_operation.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_operation.rs new file mode 100644 index 000000000000..77472a3452b6 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_operation.rs @@ -0,0 +1,126 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use collab_database::database::MutexDatabase; +use collab_database::fields::{Field, TypeOptionData}; +use collab_database::rows::{RowCell, RowDetail, RowId}; +use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting}; +use tokio::sync::RwLock; + +use flowy_error::FlowyError; +use flowy_task::TaskDispatcher; +use lib_infra::future::{Fut, FutureResult}; + +use crate::entities::{FieldType, FieldVisibility}; +use crate::services::field::TypeOptionCellDataHandler; +use crate::services::field_settings::FieldSettings; +use crate::services::filter::Filter; +use crate::services::group::GroupSetting; +use crate::services::sort::Sort; + +/// Defines the operation that can be performed on a database view +pub trait DatabaseViewOperation: Send + Sync + 'static { + /// Get the database that the view belongs to + fn get_database(&self) -> Arc; + + /// Get the view of the database with the view_id + fn get_view(&self, view_id: &str) -> Fut>; + /// If the field_ids is None, then it will return all the field revisions + fn get_fields(&self, view_id: &str, field_ids: Option>) -> Fut>>; + + /// Returns the field with the field_id + fn get_field(&self, field_id: &str) -> Option; + + fn create_field( + &self, + view_id: &str, + name: &str, + field_type: FieldType, + type_option_data: TypeOptionData, + ) -> Fut; + + fn update_field( + &self, + view_id: &str, + type_option_data: TypeOptionData, + old_field: Field, + ) -> FutureResult<(), FlowyError>; + + fn get_primary_field(&self) -> Fut>>; + + /// Returns the index of the row with row_id + fn index_of_row(&self, view_id: &str, row_id: &RowId) -> Fut>; + + /// Returns the `index` and `RowRevision` with row_id + fn get_row(&self, view_id: &str, row_id: &RowId) -> Fut)>>; + + /// Returns all the rows in the view + fn get_rows(&self, view_id: &str) -> Fut>>; + + fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Fut>>; + + fn get_cell_in_row(&self, field_id: &str, row_id: &RowId) -> Fut>; + + /// Return the database layout type for the view with given view_id + /// The default layout type is [DatabaseLayout::Grid] + fn get_layout_for_view(&self, view_id: &str) -> DatabaseLayout; + + fn get_group_setting(&self, view_id: &str) -> Vec; + + fn insert_group_setting(&self, view_id: &str, setting: GroupSetting); + + fn get_sort(&self, view_id: &str, sort_id: &str) -> Option; + + fn insert_sort(&self, view_id: &str, sort: Sort); + + fn remove_sort(&self, view_id: &str, sort_id: &str); + + fn get_all_sorts(&self, view_id: &str) -> Vec; + + fn remove_all_sorts(&self, view_id: &str); + + fn get_all_filters(&self, view_id: &str) -> Vec>; + + fn delete_filter(&self, view_id: &str, filter_id: &str); + + fn insert_filter(&self, view_id: &str, filter: Filter); + + fn get_filter(&self, view_id: &str, filter_id: &str) -> Option; + + fn get_filter_by_field_id(&self, view_id: &str, field_id: &str) -> Option; + + fn get_layout_setting(&self, view_id: &str, layout_ty: &DatabaseLayout) -> Option; + + fn insert_layout_setting( + &self, + view_id: &str, + layout_ty: &DatabaseLayout, + layout_setting: LayoutSetting, + ); + + fn update_layout_type(&self, view_id: &str, layout_type: &DatabaseLayout); + + /// Returns a `TaskDispatcher` used to poll a `Task` + fn get_task_scheduler(&self) -> Arc>; + + fn get_type_option_cell_handler( + &self, + field: &Field, + field_type: &FieldType, + ) -> Option>; + + fn get_field_settings( + &self, + view_id: &str, + field_ids: &[String], + ) -> HashMap; + + fn get_all_field_settings(&self, view_id: &str) -> HashMap; + + fn update_field_settings( + &self, + view_id: &str, + field_id: &str, + visibility: Option, + ); +} diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_sort.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_sort.rs index 6fec7d9ff9d6..6587d9ea0e97 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_sort.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_sort.rs @@ -8,14 +8,14 @@ use lib_infra::future::{to_fut, Fut}; use crate::services::cell::CellCache; use crate::services::database_view::{ - gen_handler_id, DatabaseViewChangedNotifier, DatabaseViewData, + gen_handler_id, DatabaseViewChangedNotifier, DatabaseViewOperation, }; use crate::services::filter::FilterController; use crate::services::sort::{Sort, SortController, SortDelegate, SortTaskHandler}; pub(crate) async fn make_sort_controller( view_id: &str, - delegate: Arc, + delegate: Arc, notifier: DatabaseViewChangedNotifier, filter_controller: Arc, cell_cache: CellCache, @@ -49,7 +49,7 @@ pub(crate) async fn make_sort_controller( } struct DatabaseViewSortDelegateImpl { - delegate: Arc, + delegate: Arc, filter_controller: Arc, } @@ -70,7 +70,7 @@ impl SortDelegate for DatabaseViewSortDelegateImpl { }) } - fn get_field(&self, field_id: &str) -> Fut>> { + fn get_field(&self, field_id: &str) -> Option { self.delegate.get_field(field_id) } diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/views.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/views.rs index 509ff5849b47..8859422b9c11 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/views.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/views.rs @@ -2,47 +2,46 @@ use std::collections::HashMap; use std::sync::Arc; use collab_database::database::MutexDatabase; -use collab_database::fields::Field; use collab_database::rows::{RowDetail, RowId}; use nanoid::nanoid; use tokio::sync::{broadcast, RwLock}; -use flowy_error::FlowyResult; +use flowy_error::{FlowyError, FlowyResult}; use lib_infra::future::Fut; use crate::services::cell::CellCache; use crate::services::database::DatabaseRowEvent; -use crate::services::database_view::{DatabaseViewData, DatabaseViewEditor}; +use crate::services::database_view::{DatabaseViewEditor, DatabaseViewOperation}; use crate::services::group::RowChangeset; pub type RowEventSender = broadcast::Sender; pub type RowEventReceiver = broadcast::Receiver; - +pub type EditorByViewId = HashMap>; pub struct DatabaseViews { #[allow(dead_code)] database: Arc, cell_cache: CellCache, - database_view_data: Arc, - editor_map: Arc>>>, + view_operation: Arc, + editor_by_view_id: Arc>, } impl DatabaseViews { pub async fn new( database: Arc, cell_cache: CellCache, - database_view_data: Arc, + view_operation: Arc, + editor_by_view_id: Arc>, ) -> FlowyResult { - let editor_map = Arc::new(RwLock::new(HashMap::default())); Ok(Self { database, - database_view_data, + view_operation, cell_cache, - editor_map, + editor_by_view_id, }) } pub async fn close_view(&self, view_id: &str) -> bool { - let mut editor_map = self.editor_map.write().await; + let mut editor_map = self.editor_by_view_id.write().await; if let Some(view) = editor_map.remove(view_id) { view.close().await; } @@ -50,7 +49,13 @@ impl DatabaseViews { } pub async fn editors(&self) -> Vec> { - self.editor_map.read().await.values().cloned().collect() + self + .editor_by_view_id + .read() + .await + .values() + .cloned() + .collect() } /// It may generate a RowChangeset when the Row was moved from one group to another. @@ -77,43 +82,22 @@ impl DatabaseViews { Ok(()) } - /// Notifies the view's field type-option data is changed - /// For the moment, only the groups will be generated after the type-option data changed. A - /// [Field] has a property named type_options contains a list of type-option data. - /// # Arguments - /// - /// * `field_id`: the id of the field in current view - /// - #[tracing::instrument(level = "debug", skip(self, old_field), err)] - pub async fn did_update_field_type_option( - &self, - view_id: &str, - field_id: &str, - old_field: &Field, - ) -> FlowyResult<()> { - let view_editor = self.get_view_editor(view_id).await?; - // If the id of the grouping field is equal to the updated field's id, then we need to - // update the group setting - if view_editor.is_grouping_field(field_id).await { - view_editor.v_grouping_by_field(field_id).await?; - } - view_editor - .v_did_update_field_type_option(field_id, old_field) - .await?; - Ok(()) - } - pub async fn get_view_editor(&self, view_id: &str) -> FlowyResult> { debug_assert!(!view_id.is_empty()); - if let Some(editor) = self.editor_map.read().await.get(view_id) { + if let Some(editor) = self.editor_by_view_id.read().await.get(view_id) { return Ok(editor.clone()); } - let mut editor_map = self.editor_map.write().await; + let mut editor_map = self.editor_by_view_id.try_write().map_err(|err| { + FlowyError::internal().with_context(format!( + "fail to acquire the lock of editor_by_view_id: {}", + err + )) + })?; let editor = Arc::new( DatabaseViewEditor::new( view_id.to_owned(), - self.database_view_data.clone(), + self.view_operation.clone(), self.cell_cache.clone(), ) .await?, diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_ids.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_ids.rs index 1914414b9168..55fe2635bcbf 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_ids.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_ids.rs @@ -24,7 +24,6 @@ impl SelectOptionIds { pub fn into_inner(self) -> Vec { self.0 } - pub fn to_cell_data(&self, field_type: FieldType) -> Cell { new_cell_builder(field_type) .insert_str_value(CELL_DATA, self.to_string()) diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs index 4e06f7b1aff3..7a5429076f59 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs @@ -33,7 +33,15 @@ pub trait TypeOption { /// /// Uses `StrCellData` for any `TypeOption` if their cell data is pure `String`. /// - type CellData: TypeOptionCellData + ToString + Default + Send + Sync + Clone + Debug + 'static; + type CellData: for<'a> From<&'a Cell> + + TypeOptionCellData + + ToString + + Default + + Send + + Sync + + Clone + + Debug + + 'static; /// Represents as the corresponding field type cell changeset. /// The changeset must implements the `FromCellChangesetString` and the `ToCellChangesetString` trait. diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs index 52b7db87519a..c07f8ff79f63 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs @@ -113,16 +113,21 @@ where + Sync + 'static, { + pub fn into_boxed(self) -> Box { + Box::new(self) as Box + } + pub fn new_with_boxed( inner: T, cell_filter_cache: Option, cell_data_cache: Option, ) -> Box { - Box::new(Self { + Self { inner, cell_data_cache, cell_filter_cache, - }) as Box + } + .into_boxed() } } diff --git a/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs b/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs index 6d25e24c0c76..ab6a56c45ace 100644 --- a/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs @@ -21,7 +21,7 @@ use crate::services::filter::{Filter, FilterChangeset, FilterResult, FilterResul pub trait FilterDelegate: Send + Sync + 'static { fn get_filter(&self, view_id: &str, filter_id: &str) -> Fut>>; - fn get_field(&self, field_id: &str) -> Fut>>; + fn get_field(&self, field_id: &str) -> Option; fn get_fields(&self, view_id: &str, field_ids: Option>) -> Fut>>; fn get_rows(&self, view_id: &str) -> Fut>>; fn get_row(&self, view_id: &str, rows_id: &RowId) -> Fut)>>; diff --git a/frontend/rust-lib/flowy-database2/src/services/group/action.rs b/frontend/rust-lib/flowy-database2/src/services/group/action.rs index 2163e840a1da..376391dbd1a7 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/action.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/action.rs @@ -1,20 +1,22 @@ -use collab_database::fields::Field; +use async_trait::async_trait; +use collab_database::fields::{Field, TypeOptionData}; use collab_database::rows::{Cell, Row, RowDetail}; use flowy_error::FlowyResult; use crate::entities::{GroupChangesPB, GroupPB, GroupRowsNotificationPB, InsertedGroupPB}; -use crate::services::cell::DecodedCellData; -use crate::services::group::controller::MoveGroupRowContext; +use crate::services::field::TypeOption; use crate::services::group::entities::GroupSetting; -use crate::services::group::{GroupChangesets, GroupData, GroupSettingChangeset}; +use crate::services::group::{ + GroupChangesets, GroupData, GroupSettingChangeset, MoveGroupRowContext, +}; /// Using polymorphism to provides the customs action for different group controller. /// /// For example, the `CheckboxGroupController` implements this trait to provide custom behavior. /// pub trait GroupCustomize: Send + Sync { - type CellData: DecodedCellData; + type GroupTypeOption: TypeOption; /// Returns the a value of the cell if the cell data is not exist. /// The default value is `None` /// @@ -26,13 +28,17 @@ pub trait GroupCustomize: Send + Sync { } /// Returns a bool value to determine whether the group should contain this cell or not. - fn can_group(&self, content: &str, cell_data: &Self::CellData) -> bool; + fn can_group( + &self, + content: &str, + cell_data: &::CellData, + ) -> bool; fn create_or_delete_group_when_cell_changed( &mut self, _row_detail: &RowDetail, - _old_cell_data: Option<&Self::CellData>, - _cell_data: &Self::CellData, + _old_cell_data: Option<&::CellProtobufType>, + _cell_data: &::CellProtobufType, ) -> FlowyResult<(Option, Option)> { Ok((None, None)) } @@ -43,16 +49,20 @@ pub trait GroupCustomize: Send + Sync { fn add_or_remove_row_when_cell_changed( &mut self, row_detail: &RowDetail, - cell_data: &Self::CellData, + cell_data: &::CellProtobufType, ) -> Vec; /// Deletes the row from the group - fn delete_row(&mut self, row: &Row, cell_data: &Self::CellData) -> Vec; + fn delete_row( + &mut self, + row: &Row, + cell_data: &::CellData, + ) -> Vec; /// Move row from one group to another fn move_row( &mut self, - cell_data: &Self::CellData, + cell_data: &::CellProtobufType, context: MoveGroupRowContext, ) -> Vec; @@ -60,13 +70,14 @@ pub trait GroupCustomize: Send + Sync { fn delete_group_when_move_row( &mut self, _row: &Row, - _cell_data: &Self::CellData, + _cell_data: &::CellProtobufType, ) -> Option { None } } /// Defines the shared actions any group controller can perform. +#[async_trait] pub trait GroupControllerOperation: Send + Sync { /// The field that is used for grouping the rows fn field_id(&self) -> &str; @@ -104,7 +115,10 @@ pub trait GroupControllerOperation: Send + Sync { /// Update the group if the corresponding field is changed fn did_update_group_field(&mut self, field: &Field) -> FlowyResult>; - fn apply_group_setting_changeset(&mut self, changeset: GroupChangesets) -> FlowyResult<()>; + async fn apply_group_changeset( + &mut self, + changeset: &GroupChangesets, + ) -> FlowyResult; fn apply_group_configuration_setting_changeset( &mut self, diff --git a/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs b/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs index ed44bf0ed322..aca56b5495cd 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs @@ -3,10 +3,13 @@ use std::fmt::Formatter; use std::marker::PhantomData; use std::sync::Arc; +use async_trait::async_trait; use collab_database::fields::Field; +use collab_database::rows::{Cell, RowId}; use indexmap::IndexMap; use serde::de::DeserializeOwned; use serde::Serialize; +use tracing::event; use flowy_error::{FlowyError, FlowyResult}; use lib_infra::future::Fut; @@ -27,6 +30,18 @@ pub trait GroupSettingWriter: Send + Sync + 'static { fn save_configuration(&self, view_id: &str, group_setting: GroupSetting) -> Fut>; } +#[async_trait] +pub trait GroupTypeOptionCellOperation: Send + Sync + 'static { + async fn get_cell(&self, row_id: &RowId, field_id: &str) -> FlowyResult>; + async fn update_cell( + &self, + view_id: &str, + row_id: &RowId, + field_id: &str, + cell: Cell, + ) -> FlowyResult<()>; +} + impl std::fmt::Display for GroupContext { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { self.group_by_id.iter().for_each(|(_, group)| { @@ -64,7 +79,6 @@ pub struct GroupContext { /// A reader that implement the [GroupSettingReader] trait /// - #[allow(dead_code)] reader: Arc, /// A writer that implement the [GroupSettingWriter] trait is used to save the @@ -84,6 +98,7 @@ where reader: Arc, writer: Arc, ) -> FlowyResult { + event!(tracing::Level::TRACE, "GroupContext::new"); let setting = match reader.get_group_setting(&view_id).await { None => { let default_configuration = default_group_setting(&field); @@ -356,7 +371,7 @@ where } } - pub(crate) fn update_group(&mut self, group_changeset: GroupChangeset) -> FlowyResult<()> { + pub(crate) fn update_group(&mut self, group_changeset: &GroupChangeset) -> FlowyResult<()> { let update_group = self.mut_group(&group_changeset.group_id, |group| { if let Some(visible) = group_changeset.visible { group.visible = visible; diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller.rs index 94af70149810..77845f73380c 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller.rs @@ -1,9 +1,10 @@ -use std::collections::HashMap; use std::marker::PhantomData; use std::sync::Arc; +use async_trait::async_trait; use collab_database::fields::{Field, TypeOptionData}; -use collab_database::rows::{Cell, Cells, Row, RowDetail, RowId}; +use collab_database::rows::{Cells, Row, RowDetail}; +use futures::executor::block_on; use serde::de::DeserializeOwned; use serde::Serialize; @@ -12,13 +13,16 @@ use flowy_error::FlowyResult; use crate::entities::{ FieldType, GroupChangesPB, GroupRowsNotificationPB, InsertedRowPB, RowMetaPB, }; -use crate::services::cell::{get_cell_protobuf, CellProtobufBlobParser, DecodedCellData}; +use crate::services::cell::{get_cell_protobuf, CellProtobufBlobParser}; +use crate::services::field::{default_type_option_data_from_type, TypeOption, TypeOptionCellData}; use crate::services::group::action::{ DidMoveGroupRowResult, DidUpdateGroupRowResult, GroupControllerOperation, GroupCustomize, }; use crate::services::group::configuration::GroupContext; use crate::services::group::entities::{GroupData, GroupSetting}; -use crate::services::group::{Group, GroupChangesets, GroupSettingChangeset}; +use crate::services::group::{ + GroupChangeset, GroupChangesets, GroupSettingChangeset, GroupsBuilder, MoveGroupRowContext, +}; // use collab_database::views::Group; @@ -32,108 +36,67 @@ use crate::services::group::{Group, GroupChangesets, GroupSettingChangeset}; /// pub trait GroupController: GroupControllerOperation + Send + Sync { /// Called when the type option of the [Field] was updated. - fn did_update_field_type_option(&mut self, field: &Arc); + fn did_update_field_type_option(&mut self, field: &Field); /// Called before the row was created. fn will_create_row(&mut self, cells: &mut Cells, field: &Field, group_id: &str); /// Called after the row was created. fn did_create_row(&mut self, row_detail: &RowDetail, group_id: &str); - - /// Update group name handler - fn update_group_name(&mut self, _group_id: &str, _group_name: &str) -> Option { - None - } -} - -/// The [GroupsBuilder] trait is used to generate the groups for different [FieldType] -pub trait GroupsBuilder { - type Context; - type TypeOptionType; - - fn build( - field: &Field, - context: &Self::Context, - type_option: &Option, - ) -> GeneratedGroups; -} - -pub struct GeneratedGroups { - pub no_status_group: Option, - pub group_configs: Vec, -} - -pub struct GeneratedGroupConfig { - pub group: Group, - pub filter_content: String, -} - -pub struct MoveGroupRowContext<'a> { - pub row_detail: &'a RowDetail, - pub row_changeset: &'a mut RowChangeset, - pub field: &'a Field, - pub to_group_id: &'a str, - pub to_row_id: Option, -} - -#[derive(Debug, Clone)] -pub struct RowChangeset { - pub row_id: RowId, - pub height: Option, - pub visibility: Option, - // Contains the key/value changes represents as the update of the cells. For example, - // if there is one cell was changed, then the `cell_by_field_id` will only have one key/value. - pub cell_by_field_id: HashMap, } -impl RowChangeset { - pub fn new(row_id: RowId) -> Self { - Self { - row_id, - height: None, - visibility: None, - cell_by_field_id: Default::default(), - } - } - - pub fn is_empty(&self) -> bool { - self.height.is_none() && self.visibility.is_none() && self.cell_by_field_id.is_empty() - } +#[async_trait] +pub trait GroupOperationInterceptor { + type GroupTypeOption: TypeOption; + async fn type_option_from_group_changeset( + &self, + changeset: &GroupChangeset, + type_option: &Self::GroupTypeOption, + view_id: &str, + ) -> Option; } /// C: represents the group configuration that impl [GroupConfigurationSerde] /// T: the type-option data deserializer that impl [TypeOptionDataDeserializer] /// G: the group generator, [GroupsBuilder] /// P: the parser that impl [CellProtobufBlobParser] for the CellBytes -pub struct BaseGroupController { +pub struct BaseGroupController { pub grouping_field_id: String, - pub type_option: Option, + pub type_option: T, pub context: GroupContext, - group_action_phantom: PhantomData, + group_builder_phantom: PhantomData, cell_parser_phantom: PhantomData

, + pub operation_interceptor: I, } -impl BaseGroupController +impl BaseGroupController where C: Serialize + DeserializeOwned, - T: From, - G: GroupsBuilder, TypeOptionType = T>, + T: TypeOption + From + Send + Sync, + G: GroupsBuilder, GroupTypeOption = T>, + I: GroupOperationInterceptor + Send + Sync, { pub async fn new( grouping_field: &Arc, mut configuration: GroupContext, + operation_interceptor: I, ) -> FlowyResult { let field_type = FieldType::from(grouping_field.field_type); - let type_option = grouping_field.get_type_option::(field_type); - let generated_groups = G::build(grouping_field, &configuration, &type_option); + let type_option = grouping_field + .get_type_option::(&field_type) + .unwrap_or_else(|| T::from(default_type_option_data_from_type(&field_type))); + + // TODO(nathan): remove block_on + let generated_groups = block_on(G::build(grouping_field, &configuration, &type_option)); let _ = configuration.init_groups(generated_groups)?; Ok(Self { grouping_field_id: grouping_field.id.clone(), type_option, context: configuration, - group_action_phantom: PhantomData, + group_builder_phantom: PhantomData, cell_parser_phantom: PhantomData, + operation_interceptor, }) } @@ -209,14 +172,15 @@ where } } -impl GroupControllerOperation for BaseGroupController +#[async_trait] +impl GroupControllerOperation for BaseGroupController where - P: CellProtobufBlobParser, - C: Serialize + DeserializeOwned, - T: From, - G: GroupsBuilder, TypeOptionType = T>, - - Self: GroupCustomize, + P: CellProtobufBlobParser::CellProtobufType>, + C: Serialize + DeserializeOwned + Sync + Send, + T: TypeOption + From + Send + Sync, + G: GroupsBuilder, GroupTypeOption = T>, + I: GroupOperationInterceptor + Send + Sync, + Self: GroupCustomize, { fn field_id(&self) -> &str { &self.grouping_field_id @@ -232,7 +196,7 @@ where } #[tracing::instrument(level = "trace", skip_all, fields(row_count=%rows.len(), group_result))] - fn fill_groups(&mut self, rows: &[&RowDetail], field: &Field) -> FlowyResult<()> { + fn fill_groups(&mut self, rows: &[&RowDetail], _field: &Field) -> FlowyResult<()> { for row_detail in rows { let cell = match row_detail.row.cells.get(&self.grouping_field_id) { None => self.placeholder_cell(), @@ -241,8 +205,7 @@ where if let Some(cell) = cell { let mut grouped_rows: Vec = vec![]; - let cell_bytes = get_cell_protobuf(&cell, field, None); - let cell_data = cell_bytes.parser::

()?; + let cell_data = ::CellData::from(&cell); for group in self.context.groups() { if self.can_group(&group.filter_content, &cell_data) { grouped_rows.push(GroupedRow { @@ -320,7 +283,7 @@ where fn did_delete_delete_row( &mut self, row: &Row, - field: &Field, + _field: &Field, ) -> FlowyResult { // if the cell_rev is none, then the row must in the default group. let mut result = DidMoveGroupRowResult { @@ -328,9 +291,8 @@ where row_changesets: vec![], }; if let Some(cell) = row.cells.get(&self.grouping_field_id) { - let cell_bytes = get_cell_protobuf(cell, field, None); - let cell_data = cell_bytes.parser::

()?; - if !cell_data.is_empty() { + let cell_data = ::CellData::from(cell); + if !cell_data.is_cell_empty() { tracing::error!("did_delete_delete_row {:?}", cell); result.row_changesets = self.delete_row(row, &cell_data); return Ok(result); @@ -380,13 +342,24 @@ where Ok(None) } - fn apply_group_setting_changeset(&mut self, changeset: GroupChangesets) -> FlowyResult<()> { - for group_changeset in changeset.update_groups { - if let Err(e) = self.context.update_group(group_changeset) { - tracing::error!("Failed to update group: {:?}", e); + async fn apply_group_changeset( + &mut self, + changeset: &GroupChangesets, + ) -> FlowyResult { + for group_changeset in changeset.changesets.iter() { + self.context.update_group(group_changeset)?; + } + let mut type_option_data = TypeOptionData::new(); + for group_changeset in changeset.changesets.iter() { + if let Some(new_type_option_data) = self + .operation_interceptor + .type_option_from_group_changeset(group_changeset, &self.type_option, &self.context.view_id) + .await + { + type_option_data.extend(new_type_option_data); } } - Ok(()) + Ok(type_option_data) } fn apply_group_configuration_setting_changeset( diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs index 221bdbbea0f4..490b80dca50e 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs @@ -1,20 +1,20 @@ -use std::sync::Arc; - -use collab_database::fields::Field; +use async_trait::async_trait; +use collab_database::fields::{Field, TypeOptionData}; use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail}; use serde::{Deserialize, Serialize}; use crate::entities::{FieldType, GroupRowsNotificationPB, InsertedRowPB, RowMetaPB}; use crate::services::cell::insert_checkbox_cell; use crate::services::field::{ - CheckboxCellData, CheckboxCellDataParser, CheckboxTypeOption, CHECK, UNCHECK, + CheckboxCellDataParser, CheckboxTypeOption, TypeOption, CHECK, UNCHECK, }; use crate::services::group::action::GroupCustomize; use crate::services::group::configuration::GroupContext; -use crate::services::group::controller::{ - BaseGroupController, GroupController, GroupsBuilder, MoveGroupRowContext, +use crate::services::group::controller::{BaseGroupController, GroupController}; +use crate::services::group::{ + move_group_row, GeneratedGroupConfig, GeneratedGroups, Group, GroupChangeset, + GroupOperationInterceptor, GroupsBuilder, MoveGroupRowContext, }; -use crate::services::group::{move_group_row, GeneratedGroupConfig, GeneratedGroups, Group}; #[derive(Default, Serialize, Deserialize)] pub struct CheckboxGroupConfiguration { @@ -24,14 +24,15 @@ pub struct CheckboxGroupConfiguration { pub type CheckboxGroupController = BaseGroupController< CheckboxGroupConfiguration, CheckboxTypeOption, - CheckboxGroupGenerator, + CheckboxGroupBuilder, CheckboxCellDataParser, + CheckboxGroupOperationInterceptorImpl, >; pub type CheckboxGroupContext = GroupContext; impl GroupCustomize for CheckboxGroupController { - type CellData = CheckboxCellData; + type GroupTypeOption = CheckboxTypeOption; fn placeholder_cell(&self) -> Option { Some( new_cell_builder(FieldType::Checkbox) @@ -40,7 +41,11 @@ impl GroupCustomize for CheckboxGroupController { ) } - fn can_group(&self, content: &str, cell_data: &Self::CellData) -> bool { + fn can_group( + &self, + content: &str, + cell_data: &::CellData, + ) -> bool { if cell_data.is_check() { content == CHECK } else { @@ -51,7 +56,7 @@ impl GroupCustomize for CheckboxGroupController { fn add_or_remove_row_when_cell_changed( &mut self, row_detail: &RowDetail, - cell_data: &Self::CellData, + cell_data: &::CellProtobufType, ) -> Vec { let mut changesets = vec![]; self.context.iter_mut_status_groups(|group| { @@ -100,7 +105,11 @@ impl GroupCustomize for CheckboxGroupController { changesets } - fn delete_row(&mut self, row: &Row, _cell_data: &Self::CellData) -> Vec { + fn delete_row( + &mut self, + row: &Row, + _cell_data: &::CellData, + ) -> Vec { let mut changesets = vec![]; self.context.iter_mut_groups(|group| { let mut changeset = GroupRowsNotificationPB::new(group.id.clone()); @@ -118,7 +127,7 @@ impl GroupCustomize for CheckboxGroupController { fn move_row( &mut self, - _cell_data: &Self::CellData, + _cell_data: &::CellProtobufType, mut context: MoveGroupRowContext, ) -> Vec { let mut group_changeset = vec![]; @@ -132,7 +141,7 @@ impl GroupCustomize for CheckboxGroupController { } impl GroupController for CheckboxGroupController { - fn did_update_field_type_option(&mut self, _field: &Arc) { + fn did_update_field_type_option(&mut self, _field: &Field) { // Do nothing } @@ -154,15 +163,16 @@ impl GroupController for CheckboxGroupController { } } -pub struct CheckboxGroupGenerator(); -impl GroupsBuilder for CheckboxGroupGenerator { +pub struct CheckboxGroupBuilder(); +#[async_trait] +impl GroupsBuilder for CheckboxGroupBuilder { type Context = CheckboxGroupContext; - type TypeOptionType = CheckboxTypeOption; + type GroupTypeOption = CheckboxTypeOption; - fn build( + async fn build( _field: &Field, _context: &Self::Context, - _type_option: &Option, + _type_option: &Self::GroupTypeOption, ) -> GeneratedGroups { let check_group = GeneratedGroupConfig { group: Group::new(CHECK.to_string(), "".to_string()), @@ -180,3 +190,18 @@ impl GroupsBuilder for CheckboxGroupGenerator { } } } + +pub struct CheckboxGroupOperationInterceptorImpl {} + +#[async_trait] +impl GroupOperationInterceptor for CheckboxGroupOperationInterceptorImpl { + type GroupTypeOption = CheckboxTypeOption; + async fn type_option_from_group_changeset( + &self, + _changeset: &GroupChangeset, + _type_option: &Self::GroupTypeOption, + _view_id: &str, + ) -> Option { + todo!() + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs index 3c64e4ff26dd..1797a270b930 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs @@ -1,13 +1,13 @@ use std::format; use std::str::FromStr; -use std::sync::Arc; +use async_trait::async_trait; use chrono::{ DateTime, Datelike, Days, Duration, Local, NaiveDate, NaiveDateTime, Offset, TimeZone, }; use chrono_tz::Tz; use collab_database::database::timestamp; -use collab_database::fields::Field; +use collab_database::fields::{Field, TypeOptionData}; use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail}; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; @@ -15,18 +15,16 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; use flowy_error::FlowyResult; use crate::entities::{ - DateCellDataPB, FieldType, GroupPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB, - RowMetaPB, + FieldType, GroupPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB, RowMetaPB, }; use crate::services::cell::insert_date_cell; -use crate::services::field::{DateCellData, DateCellDataParser, DateTypeOption}; +use crate::services::field::{DateCellData, DateCellDataParser, DateTypeOption, TypeOption}; use crate::services::group::action::GroupCustomize; use crate::services::group::configuration::GroupContext; -use crate::services::group::controller::{ - BaseGroupController, GroupController, GroupsBuilder, MoveGroupRowContext, -}; +use crate::services::group::controller::{BaseGroupController, GroupController}; use crate::services::group::{ make_no_status_group, move_group_row, GeneratedGroupConfig, GeneratedGroups, Group, + GroupChangeset, GroupOperationInterceptor, GroupsBuilder, MoveGroupRowContext, }; pub trait GroupConfigurationContentSerde: Sized + Send + Sync { @@ -63,14 +61,15 @@ pub enum DateCondition { pub type DateGroupController = BaseGroupController< DateGroupConfiguration, DateTypeOption, - DateGroupGenerator, + DateGroupBuilder, DateCellDataParser, + DateGroupOperationInterceptorImpl, >; pub type DateGroupContext = GroupContext; impl GroupCustomize for DateGroupController { - type CellData = DateCellDataPB; + type GroupTypeOption = DateTypeOption; fn placeholder_cell(&self) -> Option { Some( @@ -80,47 +79,48 @@ impl GroupCustomize for DateGroupController { ) } - fn can_group(&self, content: &str, cell_data: &Self::CellData) -> bool { + fn can_group( + &self, + content: &str, + cell_data: &::CellData, + ) -> bool { content == group_id( - &cell_data.into(), - self.type_option.as_ref(), + cell_data, + &self.type_option, &self.context.get_setting_content(), ) } fn create_or_delete_group_when_cell_changed( &mut self, - row_detail: &RowDetail, - old_cell_data: Option<&Self::CellData>, - cell_data: &Self::CellData, + _row_detail: &RowDetail, + _old_cell_data: Option<&::CellProtobufType>, + _cell_data: &::CellProtobufType, ) -> FlowyResult<(Option, Option)> { let setting_content = self.context.get_setting_content(); let mut inserted_group = None; if self .context .get_group(&group_id( - &cell_data.into(), - self.type_option.as_ref(), + &_cell_data.into(), + &self.type_option, &setting_content, )) .is_none() { - let group = make_group_from_date_cell( - &cell_data.into(), - self.type_option.as_ref(), - &setting_content, - ); + let group = + make_group_from_date_cell(&_cell_data.into(), &self.type_option, &setting_content); let mut new_group = self.context.add_new_group(group)?; - new_group.group.rows.push(RowMetaPB::from(row_detail)); + new_group.group.rows.push(RowMetaPB::from(_row_detail)); inserted_group = Some(new_group); } // Delete the old group if there are no rows in that group - let deleted_group = match old_cell_data.and_then(|old_cell_data| { + let deleted_group = match _old_cell_data.and_then(|old_cell_data| { self.context.get_group(&group_id( &old_cell_data.into(), - self.type_option.as_ref(), + &self.type_option, &setting_content, )) }) { @@ -148,19 +148,13 @@ impl GroupCustomize for DateGroupController { fn add_or_remove_row_when_cell_changed( &mut self, row_detail: &RowDetail, - cell_data: &Self::CellData, + cell_data: &::CellProtobufType, ) -> Vec { let mut changesets = vec![]; let setting_content = self.context.get_setting_content(); self.context.iter_mut_status_groups(|group| { let mut changeset = GroupRowsNotificationPB::new(group.id.clone()); - if group.id - == group_id( - &cell_data.into(), - self.type_option.as_ref(), - &setting_content, - ) - { + if group.id == group_id(&cell_data.into(), &self.type_option, &setting_content) { if !group.contains_row(&row_detail.row.id) { changeset .inserted_rows @@ -181,7 +175,11 @@ impl GroupCustomize for DateGroupController { changesets } - fn delete_row(&mut self, row: &Row, _cell_data: &Self::CellData) -> Vec { + fn delete_row( + &mut self, + row: &Row, + _cell_data: &::CellData, + ) -> Vec { let mut changesets = vec![]; self.context.iter_mut_groups(|group| { let mut changeset = GroupRowsNotificationPB::new(group.id.clone()); @@ -199,7 +197,7 @@ impl GroupCustomize for DateGroupController { fn move_row( &mut self, - _cell_data: &Self::CellData, + _cell_data: &::CellProtobufType, mut context: MoveGroupRowContext, ) -> Vec { let mut group_changeset = vec![]; @@ -214,13 +212,13 @@ impl GroupCustomize for DateGroupController { fn delete_group_when_move_row( &mut self, _row: &Row, - cell_data: &Self::CellData, + _cell_data: &::CellProtobufType, ) -> Option { let mut deleted_group = None; let setting_content = self.context.get_setting_content(); if let Some((_, group)) = self.context.get_group(&group_id( - &cell_data.into(), - self.type_option.as_ref(), + &_cell_data.into(), + &self.type_option, &setting_content, )) { if group.rows.len() == 1 { @@ -237,7 +235,7 @@ impl GroupCustomize for DateGroupController { } impl GroupController for DateGroupController { - fn did_update_field_type_option(&mut self, _field: &Arc) {} + fn did_update_field_type_option(&mut self, _field: &Field) {} fn will_create_row(&mut self, cells: &mut Cells, field: &Field, group_id: &str) { match self.context.get_group(group_id) { @@ -257,18 +255,19 @@ impl GroupController for DateGroupController { } } -pub struct DateGroupGenerator(); -impl GroupsBuilder for DateGroupGenerator { +pub struct DateGroupBuilder(); +#[async_trait] +impl GroupsBuilder for DateGroupBuilder { type Context = DateGroupContext; - type TypeOptionType = DateTypeOption; + type GroupTypeOption = DateTypeOption; - fn build( + async fn build( field: &Field, context: &Self::Context, - type_option: &Option, + type_option: &Self::GroupTypeOption, ) -> GeneratedGroups { // Read all the cells for the grouping field - let cells = futures::executor::block_on(context.get_all_cells()); + let cells = context.get_all_cells().await; // Generate the groups let mut group_configs: Vec = cells @@ -276,8 +275,7 @@ impl GroupsBuilder for DateGroupGenerator { .flat_map(|value| value.into_date_field_cell_data()) .filter(|cell| cell.timestamp.is_some()) .map(|cell| { - let group = - make_group_from_date_cell(&cell, type_option.as_ref(), &context.get_setting_content()); + let group = make_group_from_date_cell(&cell, type_option, &context.get_setting_content()); GeneratedGroupConfig { filter_content: group.id.clone(), group, @@ -296,7 +294,7 @@ impl GroupsBuilder for DateGroupGenerator { fn make_group_from_date_cell( cell_data: &DateCellData, - type_option: Option<&DateTypeOption>, + type_option: &DateTypeOption, setting_content: &str, ) -> Group { let group_id = group_id(cell_data, type_option, setting_content); @@ -310,11 +308,9 @@ const GROUP_ID_DATE_FORMAT: &str = "%Y/%m/%d"; fn group_id( cell_data: &DateCellData, - type_option: Option<&DateTypeOption>, + type_option: &DateTypeOption, setting_content: &str, ) -> String { - let binding = DateTypeOption::default(); - let type_option = type_option.unwrap_or(&binding); let config = DateGroupConfiguration::from_json(setting_content).unwrap_or_default(); let date_time = date_time_from_timestamp(cell_data.timestamp, &type_option.timezone_id); @@ -373,11 +369,9 @@ fn group_id( fn group_name_from_id( group_id: &str, - type_option: Option<&DateTypeOption>, + type_option: &DateTypeOption, setting_content: &str, ) -> String { - let binding = DateTypeOption::default(); - let type_option = type_option.unwrap_or(&binding); let config = DateGroupConfiguration::from_json(setting_content).unwrap_or_default(); let date = NaiveDate::parse_from_str(group_id, GROUP_ID_DATE_FORMAT).unwrap(); @@ -449,6 +443,21 @@ fn date_time_from_timestamp(timestamp: Option, timezone_id: &str) -> DateTi } } +pub struct DateGroupOperationInterceptorImpl {} + +#[async_trait] +impl GroupOperationInterceptor for DateGroupOperationInterceptorImpl { + type GroupTypeOption = DateTypeOption; + async fn type_option_from_group_changeset( + &self, + _changeset: &GroupChangeset, + _type_option: &Self::GroupTypeOption, + _view_id: &str, + ) -> Option { + todo!() + } +} + #[cfg(test)] mod tests { use std::vec; @@ -582,16 +591,11 @@ mod tests { ]; for (i, test) in tests.iter().enumerate() { - let group_id = group_id( - &test.cell_data, - Some(test.type_option), - &test.setting_content, - ); + let group_id = group_id(&test.cell_data, test.type_option, &test.setting_content); assert_eq!(test.exp_group_id, group_id, "test {}", i); if !test.exp_group_name.is_empty() { - let group_name = - group_name_from_id(&group_id, Some(test.type_option), &test.setting_content); + let group_name = group_name_from_id(&group_id, test.type_option, &test.setting_content); assert_eq!(test.exp_group_name, group_name, "test {}", i); } } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs index fe9c85f4e5a2..49c3c101471b 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs @@ -1,6 +1,7 @@ use std::sync::Arc; -use collab_database::fields::Field; +use async_trait::async_trait; +use collab_database::fields::{Field, TypeOptionData}; use collab_database::rows::{Cells, Row, RowDetail}; use flowy_error::FlowyResult; @@ -40,6 +41,7 @@ impl DefaultGroupController { } } +#[async_trait] impl GroupControllerOperation for DefaultGroupController { fn field_id(&self) -> &str { &self.field_id @@ -102,8 +104,11 @@ impl GroupControllerOperation for DefaultGroupController { Ok(None) } - fn apply_group_setting_changeset(&mut self, _changeset: GroupChangesets) -> FlowyResult<()> { - Ok(()) + async fn apply_group_changeset( + &mut self, + _changeset: &GroupChangesets, + ) -> FlowyResult { + Ok(TypeOptionData::default()) } fn apply_group_configuration_setting_changeset( @@ -115,7 +120,7 @@ impl GroupControllerOperation for DefaultGroupController { } impl GroupController for DefaultGroupController { - fn did_update_field_type_option(&mut self, _field: &Arc) { + fn did_update_field_type_option(&mut self, _field: &Field) { // Do nothing } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs index 1fd9d3f2f6ae..c90c5df3d37b 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs @@ -1,21 +1,20 @@ -use std::sync::Arc; - +use async_trait::async_trait; use collab_database::fields::{Field, TypeOptionData}; use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail}; use serde::{Deserialize, Serialize}; -use crate::entities::{FieldType, GroupRowsNotificationPB, SelectOptionCellDataPB}; +use crate::entities::{FieldType, GroupRowsNotificationPB}; use crate::services::cell::insert_select_option_cell; use crate::services::field::{ MultiSelectTypeOption, SelectOption, SelectOptionCellDataParser, SelectTypeOptionSharedAction, + TypeOption, }; use crate::services::group::action::GroupCustomize; -use crate::services::group::controller::{ - BaseGroupController, GroupController, GroupsBuilder, MoveGroupRowContext, -}; +use crate::services::group::controller::{BaseGroupController, GroupController}; use crate::services::group::{ add_or_remove_select_option_row, generate_select_option_groups, make_no_status_group, - move_group_row, remove_select_option_row, GeneratedGroups, GroupContext, + move_group_row, remove_select_option_row, GeneratedGroups, GroupChangeset, GroupContext, + GroupOperationInterceptor, GroupsBuilder, MoveGroupRowContext, }; #[derive(Default, Serialize, Deserialize)] @@ -28,18 +27,20 @@ pub type MultiSelectOptionGroupContext = GroupContext; impl GroupCustomize for MultiSelectGroupController { - type CellData = SelectOptionCellDataPB; - - fn can_group(&self, content: &str, cell_data: &Self::CellData) -> bool { - cell_data - .select_options - .iter() - .any(|option| option.id == content) + type GroupTypeOption = MultiSelectTypeOption; + + fn can_group( + &self, + content: &str, + cell_data: &::CellData, + ) -> bool { + cell_data.iter().any(|option_id| option_id == content) } fn placeholder_cell(&self) -> Option { @@ -53,7 +54,7 @@ impl GroupCustomize for MultiSelectGroupController { fn add_or_remove_row_when_cell_changed( &mut self, row_detail: &RowDetail, - cell_data: &Self::CellData, + cell_data: &::CellProtobufType, ) -> Vec { let mut changesets = vec![]; self.context.iter_mut_status_groups(|group| { @@ -64,7 +65,11 @@ impl GroupCustomize for MultiSelectGroupController { changesets } - fn delete_row(&mut self, row: &Row, cell_data: &Self::CellData) -> Vec { + fn delete_row( + &mut self, + row: &Row, + cell_data: &::CellData, + ) -> Vec { let mut changesets = vec![]; self.context.iter_mut_status_groups(|group| { if let Some(changeset) = remove_select_option_row(group, cell_data, row) { @@ -76,7 +81,7 @@ impl GroupCustomize for MultiSelectGroupController { fn move_row( &mut self, - _cell_data: &Self::CellData, + _cell_data: &::CellProtobufType, mut context: MoveGroupRowContext, ) -> Vec { let mut group_changeset = vec![]; @@ -90,7 +95,7 @@ impl GroupCustomize for MultiSelectGroupController { } impl GroupController for MultiSelectGroupController { - fn did_update_field_type_option(&mut self, _field: &Arc) {} + fn did_update_field_type_option(&mut self, _field: &Field) {} fn will_create_row(&mut self, cells: &mut Cells, field: &Field, group_id: &str) { match self.context.get_group(group_id) { @@ -107,49 +112,56 @@ impl GroupController for MultiSelectGroupController { group.add_row(row_detail.clone()) } } - - fn update_group_name(&mut self, group_id: &str, group_name: &str) -> Option { - match &self.type_option { - Some(type_option) => { - let select_option = type_option - .options - .iter() - .find(|option| option.id == group_id) - .unwrap(); - - let new_select_option = SelectOption { - name: group_name.to_owned(), - ..select_option.to_owned() - }; - - let mut new_type_option = type_option.clone(); - new_type_option.insert_option(new_select_option); - - Some(new_type_option.to_type_option_data()) - }, - None => None, - } - } } -pub struct MultiSelectGroupGenerator; -impl GroupsBuilder for MultiSelectGroupGenerator { +pub struct MultiSelectGroupBuilder; +#[async_trait] +impl GroupsBuilder for MultiSelectGroupBuilder { type Context = MultiSelectOptionGroupContext; - type TypeOptionType = MultiSelectTypeOption; + type GroupTypeOption = MultiSelectTypeOption; - fn build( + async fn build( field: &Field, _context: &Self::Context, - type_option: &Option, + type_option: &Self::GroupTypeOption, ) -> GeneratedGroups { - let group_configs = match type_option { - None => vec![], - Some(type_option) => generate_select_option_groups(&field.id, &type_option.options), - }; - + let group_configs = generate_select_option_groups(&field.id, &type_option.options); GeneratedGroups { no_status_group: Some(make_no_status_group(field)), group_configs, } } } + +pub struct MultiSelectGroupOperationInterceptorImpl; + +#[async_trait] +impl GroupOperationInterceptor for MultiSelectGroupOperationInterceptorImpl { + type GroupTypeOption = MultiSelectTypeOption; + + #[tracing::instrument(level = "trace", skip_all)] + async fn type_option_from_group_changeset( + &self, + changeset: &GroupChangeset, + type_option: &Self::GroupTypeOption, + _view_id: &str, + ) -> Option { + if let Some(name) = &changeset.name { + let mut new_type_option = type_option.clone(); + let select_option = type_option + .options + .iter() + .find(|option| option.id == changeset.group_id) + .unwrap(); + + let new_select_option = SelectOption { + name: name.to_owned(), + ..select_option.to_owned() + }; + new_type_option.insert_option(new_select_option); + return Some(new_type_option.into()); + } + + None + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs index c00ab4c3a164..de94d8e1d0d4 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs @@ -1,21 +1,22 @@ -use std::sync::Arc; - +use async_trait::async_trait; use collab_database::fields::{Field, TypeOptionData}; use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail}; use serde::{Deserialize, Serialize}; -use crate::entities::{FieldType, GroupRowsNotificationPB, SelectOptionCellDataPB}; +use crate::entities::{FieldType, GroupRowsNotificationPB}; use crate::services::cell::insert_select_option_cell; use crate::services::field::{ SelectOption, SelectOptionCellDataParser, SelectTypeOptionSharedAction, SingleSelectTypeOption, + TypeOption, }; use crate::services::group::action::GroupCustomize; -use crate::services::group::controller::{ - BaseGroupController, GroupController, GroupsBuilder, MoveGroupRowContext, -}; +use crate::services::group::controller::{BaseGroupController, GroupController}; use crate::services::group::controller_impls::select_option_controller::util::*; use crate::services::group::entities::GroupData; -use crate::services::group::{make_no_status_group, GeneratedGroups, GroupContext}; +use crate::services::group::{ + make_no_status_group, GeneratedGroups, GroupChangeset, GroupContext, GroupOperationInterceptor, + GroupsBuilder, MoveGroupRowContext, +}; #[derive(Default, Serialize, Deserialize)] pub struct SingleSelectGroupConfiguration { @@ -28,17 +29,19 @@ pub type SingleSelectOptionGroupContext = GroupContext; impl GroupCustomize for SingleSelectGroupController { - type CellData = SelectOptionCellDataPB; - fn can_group(&self, content: &str, cell_data: &Self::CellData) -> bool { - cell_data - .select_options - .iter() - .any(|option| option.id == content) + type GroupTypeOption = SingleSelectTypeOption; + fn can_group( + &self, + content: &str, + cell_data: &::CellData, + ) -> bool { + cell_data.iter().any(|option_id| option_id == content) } fn placeholder_cell(&self) -> Option { @@ -52,7 +55,7 @@ impl GroupCustomize for SingleSelectGroupController { fn add_or_remove_row_when_cell_changed( &mut self, row_detail: &RowDetail, - cell_data: &Self::CellData, + cell_data: &::CellProtobufType, ) -> Vec { let mut changesets = vec![]; self.context.iter_mut_status_groups(|group| { @@ -63,7 +66,11 @@ impl GroupCustomize for SingleSelectGroupController { changesets } - fn delete_row(&mut self, row: &Row, cell_data: &Self::CellData) -> Vec { + fn delete_row( + &mut self, + row: &Row, + cell_data: &::CellData, + ) -> Vec { let mut changesets = vec![]; self.context.iter_mut_status_groups(|group| { if let Some(changeset) = remove_select_option_row(group, cell_data, row) { @@ -75,7 +82,7 @@ impl GroupCustomize for SingleSelectGroupController { fn move_row( &mut self, - _cell_data: &Self::CellData, + _cell_data: &::CellProtobufType, mut context: MoveGroupRowContext, ) -> Vec { let mut group_changeset = vec![]; @@ -89,7 +96,7 @@ impl GroupCustomize for SingleSelectGroupController { } impl GroupController for SingleSelectGroupController { - fn did_update_field_type_option(&mut self, _field: &Arc) {} + fn did_update_field_type_option(&mut self, _field: &Field) {} fn will_create_row(&mut self, cells: &mut Cells, field: &Field, group_id: &str) { let group: Option<&mut GroupData> = self.context.get_mut_group(group_id); @@ -107,44 +114,19 @@ impl GroupController for SingleSelectGroupController { group.add_row(row_detail.clone()) } } - - fn update_group_name(&mut self, group_id: &str, group_name: &str) -> Option { - match &self.type_option { - Some(type_option) => { - let select_option = type_option - .options - .iter() - .find(|option| option.id == group_id) - .unwrap(); - - let new_select_option = SelectOption { - name: group_name.to_owned(), - ..select_option.to_owned() - }; - - let mut new_type_option = type_option.clone(); - new_type_option.insert_option(new_select_option); - - Some(new_type_option.to_type_option_data()) - }, - None => None, - } - } } -pub struct SingleSelectGroupGenerator(); -impl GroupsBuilder for SingleSelectGroupGenerator { +pub struct SingleSelectGroupBuilder(); +#[async_trait] +impl GroupsBuilder for SingleSelectGroupBuilder { type Context = SingleSelectOptionGroupContext; - type TypeOptionType = SingleSelectTypeOption; - fn build( + type GroupTypeOption = SingleSelectTypeOption; + async fn build( field: &Field, _context: &Self::Context, - type_option: &Option, + type_option: &Self::GroupTypeOption, ) -> GeneratedGroups { - let group_configs = match type_option { - None => vec![], - Some(type_option) => generate_select_option_groups(&field.id, &type_option.options), - }; + let group_configs = generate_select_option_groups(&field.id, &type_option.options); GeneratedGroups { no_status_group: Some(make_no_status_group(field)), @@ -152,3 +134,36 @@ impl GroupsBuilder for SingleSelectGroupGenerator { } } } + +pub struct SingleSelectGroupOperationInterceptorImpl; + +#[async_trait] +impl GroupOperationInterceptor for SingleSelectGroupOperationInterceptorImpl { + type GroupTypeOption = SingleSelectTypeOption; + + #[tracing::instrument(level = "trace", skip_all)] + async fn type_option_from_group_changeset( + &self, + changeset: &GroupChangeset, + type_option: &Self::GroupTypeOption, + _view_id: &str, + ) -> Option { + if let Some(name) = &changeset.name { + let mut new_type_option = type_option.clone(); + let select_option = type_option + .options + .iter() + .find(|option| option.id == changeset.group_id) + .unwrap(); + + let new_select_option = SelectOption { + name: name.to_owned(), + ..select_option.to_owned() + }; + new_type_option.insert_option(new_select_option); + return Some(new_type_option.into()); + } + + None + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs index 2251a4ae0639..e68b1be0edb6 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs @@ -8,9 +8,8 @@ use crate::entities::{ use crate::services::cell::{ insert_checkbox_cell, insert_date_cell, insert_select_option_cell, insert_url_cell, }; -use crate::services::field::{SelectOption, CHECK}; -use crate::services::group::controller::MoveGroupRowContext; -use crate::services::group::{GeneratedGroupConfig, Group, GroupData}; +use crate::services::field::{SelectOption, SelectOptionIds, CHECK}; +use crate::services::group::{GeneratedGroupConfig, Group, GroupData, MoveGroupRowContext}; pub fn add_or_remove_select_option_row( group: &mut GroupData, @@ -52,12 +51,12 @@ pub fn add_or_remove_select_option_row( pub fn remove_select_option_row( group: &mut GroupData, - cell_data: &SelectOptionCellDataPB, + cell_data: &SelectOptionIds, row: &Row, ) -> Option { let mut changeset = GroupRowsNotificationPB::new(group.id.clone()); - cell_data.select_options.iter().for_each(|option| { - if option.id == group.id && group.contains_row(&row.id) { + cell_data.iter().for_each(|option_id| { + if option_id == &group.id && group.contains_row(&row.id) { group.remove_row(&row.id); changeset.deleted_rows.push(row.id.clone().into_inner()); } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs index f08ab426d53c..4e85557101da 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs @@ -1,6 +1,7 @@ use std::sync::Arc; -use collab_database::fields::Field; +use async_trait::async_trait; +use collab_database::fields::{Field, TypeOptionData}; use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail}; use serde::{Deserialize, Serialize}; @@ -8,17 +9,16 @@ use flowy_error::FlowyResult; use crate::entities::{ FieldType, GroupPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB, RowMetaPB, - URLCellDataPB, }; use crate::services::cell::insert_url_cell; -use crate::services::field::{URLCellData, URLCellDataParser, URLTypeOption}; +use crate::services::field::{TypeOption, URLCellData, URLCellDataParser, URLTypeOption}; use crate::services::group::action::GroupCustomize; use crate::services::group::configuration::GroupContext; -use crate::services::group::controller::{ - BaseGroupController, GroupController, GroupsBuilder, MoveGroupRowContext, -}; +use crate::services::group::controller::{BaseGroupController, GroupController}; use crate::services::group::{ make_no_status_group, move_group_row, GeneratedGroupConfig, GeneratedGroups, Group, + GroupChangeset, GroupOperationInterceptor, GroupTypeOptionCellOperation, GroupsBuilder, + MoveGroupRowContext, }; #[derive(Default, Serialize, Deserialize)] @@ -26,13 +26,18 @@ pub struct URLGroupConfiguration { pub hide_empty: bool, } -pub type URLGroupController = - BaseGroupController; +pub type URLGroupController = BaseGroupController< + URLGroupConfiguration, + URLTypeOption, + URLGroupGenerator, + URLCellDataParser, + URLGroupOperationInterceptorImpl, +>; pub type URLGroupContext = GroupContext; impl GroupCustomize for URLGroupController { - type CellData = URLCellDataPB; + type GroupTypeOption = URLTypeOption; fn placeholder_cell(&self) -> Option { Some( @@ -42,15 +47,19 @@ impl GroupCustomize for URLGroupController { ) } - fn can_group(&self, content: &str, cell_data: &Self::CellData) -> bool { - cell_data.content == content + fn can_group( + &self, + content: &str, + cell_data: &::CellData, + ) -> bool { + cell_data.data == content } fn create_or_delete_group_when_cell_changed( &mut self, - row_detail: &RowDetail, - _old_cell_data: Option<&Self::CellData>, - _cell_data: &Self::CellData, + _row_detail: &RowDetail, + _old_cell_data: Option<&::CellProtobufType>, + _cell_data: &::CellProtobufType, ) -> FlowyResult<(Option, Option)> { // Just return if the group with this url already exists let mut inserted_group = None; @@ -58,7 +67,7 @@ impl GroupCustomize for URLGroupController { let cell_data: URLCellData = _cell_data.clone().into(); let group = make_group_from_url_cell(&cell_data); let mut new_group = self.context.add_new_group(group)?; - new_group.group.rows.push(RowMetaPB::from(row_detail)); + new_group.group.rows.push(RowMetaPB::from(_row_detail)); inserted_group = Some(new_group); } @@ -90,7 +99,7 @@ impl GroupCustomize for URLGroupController { fn add_or_remove_row_when_cell_changed( &mut self, row_detail: &RowDetail, - cell_data: &Self::CellData, + cell_data: &::CellProtobufType, ) -> Vec { let mut changesets = vec![]; self.context.iter_mut_status_groups(|group| { @@ -116,7 +125,11 @@ impl GroupCustomize for URLGroupController { changesets } - fn delete_row(&mut self, row: &Row, _cell_data: &Self::CellData) -> Vec { + fn delete_row( + &mut self, + row: &Row, + _cell_data: &::CellData, + ) -> Vec { let mut changesets = vec![]; self.context.iter_mut_groups(|group| { let mut changeset = GroupRowsNotificationPB::new(group.id.clone()); @@ -134,7 +147,7 @@ impl GroupCustomize for URLGroupController { fn move_row( &mut self, - _cell_data: &Self::CellData, + _cell_data: &::CellProtobufType, mut context: MoveGroupRowContext, ) -> Vec { let mut group_changeset = vec![]; @@ -145,11 +158,10 @@ impl GroupCustomize for URLGroupController { }); group_changeset } - fn delete_group_when_move_row( &mut self, _row: &Row, - _cell_data: &Self::CellData, + _cell_data: &::CellProtobufType, ) -> Option { let mut deleted_group = None; if let Some((_, group)) = self.context.get_group(&_cell_data.content) { @@ -167,7 +179,7 @@ impl GroupCustomize for URLGroupController { } impl GroupController for URLGroupController { - fn did_update_field_type_option(&mut self, _field: &Arc) {} + fn did_update_field_type_option(&mut self, _field: &Field) {} fn will_create_row(&mut self, cells: &mut Cells, field: &Field, group_id: &str) { match self.context.get_group(group_id) { @@ -187,17 +199,18 @@ impl GroupController for URLGroupController { } pub struct URLGroupGenerator(); +#[async_trait] impl GroupsBuilder for URLGroupGenerator { type Context = URLGroupContext; - type TypeOptionType = URLTypeOption; + type GroupTypeOption = URLTypeOption; - fn build( + async fn build( field: &Field, context: &Self::Context, - _type_option: &Option, + _type_option: &Self::GroupTypeOption, ) -> GeneratedGroups { // Read all the cells for the grouping field - let cells = futures::executor::block_on(context.get_all_cells()); + let cells = context.get_all_cells().await; // Generate the groups let group_configs = cells @@ -223,3 +236,21 @@ fn make_group_from_url_cell(cell: &URLCellData) -> Group { let group_name = cell.data.clone(); Group::new(group_id, group_name) } + +pub struct URLGroupOperationInterceptorImpl { + #[allow(dead_code)] + pub(crate) cell_writer: Arc, +} + +#[async_trait::async_trait] +impl GroupOperationInterceptor for URLGroupOperationInterceptorImpl { + type GroupTypeOption = URLTypeOption; + async fn type_option_from_group_changeset( + &self, + _changeset: &GroupChangeset, + _type_option: &Self::GroupTypeOption, + _view_id: &str, + ) -> Option { + todo!() + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/group/entities.rs b/frontend/rust-lib/flowy-database2/src/services/group/entities.rs index 72c35c91b9bb..f7427f95102c 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/entities.rs @@ -20,7 +20,13 @@ pub struct GroupSettingChangeset { } pub struct GroupChangesets { - pub update_groups: Vec, + pub changesets: Vec, +} + +impl From> for GroupChangesets { + fn from(changesets: Vec) -> Self { + Self { changesets } + } } #[derive(Clone, Default, Debug)] diff --git a/frontend/rust-lib/flowy-database2/src/services/group/group_builder.rs b/frontend/rust-lib/flowy-database2/src/services/group/group_builder.rs index bc610302be6b..36f0c5b568d8 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/group_builder.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/group_builder.rs @@ -1,19 +1,82 @@ +use std::collections::HashMap; use std::sync::Arc; +use async_trait::async_trait; use collab_database::fields::Field; -use collab_database::rows::RowDetail; +use collab_database::rows::{Cell, RowDetail, RowId}; use collab_database::views::DatabaseLayout; use flowy_error::FlowyResult; use crate::entities::FieldType; +use crate::services::field::TypeOption; use crate::services::group::{ - CheckboxGroupContext, CheckboxGroupController, DateGroupContext, DateGroupController, - DefaultGroupController, Group, GroupController, GroupSetting, GroupSettingReader, - GroupSettingWriter, MultiSelectGroupController, MultiSelectOptionGroupContext, - SingleSelectGroupController, SingleSelectOptionGroupContext, URLGroupContext, URLGroupController, + CheckboxGroupContext, CheckboxGroupController, CheckboxGroupOperationInterceptorImpl, + DateGroupContext, DateGroupController, DateGroupOperationInterceptorImpl, DefaultGroupController, + Group, GroupController, GroupSetting, GroupSettingReader, GroupSettingWriter, + GroupTypeOptionCellOperation, MultiSelectGroupController, + MultiSelectGroupOperationInterceptorImpl, MultiSelectOptionGroupContext, + SingleSelectGroupController, SingleSelectGroupOperationInterceptorImpl, + SingleSelectOptionGroupContext, URLGroupContext, URLGroupController, + URLGroupOperationInterceptorImpl, }; +/// The [GroupsBuilder] trait is used to generate the groups for different [FieldType] +#[async_trait] +pub trait GroupsBuilder: Send + Sync + 'static { + type Context; + type GroupTypeOption: TypeOption; + + async fn build( + field: &Field, + context: &Self::Context, + type_option: &Self::GroupTypeOption, + ) -> GeneratedGroups; +} + +pub struct GeneratedGroups { + pub no_status_group: Option, + pub group_configs: Vec, +} + +pub struct GeneratedGroupConfig { + pub group: Group, + pub filter_content: String, +} + +pub struct MoveGroupRowContext<'a> { + pub row_detail: &'a RowDetail, + pub row_changeset: &'a mut RowChangeset, + pub field: &'a Field, + pub to_group_id: &'a str, + pub to_row_id: Option, +} + +#[derive(Debug, Clone)] +pub struct RowChangeset { + pub row_id: RowId, + pub height: Option, + pub visibility: Option, + // Contains the key/value changes represents as the update of the cells. For example, + // if there is one cell was changed, then the `cell_by_field_id` will only have one key/value. + pub cell_by_field_id: HashMap, +} + +impl RowChangeset { + pub fn new(row_id: RowId) -> Self { + Self { + row_id, + height: None, + visibility: None, + cell_by_field_id: Default::default(), + } + } + + pub fn is_empty(&self) -> bool { + self.height.is_none() && self.visibility.is_none() && self.cell_by_field_id.is_empty() + } +} + /// Returns a group controller. /// /// Each view can be grouped by one field, each field has its own group controller. @@ -31,16 +94,18 @@ use crate::services::group::{ fields(grouping_field_id=%grouping_field.id, grouping_field_type) err )] -pub async fn make_group_controller( +pub async fn make_group_controller( view_id: String, grouping_field: Arc, row_details: Vec>, setting_reader: R, setting_writer: W, + type_option_cell_writer: TW, ) -> FlowyResult> where R: GroupSettingReader, W: GroupSettingWriter, + TW: GroupTypeOptionCellOperation, { let grouping_field_type = FieldType::from(grouping_field.field_type); tracing::Span::current().record("grouping_field", &grouping_field_type.default_name()); @@ -48,6 +113,7 @@ where let mut group_controller: Box; let configuration_reader = Arc::new(setting_reader); let configuration_writer = Arc::new(setting_writer); + let type_option_cell_writer = Arc::new(type_option_cell_writer); match grouping_field_type { FieldType::SingleSelect => { @@ -58,7 +124,10 @@ where configuration_writer, ) .await?; - let controller = SingleSelectGroupController::new(&grouping_field, configuration).await?; + let operation_interceptor = SingleSelectGroupOperationInterceptorImpl; + let controller = + SingleSelectGroupController::new(&grouping_field, configuration, operation_interceptor) + .await?; group_controller = Box::new(controller); }, FieldType::MultiSelect => { @@ -69,7 +138,10 @@ where configuration_writer, ) .await?; - let controller = MultiSelectGroupController::new(&grouping_field, configuration).await?; + let operation_interceptor = MultiSelectGroupOperationInterceptorImpl; + let controller = + MultiSelectGroupController::new(&grouping_field, configuration, operation_interceptor) + .await?; group_controller = Box::new(controller); }, FieldType::Checkbox => { @@ -80,7 +152,9 @@ where configuration_writer, ) .await?; - let controller = CheckboxGroupController::new(&grouping_field, configuration).await?; + let operation_interceptor = CheckboxGroupOperationInterceptorImpl {}; + let controller = + CheckboxGroupController::new(&grouping_field, configuration, operation_interceptor).await?; group_controller = Box::new(controller); }, FieldType::URL => { @@ -91,7 +165,11 @@ where configuration_writer, ) .await?; - let controller = URLGroupController::new(&grouping_field, configuration).await?; + let operation_interceptor = URLGroupOperationInterceptorImpl { + cell_writer: type_option_cell_writer, + }; + let controller = + URLGroupController::new(&grouping_field, configuration, operation_interceptor).await?; group_controller = Box::new(controller); }, FieldType::DateTime => { @@ -102,7 +180,9 @@ where configuration_writer, ) .await?; - let controller = DateGroupController::new(&grouping_field, configuration).await?; + let operation_interceptor = DateGroupOperationInterceptorImpl {}; + let controller = + DateGroupController::new(&grouping_field, configuration, operation_interceptor).await?; group_controller = Box::new(controller); }, _ => { diff --git a/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs b/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs index 3540ba9c2328..fe4cf2d55c88 100644 --- a/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs @@ -26,7 +26,7 @@ pub trait SortDelegate: Send + Sync { fn get_sort(&self, view_id: &str, sort_id: &str) -> Fut>>; /// Returns all the rows after applying grid's filter fn get_rows(&self, view_id: &str) -> Fut>>; - fn get_field(&self, field_id: &str) -> Fut>>; + fn get_field(&self, field_id: &str) -> Option; fn get_fields(&self, view_id: &str, field_ids: Option>) -> Fut>>; } diff --git a/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs index 55ab2bc1d49b..a8a39c645ad7 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs @@ -10,7 +10,6 @@ use flowy_database2::services::field::{ edit_single_select_type_option, SelectOption, SelectTypeOptionSharedAction, SingleSelectTypeOption, }; -use flowy_database2::services::group::GroupSettingChangeset; use lib_infra::util::timestamp; use crate::database::database_editor::DatabaseEditorTest; @@ -68,12 +67,6 @@ pub enum GroupScript { group_id: String, group_name: String, }, - AssertGroupConfiguration { - hide_ungrouped: bool, - }, - UpdateGroupConfiguration { - hide_ungrouped: Option, - }, } pub struct DatabaseGroupTest { @@ -276,25 +269,6 @@ impl DatabaseGroupTest { assert_eq!(group_id, group.group_id, "group index: {}", group_index); assert_eq!(group_name, group.group_name, "group index: {}", group_index); }, - GroupScript::AssertGroupConfiguration { hide_ungrouped } => { - let group_configuration = self - .editor - .get_group_configuration_settings(&self.view_id) - .await - .unwrap(); - let group_configuration = group_configuration.get(0).unwrap(); - assert_eq!(group_configuration.hide_ungrouped, hide_ungrouped); - }, - GroupScript::UpdateGroupConfiguration { hide_ungrouped } => { - self - .editor - .update_group_configuration_setting( - &self.view_id, - GroupSettingChangeset { hide_ungrouped }, - ) - .await - .unwrap(); - }, } } diff --git a/frontend/rust-lib/flowy-error/Cargo.toml b/frontend/rust-lib/flowy-error/Cargo.toml index ac48436bb789..2a06f299efb4 100644 --- a/frontend/rust-lib/flowy-error/Cargo.toml +++ b/frontend/rust-lib/flowy-error/Cargo.toml @@ -12,6 +12,7 @@ bytes = "1.4" anyhow = "1.0" thiserror = "1.0" validator = "0.16.0" +tokio = { version = "1.0", features = ["sync"]} fancy-regex = { version = "0.11.0" } lib-dispatch = { workspace = true, optional = true } diff --git a/frontend/rust-lib/flowy-error/src/errors.rs b/frontend/rust-lib/flowy-error/src/errors.rs index d5a462058d18..358ed13eb8cc 100644 --- a/frontend/rust-lib/flowy-error/src/errors.rs +++ b/frontend/rust-lib/flowy-error/src/errors.rs @@ -157,3 +157,9 @@ impl From for FlowyError { FlowyError::internal().with_context(e) } } + +impl From for FlowyError { + fn from(e: tokio::sync::oneshot::error::RecvError) -> Self { + FlowyError::internal().with_context(e) + } +} From 197da5f425327f1f36ed58db724da5b3df75dfaf Mon Sep 17 00:00:00 2001 From: Kritarth Sharma <42487588+Kritarthsharma@users.noreply.github.com> Date: Sat, 28 Oct 2023 16:41:52 +0530 Subject: [PATCH 02/56] fix: removed the parent padding and instead applied padding individually. (#3814) * fix: Removed the parent padding and instead applied individual padding to each child to ensure it doesn't affect the sidebar divider. * chore: Addressed comments and made changes --- .../home/menu/sidebar/sidebar.dart | 55 +++++++++++-------- .../menu/sidebar/sidebar_new_page_button.dart | 5 +- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart index d613e21e0978..e2497aebf06d 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart @@ -87,6 +87,7 @@ class HomeSideBar extends StatelessWidget { List views, List favoriteViews, ) { + const menuHorizontalInset = EdgeInsets.symmetric(horizontal: 12); return DecoratedBox( decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceVariant, @@ -94,19 +95,25 @@ class HomeSideBar extends StatelessWidget { right: BorderSide(color: Theme.of(context).dividerColor), ), ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - // top menu - const SidebarTopMenu(), - // user, setting - SidebarUser(user: user, views: views), - const VSpace(20), - // scrollable document list - Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + // top menu + const Padding( + padding: menuHorizontalInset, + child: SidebarTopMenu(), + ), + // user, setting + Padding( + padding: menuHorizontalInset, + child: SidebarUser(user: user, views: views), + ), + const VSpace(20), + // scrollable document list + Expanded( + child: Padding( + padding: menuHorizontalInset, child: SingleChildScrollView( child: SidebarFolder( views: views, @@ -114,17 +121,17 @@ class HomeSideBar extends StatelessWidget { ), ), ), - const VSpace(10), - // trash - const SidebarTrashButton(), - const VSpace(10), - // new page button - const Padding( - padding: EdgeInsets.only(left: 6.0), - child: SidebarNewPageButton(), - ), - ], - ), + ), + const VSpace(10), + // trash + const Padding( + padding: menuHorizontalInset, + child: SidebarTrashButton(), + ), + const VSpace(10), + // new page button + const SidebarNewPageButton(), + ], ), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart index 18d443745d98..f5411a3fecf2 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart @@ -48,7 +48,10 @@ class SidebarNewPageButton extends StatelessWidget { height: 60, child: TopBorder( color: Theme.of(context).dividerColor, - child: child, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18), + child: child, + ), ), ); } From 8a735799c6737744d4a1765b52a7d5544a7353c7 Mon Sep 17 00:00:00 2001 From: Nikhil Raj <112814295+gantanikhilraj@users.noreply.github.com> Date: Sat, 28 Oct 2023 19:41:32 +0530 Subject: [PATCH 03/56] Added tooltip when hovering over No-Data (#3612) * Added tooltip when hovering over NO-Data * chore: i18n for tooltip * fix: wrong token * chore: use flowytooltip --------- Co-authored-by: Richard Shiue <71320345+richardshiue@users.noreply.github.com> --- .../calendar/presentation/calendar_page.dart | 11 ++++++++--- frontend/resources/translations/en.json | 6 +++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart index 1fbd80ae3dcb..c205f3d79220 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart @@ -14,6 +14,7 @@ import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -375,9 +376,13 @@ class _UnscheduledEventsButtonState extends State { _popoverController.show(); } }, - child: FlowyText.regular( - "${LocaleKeys.calendar_settings_noDateTitle.tr()} (${state.unscheduleEvents.length})", - fontSize: 10, + child: FlowyTooltip( + message: LocaleKeys.calendar_settings_noDateHint + .plural(state.unscheduleEvents.length), + child: FlowyText.regular( + "${LocaleKeys.calendar_settings_noDateTitle.tr()} (${state.unscheduleEvents.length})", + fontSize: 10, + ), ), ), popupBuilder: (context) { diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 76185d7b7067..1915afd37269 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -771,7 +771,11 @@ "firstDayOfWeek": "Start week on", "layoutDateField": "Layout calendar by", "noDateTitle": "No Date", - "noDateHint": "Unscheduled events will show up here", + "noDateHint": { + "zero": "Unscheduled events will show up here", + "one": "{} unscheduled event", + "other": "{} unscheduled events" + }, "clickToAdd": "Click to add to the calendar", "name": "Calendar layout" }, From 993532a2f01668e717b32034ee6c3c10d7561cab Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Sat, 28 Oct 2023 23:13:27 +0800 Subject: [PATCH 04/56] fix: edit card title not working (#3819) --- .../plugins/database_view/board/application/board_bloc.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart index b398dec9e889..d64c15485752 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart @@ -112,6 +112,7 @@ class BoardBloc extends Bloc { startEditingRow: (group, row) { emit( state.copyWith( + isEditingRow: true, editingRow: BoardEditingRow( group: group, row: row, @@ -130,7 +131,7 @@ class BoardBloc extends Bloc { false, ); - emit(state.copyWith(isEditingRow: false)); + emit(state.copyWith(isEditingRow: false, editingRow: null)); } }, didReceiveGridUpdate: (DatabasePB grid) { From b9a25f449f58232b017f620d4c704363425ba88e Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Sun, 29 Oct 2023 11:26:49 +0800 Subject: [PATCH 05/56] refactor: hide ungrouped feature (#3817) * refactor: remove unused notification and listener * revert: remove hide_ungrouped from group settings * chore: add board layout setting * chore: listen to layout settings on ui * fix: duplicated group controller initialization * chore: add a tooltip to the ungrouped items button * chore: trailing comma --- .../application/database_controller.dart | 65 +++---------- .../application/database_view_service.dart | 7 ++ .../application/group/group_listener.dart | 16 --- .../layout/calendar_setting_listener.dart | 50 ---------- .../application/setting/group_bloc.dart | 47 ++++----- .../board/application/board_bloc.dart | 35 ++++--- .../board/presentation/board_page.dart | 3 +- .../presentation/ungrouped_items_button.dart | 44 +++++---- .../calendar/application/calendar_bloc.dart | 11 ++- .../widgets/group/database_group.dart | 19 +++- .../widgets/setting/setting_button.dart | 4 +- frontend/resources/translations/en.json | 1 + .../src/entities/board_entities.rs | 25 +++++ .../src/entities/group_entities/group.rs | 26 +---- .../flowy-database2/src/entities/mod.rs | 2 + .../src/entities/setting_entities.rs | 50 +++++++++- .../flowy-database2/src/event_handler.rs | 44 +-------- .../rust-lib/flowy-database2/src/event_map.rs | 12 +-- .../flowy-database2/src/notification.rs | 18 ++-- .../src/services/database/database_editor.rs | 52 ++-------- .../src/services/database/util.rs | 22 +++-- .../src/services/database_view/layout_deps.rs | 26 ++++- .../src/services/database_view/view_editor.rs | 97 ++++++++----------- .../src/services/group/action.rs | 10 +- .../src/services/group/configuration.rs | 19 +--- .../src/services/group/controller.rs | 13 +-- .../controller_impls/default_controller.rs | 12 +-- .../src/services/group/entities.rs | 14 +-- .../src/services/group/group_builder.rs | 2 +- .../flowy-database2/src/services/group/mod.rs | 2 +- .../src/services/setting/entities.rs | 29 ++++++ .../rust-lib/flowy-database2/src/template.rs | 7 +- .../tests/database/group_test/test.rs | 17 ---- .../tests/database/layout_test/script.rs | 70 ++++++++++--- .../tests/database/layout_test/test.rs | 22 +++++ .../database/mock_data/board_mock_data.rs | 10 +- 36 files changed, 418 insertions(+), 485 deletions(-) delete mode 100644 frontend/appflowy_flutter/lib/plugins/database_view/application/layout/calendar_setting_listener.dart create mode 100644 frontend/rust-lib/flowy-database2/src/entities/board_entities.rs diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart index 6616dd80d1ae..0640c1fc6fca 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart @@ -1,7 +1,7 @@ import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; import 'package:appflowy/plugins/database_view/application/view/view_cache.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/board_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; @@ -47,12 +47,10 @@ class GroupCallbacks { } class DatabaseLayoutSettingCallbacks { - final void Function(DatabaseLayoutSettingPB) onLayoutChanged; - final void Function(DatabaseLayoutSettingPB) onLoadLayout; + final void Function(DatabaseLayoutSettingPB) onLayoutSettingsChanged; DatabaseLayoutSettingCallbacks({ - required this.onLayoutChanged, - required this.onLoadLayout, + required this.onLayoutSettingsChanged, }); } @@ -125,11 +123,11 @@ class DatabaseController { void addListener({ DatabaseCallbacks? onDatabaseChanged, - DatabaseLayoutSettingCallbacks? onLayoutChanged, + DatabaseLayoutSettingCallbacks? onLayoutSettingsChanged, GroupCallbacks? onGroupChanged, }) { - if (onLayoutChanged != null) { - _layoutCallbacks.add(onLayoutChanged); + if (onLayoutSettingsChanged != null) { + _layoutCallbacks.add(onLayoutSettingsChanged); } if (onDatabaseChanged != null) { @@ -228,12 +226,14 @@ class DatabaseController { ); } - Future updateLayoutSetting( - CalendarLayoutSettingPB calendarlLayoutSetting, - ) async { + Future updateLayoutSetting({ + BoardLayoutSettingPB? boardLayoutSetting, + CalendarLayoutSettingPB? calendarLayoutSetting, + }) async { await _databaseViewBackendSvc .updateLayoutSetting( - calendarLayoutSetting: calendarlLayoutSetting, + boardLayoutSetting: boardLayoutSetting, + calendarLayoutSetting: calendarLayoutSetting, layoutType: databaseLayout, ) .then((result) { @@ -241,15 +241,6 @@ class DatabaseController { }); } - void updateGroupConfiguration(bool hideUngrouped) async { - final payload = GroupSettingChangesetPB( - viewId: viewId, - groupConfigurationId: "", - hideUngrouped: hideUngrouped, - ); - DatabaseEventUpdateGroupConfiguration(payload).send(); - } - Future dispose() async { await _databaseViewBackendSvc.closeView(); await fieldController.dispose(); @@ -261,9 +252,6 @@ class DatabaseController { } Future _loadGroups() async { - final configResult = await loadGroupConfigurations(viewId: viewId); - _handleGroupConfigurationChanged(configResult); - final groupsResult = await _databaseViewBackendSvc.loadGroups(); groupsResult.fold( (groups) { @@ -280,10 +268,9 @@ class DatabaseController { result.fold( (newDatabaseLayoutSetting) { databaseLayoutSetting = newDatabaseLayoutSetting; - databaseLayoutSetting?.freeze(); for (final callback in _layoutCallbacks) { - callback.onLoadLayout(newDatabaseLayoutSetting); + callback.onLayoutSettingsChanged(newDatabaseLayoutSetting); } }, (r) => Log.error(r), @@ -339,7 +326,6 @@ class DatabaseController { void _listenOnGroupChanged() { _groupListener.start( - onGroupConfigurationChanged: _handleGroupConfigurationChanged, onNumOfGroupsChanged: (result) { result.fold( (changeset) { @@ -386,7 +372,7 @@ class DatabaseController { databaseLayoutSetting?.freeze(); for (final callback in _layoutCallbacks) { - callback.onLayoutChanged(newLayout); + callback.onLayoutSettingsChanged(newLayout); } }, (r) => Log.error(r), @@ -394,29 +380,6 @@ class DatabaseController { }, ); } - - Future, FlowyError>> loadGroupConfigurations({ - required String viewId, - }) { - final payload = DatabaseViewIdPB(value: viewId); - - return DatabaseEventGetGroupConfigurations(payload).send().then((result) { - return result.fold((l) => left(l.items), (r) => right(r)); - }); - } - - void _handleGroupConfigurationChanged( - Either, FlowyError> result, - ) { - result.fold( - (configurations) { - for (final callback in _groupCallbacks) { - callback.onGroupConfigurationChanged?.call(configurations); - } - }, - (r) => Log.error(r), - ); - } } class RowDataBuilder { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart index 623f99b94d28..4cfeff80754b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart @@ -1,4 +1,5 @@ import 'package:appflowy/plugins/database_view/application/row/row_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/board_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/group_changeset.pb.dart'; @@ -114,11 +115,17 @@ class DatabaseViewBackendService { Future> updateLayoutSetting({ required DatabaseLayoutPB layoutType, + BoardLayoutSettingPB? boardLayoutSetting, CalendarLayoutSettingPB? calendarLayoutSetting, }) { final payload = LayoutSettingChangesetPB.create() ..viewId = viewId ..layoutType = layoutType; + + if (boardLayoutSetting != null) { + payload.board = boardLayoutSetting; + } + if (calendarLayoutSetting != null) { payload.calendar = calendarLayoutSetting; } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/group/group_listener.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/group/group_listener.dart index bd419da29b0c..a07b287f43d1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/group/group_listener.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/group/group_listener.dart @@ -15,8 +15,6 @@ typedef GroupByNewFieldValue = Either, FlowyError>; class DatabaseGroupListener { final String viewId; - PublishNotifier? _groupConfigurationNotifier = - PublishNotifier(); PublishNotifier? _numOfGroupsNotifier = PublishNotifier(); PublishNotifier? _groupByFieldNotifier = PublishNotifier(); @@ -24,13 +22,9 @@ class DatabaseGroupListener { DatabaseGroupListener(this.viewId); void start({ - required void Function(GroupConfigurationUpdateValue) - onGroupConfigurationChanged, required void Function(GroupUpdateValue) onNumOfGroupsChanged, required void Function(GroupByNewFieldValue) onGroupByNewField, }) { - _groupConfigurationNotifier - ?.addPublishListener(onGroupConfigurationChanged); _numOfGroupsNotifier?.addPublishListener(onNumOfGroupsChanged); _groupByFieldNotifier?.addPublishListener(onGroupByNewField); _listener = DatabaseNotificationListener( @@ -44,13 +38,6 @@ class DatabaseGroupListener { Either result, ) { switch (ty) { - case DatabaseNotification.DidUpdateGroupConfiguration: - result.fold( - (payload) => _groupConfigurationNotifier?.value = - left(RepeatedGroupSettingPB.fromBuffer(payload).items), - (error) => _groupConfigurationNotifier?.value = right(error), - ); - break; case DatabaseNotification.DidUpdateNumOfGroups: result.fold( (payload) => _numOfGroupsNotifier?.value = @@ -72,9 +59,6 @@ class DatabaseGroupListener { Future stop() async { await _listener?.stop(); - _groupConfigurationNotifier?.dispose(); - _groupConfigurationNotifier = null; - _numOfGroupsNotifier?.dispose(); _numOfGroupsNotifier = null; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/calendar_setting_listener.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/calendar_setting_listener.dart deleted file mode 100644 index 57e2c112a0b1..000000000000 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/calendar_setting_listener.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'dart:typed_data'; - -import 'package:appflowy/core/notification/grid_notification.dart'; -import 'package:flowy_infra/notifier.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:dartz/dartz.dart'; - -typedef NewLayoutFieldValue = Either; - -class DatabaseCalendarLayoutListener { - final String viewId; - PublishNotifier? _newLayoutFieldNotifier = - PublishNotifier(); - DatabaseNotificationListener? _listener; - DatabaseCalendarLayoutListener(this.viewId); - - void start({ - required void Function(NewLayoutFieldValue) onCalendarLayoutChanged, - }) { - _newLayoutFieldNotifier?.addPublishListener(onCalendarLayoutChanged); - _listener = DatabaseNotificationListener( - objectId: viewId, - handler: _handler, - ); - } - - void _handler( - DatabaseNotification ty, - Either result, - ) { - switch (ty) { - case DatabaseNotification.DidSetNewLayoutField: - result.fold( - (payload) => _newLayoutFieldNotifier?.value = - left(DatabaseLayoutSettingPB.fromBuffer(payload)), - (error) => _newLayoutFieldNotifier?.value = right(error), - ); - break; - default: - break; - } - } - - Future stop() async { - await _listener?.stop(); - _newLayoutFieldNotifier?.dispose(); - _newLayoutFieldNotifier = null; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/group_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/group_bloc.dart index 22504f6562e3..3e6bacba5953 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/group_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/group_bloc.dart @@ -1,6 +1,7 @@ import 'package:appflowy/plugins/database_view/application/database_controller.dart'; import 'package:appflowy/plugins/database_view/application/field/field_info.dart'; import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/board_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -14,7 +15,7 @@ class DatabaseGroupBloc extends Bloc { final DatabaseController _databaseController; final GroupBackendService _groupBackendSvc; Function(List)? _onFieldsFn; - GroupCallbacks? _groupCallbacks; + DatabaseLayoutSettingCallbacks? _layoutSettingCallbacks; DatabaseGroupBloc({ required String viewId, @@ -25,13 +26,13 @@ class DatabaseGroupBloc extends Bloc { DatabaseGroupState.initial( viewId, databaseController.fieldController.fieldInfos, + databaseController.databaseLayoutSetting!.board, ), ) { on( (event, emit) async { event.when( initial: () { - _loadGroupConfigurations(); _startListening(); }, didReceiveFieldUpdate: (fieldInfos) { @@ -43,8 +44,8 @@ class DatabaseGroupBloc extends Bloc { ); result.fold((l) => null, (err) => Log.error(err)); }, - didUpdateHideUngrouped: (bool hideUngrouped) { - emit(state.copyWith(hideUngrouped: hideUngrouped)); + didUpdateLayoutSettings: (layoutSettings) { + emit(state.copyWith(layoutSettings: layoutSettings)); }, ); }, @@ -58,7 +59,7 @@ class DatabaseGroupBloc extends Bloc { .removeListener(onFieldsListener: _onFieldsFn!); _onFieldsFn = null; } - _groupCallbacks = null; + _layoutSettingCallbacks = null; return super.close(); } @@ -70,32 +71,18 @@ class DatabaseGroupBloc extends Bloc { listenWhen: () => !isClosed, ); - _groupCallbacks = GroupCallbacks( - onGroupConfigurationChanged: (configurations) { - if (isClosed) { + _layoutSettingCallbacks = DatabaseLayoutSettingCallbacks( + onLayoutSettingsChanged: (layoutSettings) { + if (isClosed || !layoutSettings.hasBoard()) { return; } - final configuration = configurations.first; add( - DatabaseGroupEvent.didUpdateHideUngrouped( - configuration.hideUngrouped, - ), + DatabaseGroupEvent.didUpdateLayoutSettings(layoutSettings.board), ); }, ); - _databaseController.addListener(onGroupChanged: _groupCallbacks); - } - - void _loadGroupConfigurations() async { - final configResult = await _databaseController.loadGroupConfigurations( - viewId: _databaseController.viewId, - ); - configResult.fold( - (configurations) { - final hideUngrouped = configurations.first.hideUngrouped; - add(DatabaseGroupEvent.didUpdateHideUngrouped(hideUngrouped)); - }, - (err) => Log.error(err), + _databaseController.addListener( + onLayoutSettingsChanged: _layoutSettingCallbacks, ); } } @@ -110,8 +97,9 @@ class DatabaseGroupEvent with _$DatabaseGroupEvent { const factory DatabaseGroupEvent.didReceiveFieldUpdate( List fields, ) = _DidReceiveFieldUpdate; - const factory DatabaseGroupEvent.didUpdateHideUngrouped(bool hideUngrouped) = - _DidUpdateHideUngrouped; + const factory DatabaseGroupEvent.didUpdateLayoutSettings( + BoardLayoutSettingPB layoutSettings, + ) = _DidUpdateLayoutSettings; } @freezed @@ -119,16 +107,17 @@ class DatabaseGroupState with _$DatabaseGroupState { const factory DatabaseGroupState({ required String viewId, required List fieldInfos, - required bool hideUngrouped, + required BoardLayoutSettingPB layoutSettings, }) = _DatabaseGroupState; factory DatabaseGroupState.initial( String viewId, List fieldInfos, + BoardLayoutSettingPB layoutSettings, ) => DatabaseGroupState( viewId: viewId, fieldInfos: fieldInfos, - hideUngrouped: true, + layoutSettings: layoutSettings, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart index d64c15485752..bd5decdd8d43 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart @@ -147,8 +147,8 @@ class BoardBloc extends Bloc { ), ); }, - didUpdateHideUngrouped: (bool hideUngrouped) { - emit(state.copyWith(hideUngrouped: hideUngrouped)); + didUpdateLayoutSettings: (layoutSettings) { + emit(state.copyWith(layoutSettings: layoutSettings)); }, startEditingHeader: (String groupId) { emit( @@ -200,7 +200,7 @@ class BoardBloc extends Bloc { if (ungroupedGroupIndex != -1) { ungroupedGroup = groups[ungroupedGroupIndex]; final group = groups.removeAt(ungroupedGroupIndex); - if (!state.hideUngrouped) { + if (!(state.layoutSettings?.hideUngroupedColumn ?? false)) { groups.add(group); } } @@ -230,20 +230,21 @@ class BoardBloc extends Bloc { } }, ); - final onGroupChanged = GroupCallbacks( - onGroupConfigurationChanged: (configurations) { - if (isClosed) return; - final config = configurations.first; - if (config.hideUngrouped) { - boardController.removeGroup(config.fieldId); + final onLayoutSettingsChanged = DatabaseLayoutSettingCallbacks( + onLayoutSettingsChanged: (layoutSettings) { + if (isClosed || !layoutSettings.hasBoard()) { + return; + } + if (layoutSettings.board.hideUngroupedColumn) { + boardController.removeGroup(ungroupedGroup!.fieldId); } else if (ungroupedGroup != null) { final newGroup = initializeGroupData(ungroupedGroup!); - final controller = initializeGroupController(ungroupedGroup!); - groupControllers[controller.group.groupId] = (controller); boardController.addGroup(newGroup); } - add(BoardEvent.didUpdateHideUngrouped(config.hideUngrouped)); + add(BoardEvent.didUpdateLayoutSettings(layoutSettings.board)); }, + ); + final onGroupChanged = GroupCallbacks( onGroupByField: (groups) { if (isClosed) return; ungroupedGroup = null; @@ -274,6 +275,7 @@ class BoardBloc extends Bloc { databaseController.addListener( onDatabaseChanged: onDatabaseChanged, + onLayoutSettingsChanged: onLayoutSettingsChanged, onGroupChanged: onGroupChanged, ); } @@ -360,8 +362,9 @@ class BoardEvent with _$BoardEvent { ) = _DidReceiveGridUpdate; const factory BoardEvent.didReceiveGroups(List groups) = _DidReceiveGroups; - const factory BoardEvent.didUpdateHideUngrouped(bool hideUngrouped) = - _DidUpdateHideUngrouped; + const factory BoardEvent.didUpdateLayoutSettings( + BoardLayoutSettingPB layoutSettings, + ) = _DidUpdateLayoutSettings; } @freezed @@ -376,7 +379,7 @@ class BoardState with _$BoardState { BoardEditingRow? editingRow, required LoadingState loadingState, required Option noneOrError, - required bool hideUngrouped, + required BoardLayoutSettingPB? layoutSettings, }) = _BoardState; factory BoardState.initial(String viewId) => BoardState( @@ -387,7 +390,7 @@ class BoardState with _$BoardState { isEditingRow: false, noneOrError: none(), loadingState: const LoadingState.loading(), - hideUngrouped: false, + layoutSettings: null, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart index 4a3673a0e875..f2d584d9be43 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart @@ -167,7 +167,8 @@ class _BoardContentState extends State { mainAxisSize: MainAxisSize.min, children: [ const VSpace(8.0), - if (state.hideUngrouped) _buildBoardHeader(context), + if (state.layoutSettings?.hideUngroupedColumn ?? false) + _buildBoardHeader(context), Expanded( child: AppFlowyBoard( boardScrollController: scrollManager, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/ungrouped_items_button.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/ungrouped_items_button.dart index b77809b11ff4..dc1370832186 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/ungrouped_items_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/ungrouped_items_button.dart @@ -17,6 +17,7 @@ import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -61,31 +62,36 @@ class _UnscheduledEventsButtonState extends State { offset: const Offset(0, 8), constraints: const BoxConstraints(maxWidth: 282, maxHeight: 600), - child: OutlinedButton( - style: OutlinedButton.styleFrom( - shape: RoundedRectangleBorder( + child: FlowyTooltip( + message: LocaleKeys.board_ungroupedButtonTooltip.tr(), + child: OutlinedButton( + style: OutlinedButton.styleFrom( + shape: RoundedRectangleBorder( + side: BorderSide( + color: Theme.of(context).dividerColor, + width: 1, + ), + borderRadius: Corners.s6Border, + ), side: BorderSide( color: Theme.of(context).dividerColor, width: 1, ), - borderRadius: Corners.s6Border, + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + visualDensity: VisualDensity.compact, ), - side: BorderSide( - color: Theme.of(context).dividerColor, - width: 1, + onPressed: () { + if (state.ungroupedItems.isNotEmpty) { + _popoverController.show(); + } + }, + child: FlowyText.regular( + "${LocaleKeys.board_ungroupedButtonText.tr()} (${state.ungroupedItems.length})", + fontSize: 10, ), - padding: - const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - visualDensity: VisualDensity.compact, - ), - onPressed: () { - if (state.ungroupedItems.isNotEmpty) { - _popoverController.show(); - } - }, - child: FlowyText.regular( - "${LocaleKeys.board_ungroupedButtonText.tr()} (${state.ungroupedItems.length})", - fontSize: 10, ), ), popupBuilder: (context) { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart index 6a518e3975c6..f4a2cbdf7aa0 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart @@ -198,7 +198,9 @@ class CalendarBloc extends Bloc { Future _updateCalendarLayoutSetting( CalendarLayoutSettingPB layoutSetting, ) async { - return databaseController.updateLayoutSetting(layoutSetting); + return databaseController.updateLayoutSetting( + calendarLayoutSetting: layoutSetting, + ); } Future?> _loadEvent(RowId rowId) async { @@ -319,14 +321,13 @@ class CalendarBloc extends Bloc { }, ); - final onLayoutChanged = DatabaseLayoutSettingCallbacks( - onLayoutChanged: _didReceiveLayoutSetting, - onLoadLayout: _didReceiveLayoutSetting, + final onLayoutSettingsChanged = DatabaseLayoutSettingCallbacks( + onLayoutSettingsChanged: _didReceiveLayoutSetting, ); databaseController.addListener( onDatabaseChanged: onDatabaseChanged, - onLayoutChanged: onLayoutChanged, + onLayoutSettingsChanged: onLayoutSettingsChanged, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/group/database_group.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/group/database_group.dart index 066cf9f1b693..20699de28ff3 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/group/database_group.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/group/database_group.dart @@ -8,6 +8,7 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/common/ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/board_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -18,6 +19,7 @@ import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:protobuf/protobuf.dart' hide FieldInfo; class DatabaseGroupList extends StatelessWidget { final String viewId; @@ -60,9 +62,9 @@ class DatabaseGroupList extends StatelessWidget { ), ), Toggle( - value: !state.hideUngrouped, + value: !state.layoutSettings.hideUngroupedColumn, onChanged: (value) => - databaseController.updateGroupConfiguration(value), + _updateLayoutSettings(state.layoutSettings, value), style: ToggleStyle.big, padding: EdgeInsets.zero, ), @@ -105,6 +107,19 @@ class DatabaseGroupList extends StatelessWidget { ), ); } + + Future _updateLayoutSettings( + BoardLayoutSettingPB layoutSettings, + bool hideUngrouped, + ) { + layoutSettings.freeze(); + final newLayoutSetting = layoutSettings.rebuild((message) { + message.hideUngroupedColumn = hideUngrouped; + }); + return databaseController.updateLayoutSetting( + boardLayoutSetting: newLayoutSetting, + ); + } } class _GridGroupCell extends StatelessWidget { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/setting_button.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/setting_button.dart index 5b809371eb45..160ff16db5fc 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/setting_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/setting_button.dart @@ -115,7 +115,9 @@ class ICalendarSettingImpl extends ICalendarSetting { @override void updateLayoutSettings(CalendarLayoutSettingPB layoutSettings) { - _databaseController.updateLayoutSetting(layoutSettings); + _databaseController.updateLayoutSetting( + calendarLayoutSetting: layoutSettings, + ); } @override diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 1915afd37269..4adb7acc98aa 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -751,6 +751,7 @@ "menuName": "Board", "showUngrouped": "Show ungrouped items", "ungroupedButtonText": "Ungrouped", + "ungroupedButtonTooltip": "Contains cards that don't belong in any group", "ungroupedItemsTitle": "Click to add to the board", "groupBy": "Group by", "referencedBoardPrefix": "View of" diff --git a/frontend/rust-lib/flowy-database2/src/entities/board_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/board_entities.rs new file mode 100644 index 000000000000..addab49033b5 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/board_entities.rs @@ -0,0 +1,25 @@ +use flowy_derive::ProtoBuf; + +use crate::services::setting::BoardLayoutSetting; + +#[derive(Debug, Clone, Default, Eq, PartialEq, ProtoBuf)] +pub struct BoardLayoutSettingPB { + #[pb(index = 1)] + pub hide_ungrouped_column: bool, +} + +impl From for BoardLayoutSettingPB { + fn from(setting: BoardLayoutSetting) -> Self { + Self { + hide_ungrouped_column: setting.hide_ungrouped_column, + } + } +} + +impl From for BoardLayoutSetting { + fn from(setting: BoardLayoutSettingPB) -> Self { + Self { + hide_ungrouped_column: setting.hide_ungrouped_column, + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/group_entities/group.rs b/frontend/rust-lib/flowy-database2/src/entities/group_entities/group.rs index 6e7c2a598fa8..7200c2987e33 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/group_entities/group.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/group_entities/group.rs @@ -5,7 +5,7 @@ use flowy_error::ErrorCode; use crate::entities::parser::NotEmptyStr; use crate::entities::{FieldType, RowMetaPB}; -use crate::services::group::{GroupChangeset, GroupData, GroupSetting, GroupSettingChangeset}; +use crate::services::group::{GroupChangeset, GroupData, GroupSetting}; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct GroupSettingPB { @@ -14,9 +14,6 @@ pub struct GroupSettingPB { #[pb(index = 2)] pub field_id: String, - - #[pb(index = 3)] - pub hide_ungrouped: bool, } impl std::convert::From<&GroupSetting> for GroupSettingPB { @@ -24,7 +21,6 @@ impl std::convert::From<&GroupSetting> for GroupSettingPB { GroupSettingPB { id: rev.id.clone(), field_id: rev.field_id.clone(), - hide_ungrouped: rev.hide_ungrouped, } } } @@ -52,26 +48,6 @@ impl std::convert::From> for RepeatedGroupSettingPB { } } -#[derive(Debug, Default, ProtoBuf)] -pub struct GroupSettingChangesetPB { - #[pb(index = 1)] - pub view_id: String, - - #[pb(index = 2)] - pub group_configuration_id: String, - - #[pb(index = 3, one_of)] - pub hide_ungrouped: Option, -} - -impl From for GroupSettingChangeset { - fn from(value: GroupSettingChangesetPB) -> Self { - Self { - hide_ungrouped: value.hide_ungrouped, - } - } -} - #[derive(ProtoBuf, Debug, Default, Clone)] pub struct RepeatedGroupPB { #[pb(index = 1)] diff --git a/frontend/rust-lib/flowy-database2/src/entities/mod.rs b/frontend/rust-lib/flowy-database2/src/entities/mod.rs index 0afd82595287..7721615a9749 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/mod.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/mod.rs @@ -1,3 +1,4 @@ +mod board_entities; mod calendar_entities; mod cell_entities; mod database_entities; @@ -16,6 +17,7 @@ mod macros; mod share_entities; mod type_option_entities; +pub use board_entities::*; pub use calendar_entities::*; pub use cell_entities::*; pub use database_entities::*; diff --git a/frontend/rust-lib/flowy-database2/src/entities/setting_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/setting_entities.rs index 4a6f3de1a2f4..7bd4284ecbfa 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/setting_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/setting_entities.rs @@ -13,7 +13,9 @@ use crate::entities::{ RepeatedSortPB, UpdateFilterParams, UpdateFilterPayloadPB, UpdateGroupPB, UpdateSortParams, UpdateSortPayloadPB, }; -use crate::services::setting::CalendarLayoutSetting; +use crate::services::setting::{BoardLayoutSetting, CalendarLayoutSetting}; + +use super::BoardLayoutSettingPB; /// [DatabaseViewSettingPB] defines the setting options for the grid. Such as the filter, group, and sort. #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] @@ -151,19 +153,51 @@ pub struct DatabaseLayoutSettingPB { pub layout_type: DatabaseLayoutPB, #[pb(index = 2, one_of)] + pub board: Option, + + #[pb(index = 3, one_of)] pub calendar: Option, } +impl DatabaseLayoutSettingPB { + pub fn from_board(layout_setting: BoardLayoutSetting) -> Self { + Self { + layout_type: DatabaseLayoutPB::Board, + board: Some(layout_setting.into()), + calendar: None, + } + } + + pub fn from_calendar(layout_setting: CalendarLayoutSetting) -> Self { + Self { + layout_type: DatabaseLayoutPB::Calendar, + calendar: Some(layout_setting.into()), + board: None, + } + } +} + #[derive(Debug, Clone, Default)] pub struct LayoutSettingParams { pub layout_type: DatabaseLayout, + pub board: Option, pub calendar: Option, } +impl LayoutSettingParams { + pub fn new(layout_type: DatabaseLayout) -> Self { + Self { + layout_type, + ..Default::default() + } + } +} + impl From for DatabaseLayoutSettingPB { fn from(data: LayoutSettingParams) -> Self { Self { layout_type: data.layout_type.into(), + board: data.board.map(|board| board.into()), calendar: data.calendar.map(|calendar| calendar.into()), } } @@ -178,6 +212,9 @@ pub struct LayoutSettingChangesetPB { pub layout_type: DatabaseLayoutPB, #[pb(index = 3, one_of)] + pub board: Option, + + #[pb(index = 4, one_of)] pub calendar: Option, } @@ -185,9 +222,17 @@ pub struct LayoutSettingChangesetPB { pub struct LayoutSettingChangeset { pub view_id: String, pub layout_type: DatabaseLayout, + pub board: Option, pub calendar: Option, } +impl LayoutSettingChangeset { + pub fn is_valid(&self) -> bool { + self.board.is_some() && self.layout_type == DatabaseLayout::Board + || self.calendar.is_some() && self.layout_type == DatabaseLayout::Calendar + } +} + impl TryInto for LayoutSettingChangesetPB { type Error = ErrorCode; @@ -199,7 +244,8 @@ impl TryInto for LayoutSettingChangesetPB { Ok(LayoutSettingChangeset { view_id, layout_type: self.layout_type.into(), - calendar: self.calendar.map(|calendar| calendar.into()), + board: self.board.map(Into::into), + calendar: self.calendar.map(Into::into), }) } } diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index 310311181244..06547c720ff3 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -646,36 +646,6 @@ pub(crate) async fn update_date_cell_handler( Ok(()) } -#[tracing::instrument(level = "trace", skip_all, err)] -pub(crate) async fn get_group_configurations_handler( - data: AFPluginData, - manager: AFPluginState>, -) -> DataResult { - let manager = upgrade_manager(manager)?; - let params = data.into_inner(); - let database_editor = manager.get_database_with_view_id(params.as_ref()).await?; - let group_configs = database_editor - .get_group_configuration_settings(params.as_ref()) - .await?; - data_result_ok(group_configs.into()) -} - -#[tracing::instrument(level = "trace", skip_all, err)] -pub(crate) async fn update_group_configuration_handler( - data: AFPluginData, - manager: AFPluginState>, -) -> Result<(), FlowyError> { - let manager = upgrade_manager(manager)?; - let params = data.into_inner(); - let view_id = params.view_id.clone(); - let database_editor = manager.get_database_with_view_id(&view_id).await?; - database_editor - .update_group_configuration_setting(&view_id, params.into()) - .await?; - - Ok(()) -} - #[tracing::instrument(level = "trace", skip_all, err)] pub(crate) async fn get_groups_handler( data: AFPluginData, @@ -786,15 +756,11 @@ pub(crate) async fn set_layout_setting_handler( manager: AFPluginState>, ) -> FlowyResult<()> { let manager = upgrade_manager(manager)?; - let params: LayoutSettingChangeset = data.into_inner().try_into()?; - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; - let layout_params = LayoutSettingParams { - layout_type: params.layout_type, - calendar: params.calendar, - }; - database_editor - .set_layout_setting(¶ms.view_id, layout_params) - .await; + let changeset = data.into_inner(); + let view_id = changeset.view_id.clone(); + let params: LayoutSettingChangeset = changeset.try_into()?; + let database_editor = manager.get_database_with_view_id(&view_id).await?; + database_editor.set_layout_setting(&view_id, params).await?; Ok(()) } diff --git a/frontend/rust-lib/flowy-database2/src/event_map.rs b/frontend/rust-lib/flowy-database2/src/event_map.rs index 594feac09c68..4bb000afae99 100644 --- a/frontend/rust-lib/flowy-database2/src/event_map.rs +++ b/frontend/rust-lib/flowy-database2/src/event_map.rs @@ -54,8 +54,6 @@ pub fn init(database_manager: Weak) -> AFPlugin { // Date .event(DatabaseEvent::UpdateDateCell, update_date_cell_handler) // Group - .event(DatabaseEvent::GetGroupConfigurations, get_group_configurations_handler) - .event(DatabaseEvent::UpdateGroupConfiguration, update_group_configuration_handler) .event(DatabaseEvent::SetGroupByField, set_group_by_field_handler) .event(DatabaseEvent::MoveGroup, move_group_handler) .event(DatabaseEvent::MoveGroupRow, move_group_row_handler) @@ -266,14 +264,10 @@ pub enum DatabaseEvent { #[event(input = "DateChangesetPB")] UpdateDateCell = 80, - #[event(input = "DatabaseViewIdPB", output = "RepeatedGroupSettingPB")] - GetGroupConfigurations = 90, - - #[event(input = "GroupSettingChangesetPB")] - UpdateGroupConfiguration = 91, - + /// [SetGroupByField] event is used to create a new grouping in a database + /// view based on the `field_id` #[event(input = "GroupByFieldPayloadPB")] - SetGroupByField = 92, + SetGroupByField = 90, #[event(input = "DatabaseViewIdPB", output = "RepeatedGroupPB")] GetGroups = 100, diff --git a/frontend/rust-lib/flowy-database2/src/notification.rs b/frontend/rust-lib/flowy-database2/src/notification.rs index acf90e471704..5d7e7e1df44c 100644 --- a/frontend/rust-lib/flowy-database2/src/notification.rs +++ b/frontend/rust-lib/flowy-database2/src/notification.rs @@ -20,8 +20,6 @@ pub enum DatabaseNotification { DidUpdateCell = 40, /// Trigger after editing a field properties including rename,update type option, etc DidUpdateField = 50, - /// Trigger after the group configuration is changed - DidUpdateGroupConfiguration = 59, /// Trigger after the number of groups is changed DidUpdateNumOfGroups = 60, /// Trigger after inserting/deleting/updating/moving a row @@ -42,18 +40,16 @@ pub enum DatabaseNotification { DidUpdateSettings = 70, // Trigger when the layout setting of the database is updated DidUpdateLayoutSettings = 80, - // Trigger when the layout field of the database is changed - DidSetNewLayoutField = 81, // Trigger when the layout of the database is changed - DidUpdateDatabaseLayout = 82, + DidUpdateDatabaseLayout = 81, // Trigger when the database view is deleted - DidDeleteDatabaseView = 83, + DidDeleteDatabaseView = 82, // Trigger when the database view is moved to trash - DidMoveDatabaseViewToTrash = 84, - DidUpdateDatabaseSyncUpdate = 85, - DidUpdateDatabaseSnapshotState = 86, + DidMoveDatabaseViewToTrash = 83, + DidUpdateDatabaseSyncUpdate = 84, + DidUpdateDatabaseSnapshotState = 85, // Trigger when the field setting is changed - DidUpdateFieldSettings = 87, + DidUpdateFieldSettings = 86, } impl std::convert::From for i32 { @@ -71,7 +67,6 @@ impl std::convert::From for DatabaseNotification { 22 => DatabaseNotification::DidUpdateFields, 40 => DatabaseNotification::DidUpdateCell, 50 => DatabaseNotification::DidUpdateField, - 59 => DatabaseNotification::DidUpdateGroupConfiguration, 60 => DatabaseNotification::DidUpdateNumOfGroups, 61 => DatabaseNotification::DidUpdateGroupRow, 62 => DatabaseNotification::DidGroupByField, @@ -82,7 +77,6 @@ impl std::convert::From for DatabaseNotification { 67 => DatabaseNotification::DidUpdateRowMeta, 70 => DatabaseNotification::DidUpdateSettings, 80 => DatabaseNotification::DidUpdateLayoutSettings, - 81 => DatabaseNotification::DidSetNewLayoutField, 82 => DatabaseNotification::DidUpdateDatabaseLayout, 83 => DatabaseNotification::DidDeleteDatabaseView, 84 => DatabaseNotification::DidMoveDatabaseViewToTrash, diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index b51252a54d18..e5343693de23 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -34,9 +34,7 @@ use crate::services::field_settings::{ default_field_settings_by_layout_map, FieldSettings, FieldSettingsChangesetParams, }; use crate::services::filter::Filter; -use crate::services::group::{ - default_group_setting, GroupChangesets, GroupSetting, GroupSettingChangeset, RowChangeset, -}; +use crate::services::group::{default_group_setting, GroupChangesets, GroupSetting, RowChangeset}; use crate::services::share::csv::{CSVExport, CSVFormat}; use crate::services::sort::Sort; @@ -870,40 +868,6 @@ impl DatabaseEditor { Ok(()) } - pub async fn get_group_configuration_settings( - &self, - view_id: &str, - ) -> FlowyResult> { - let view = self.database_views.get_view_editor(view_id).await?; - - let group_settings = view - .v_get_group_configuration_settings() - .await - .into_iter() - .map(|value| GroupSettingPB::from(&value)) - .collect::>(); - - Ok(group_settings) - } - - pub async fn update_group_configuration_setting( - &self, - view_id: &str, - changeset: GroupSettingChangeset, - ) -> FlowyResult<()> { - let view = self.database_views.get_view_editor(view_id).await?; - let group_configuration = view.v_update_group_configuration_setting(changeset).await?; - - if let Some(configuration) = group_configuration { - let payload: RepeatedGroupSettingPB = vec![configuration].into(); - send_notification(view_id, DatabaseNotification::DidUpdateGroupConfiguration) - .payload(payload) - .send(); - } - - Ok(()) - } - #[tracing::instrument(level = "trace", skip_all, err)] pub async fn load_groups(&self, view_id: &str) -> FlowyResult { let view = self.database_views.get_view_editor(view_id).await?; @@ -994,11 +958,15 @@ impl DatabaseEditor { Ok(()) } - pub async fn set_layout_setting(&self, view_id: &str, layout_setting: LayoutSettingParams) { - tracing::trace!("set_layout_setting: {:?}", layout_setting); - if let Ok(view) = self.database_views.get_view_editor(view_id).await { - let _ = view.v_set_layout_settings(layout_setting).await; - }; + #[tracing::instrument(level = "trace", skip_all)] + pub async fn set_layout_setting( + &self, + view_id: &str, + layout_setting: LayoutSettingChangeset, + ) -> FlowyResult<()> { + let view_editor = self.database_views.get_view_editor(view_id).await?; + view_editor.v_set_layout_settings(layout_setting).await?; + Ok(()) } pub async fn get_layout_setting( diff --git a/frontend/rust-lib/flowy-database2/src/services/database/util.rs b/frontend/rust-lib/flowy-database2/src/services/database/util.rs index 393a8f0c1af1..69bac217c89d 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/util.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/util.rs @@ -1,23 +1,27 @@ -use collab_database::views::DatabaseView; +use collab_database::views::{DatabaseLayout, DatabaseView}; use crate::entities::{ - CalendarLayoutSettingPB, DatabaseLayoutPB, DatabaseLayoutSettingPB, DatabaseViewSettingPB, - FieldSettingsPB, FilterPB, GroupSettingPB, SortPB, + DatabaseLayoutPB, DatabaseLayoutSettingPB, DatabaseViewSettingPB, FieldSettingsPB, FilterPB, + GroupSettingPB, SortPB, }; use crate::services::field_settings::FieldSettings; use crate::services::filter::Filter; use crate::services::group::GroupSetting; -use crate::services::setting::CalendarLayoutSetting; use crate::services::sort::Sort; pub(crate) fn database_view_setting_pb_from_view(view: DatabaseView) -> DatabaseViewSettingPB { let layout_type: DatabaseLayoutPB = view.layout.into(); let layout_setting = if let Some(layout_setting) = view.layout_settings.get(&view.layout) { - let calendar_setting = - CalendarLayoutSettingPB::from(CalendarLayoutSetting::from(layout_setting.clone())); - DatabaseLayoutSettingPB { - layout_type: layout_type.clone(), - calendar: Some(calendar_setting), + match view.layout { + DatabaseLayout::Board => { + let board_setting = layout_setting.clone().into(); + DatabaseLayoutSettingPB::from_board(board_setting) + }, + DatabaseLayout::Calendar => { + let calendar_setting = layout_setting.clone().into(); + DatabaseLayoutSettingPB::from_calendar(calendar_setting) + }, + _ => DatabaseLayoutSettingPB::default(), } } else { DatabaseLayoutSettingPB::default() diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/layout_deps.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/layout_deps.rs index 4171be414696..711ca4d33fb8 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/layout_deps.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/layout_deps.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use crate::entities::FieldType; use crate::services::field::{DateTypeOption, SingleSelectTypeOption}; use crate::services::field_settings::default_field_settings_by_layout_map; -use crate::services::setting::CalendarLayoutSetting; +use crate::services::setting::{BoardLayoutSetting, CalendarLayoutSetting}; /// When creating a database, we need to resolve the dependencies of the views. /// Different database views have different dependencies. For example, a board @@ -32,6 +32,7 @@ impl DatabaseLayoutDepsResolver { match self.database_layout { DatabaseLayout::Grid => (None, None), DatabaseLayout::Board => { + let layout_settings = BoardLayoutSetting::new().into(); if !self .database .lock() @@ -40,9 +41,9 @@ impl DatabaseLayoutDepsResolver { .any(|field| FieldType::from(field.field_type).can_be_group()) { let select_field = self.create_select_field(); - (Some(select_field), None) + (Some(select_field), Some(layout_settings)) } else { - (None, None) + (None, Some(layout_settings)) } }, DatabaseLayout::Calendar => { @@ -74,7 +75,9 @@ impl DatabaseLayoutDepsResolver { // Insert the layout setting if it's not exist match &self.database_layout { DatabaseLayout::Grid => {}, - DatabaseLayout::Board => {}, + DatabaseLayout::Board => { + self.create_board_layout_setting_if_need(view_id); + }, DatabaseLayout::Calendar => { let date_field_id = match fields .into_iter() @@ -97,6 +100,21 @@ impl DatabaseLayoutDepsResolver { } } + fn create_board_layout_setting_if_need(&self, view_id: &str) { + if self + .database + .lock() + .get_layout_setting::(view_id, &self.database_layout) + .is_none() + { + let layout_setting = BoardLayoutSetting::new(); + self + .database + .lock() + .insert_layout_setting(view_id, &self.database_layout, layout_setting); + } + } + fn create_calendar_layout_setting_if_need(&self, view_id: &str, field_id: &str) { if self .database diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs index 66d976047660..8dfc39416b18 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs @@ -13,8 +13,8 @@ use flowy_error::{FlowyError, FlowyResult}; use crate::entities::{ CalendarEventPB, DatabaseLayoutMetaPB, DatabaseLayoutSettingPB, DeleteFilterParams, DeleteGroupParams, DeleteSortParams, FieldType, FieldVisibility, GroupChangesPB, GroupPB, - GroupRowsNotificationPB, InsertedRowPB, LayoutSettingParams, RowMetaPB, RowsChangePB, - SortChangesetNotificationPB, SortPB, UpdateFilterParams, UpdateSortParams, + GroupRowsNotificationPB, InsertedRowPB, LayoutSettingChangeset, LayoutSettingParams, RowMetaPB, + RowsChangePB, SortChangesetNotificationPB, SortPB, UpdateFilterParams, UpdateSortParams, }; use crate::notification::{send_notification, DatabaseNotification}; use crate::services::cell::CellCache; @@ -34,10 +34,7 @@ use crate::services::field_settings::FieldSettings; use crate::services::filter::{ Filter, FilterChangeset, FilterController, FilterType, UpdatedFilterType, }; -use crate::services::group::{ - GroupChangesets, GroupController, GroupSetting, GroupSettingChangeset, MoveGroupRowContext, - RowChangeset, -}; +use crate::services::group::{GroupChangesets, GroupController, MoveGroupRowContext, RowChangeset}; use crate::services::setting::CalendarLayoutSetting; use crate::services::sort::{DeletedSortType, Sort, SortChangeset, SortController, SortType}; @@ -377,19 +374,6 @@ impl DatabaseViewEditor { Ok(()) } - pub async fn v_update_group_configuration_setting( - &self, - changeset: GroupSettingChangeset, - ) -> FlowyResult> { - let result = self - .mut_group_controller(|group_controller, _| { - group_controller.apply_group_configuration_setting_changeset(changeset) - }) - .await; - - Ok(result.flatten()) - } - pub async fn v_update_group(&self, changeset: GroupChangesets) -> FlowyResult<()> { let mut type_option_data = TypeOptionData::new(); let old_field = if let Some(controller) = self.group_controller.write().await.as_mut() { @@ -412,9 +396,6 @@ impl DatabaseViewEditor { Ok(()) } - pub async fn v_get_group_configuration_settings(&self) -> Vec { - self.delegate.get_group_setting(&self.view_id) - } pub async fn v_get_all_sorts(&self) -> Vec { self.delegate.get_all_sorts(&self.view_id) } @@ -550,7 +531,11 @@ impl DatabaseViewEditor { let mut layout_setting = LayoutSettingParams::default(); match layout_ty { DatabaseLayout::Grid => {}, - DatabaseLayout::Board => {}, + DatabaseLayout::Board => { + if let Some(value) = self.delegate.get_layout_setting(&self.view_id, layout_ty) { + layout_setting.board = Some(value.into()); + } + }, DatabaseLayout::Calendar => { if let Some(value) = self.delegate.get_layout_setting(&self.view_id, layout_ty) { let calendar_setting = CalendarLayoutSetting::from(value); @@ -574,48 +559,50 @@ impl DatabaseViewEditor { layout_setting } - /// Update the calendar settings and send the notification to refresh the UI - pub async fn v_set_layout_settings(&self, params: LayoutSettingParams) -> FlowyResult<()> { - // Maybe it needs no send notification to refresh the UI - if let Some(new_calendar_setting) = params.calendar { - if let Some(field) = self.delegate.get_field(&new_calendar_setting.field_id) { - let field_type = FieldType::from(field.field_type); - if field_type != FieldType::DateTime { - return Err(FlowyError::unexpect_calendar_field_type()); - } + /// Update the layout settings and send the notification to refresh the UI + pub async fn v_set_layout_settings(&self, params: LayoutSettingChangeset) -> FlowyResult<()> { + if self.v_get_layout_type().await != params.layout_type || !params.is_valid() { + return Err(FlowyError::invalid_data()); + } - let old_calender_setting = self - .v_get_layout_settings(¶ms.layout_type) - .await - .calendar; + let layout_setting_pb = match params.layout_type { + DatabaseLayout::Board => { + let layout_setting = params.board.unwrap(); self.delegate.insert_layout_setting( &self.view_id, ¶ms.layout_type, - new_calendar_setting.clone().into(), + layout_setting.clone().into(), ); - let new_field_id = new_calendar_setting.field_id.clone(); - let layout_setting_pb: DatabaseLayoutSettingPB = LayoutSettingParams { - layout_type: params.layout_type, - calendar: Some(new_calendar_setting), - } - .into(); - if let Some(old_calendar_setting) = old_calender_setting { - // compare the new layout field id is equal to old layout field id - // if not equal, send the DidSetNewLayoutField notification - // if equal, send the DidUpdateLayoutSettings notification - if old_calendar_setting.field_id != new_field_id { - send_notification(&self.view_id, DatabaseNotification::DidSetNewLayoutField) - .payload(layout_setting_pb.clone()) - .send(); + Some(DatabaseLayoutSettingPB::from_board(layout_setting)) + }, + DatabaseLayout::Calendar => { + let layout_setting = params.calendar.unwrap(); + + if let Some(field) = self.delegate.get_field(&layout_setting.field_id) { + if FieldType::from(field.field_type) != FieldType::DateTime { + return Err(FlowyError::unexpect_calendar_field_type()); } + + self.delegate.insert_layout_setting( + &self.view_id, + ¶ms.layout_type, + layout_setting.clone().into(), + ); + + Some(DatabaseLayoutSettingPB::from_calendar(layout_setting)) + } else { + None } + }, + _ => None, + }; - send_notification(&self.view_id, DatabaseNotification::DidUpdateLayoutSettings) - .payload(layout_setting_pb) - .send(); - } + if let Some(payload) = layout_setting_pb { + send_notification(&self.view_id, DatabaseNotification::DidUpdateLayoutSettings) + .payload(payload) + .send(); } Ok(()) diff --git a/frontend/rust-lib/flowy-database2/src/services/group/action.rs b/frontend/rust-lib/flowy-database2/src/services/group/action.rs index 376391dbd1a7..1c28fce8ad9b 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/action.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/action.rs @@ -6,10 +6,7 @@ use flowy_error::FlowyResult; use crate::entities::{GroupChangesPB, GroupPB, GroupRowsNotificationPB, InsertedGroupPB}; use crate::services::field::TypeOption; -use crate::services::group::entities::GroupSetting; -use crate::services::group::{ - GroupChangesets, GroupData, GroupSettingChangeset, MoveGroupRowContext, -}; +use crate::services::group::{GroupChangesets, GroupData, MoveGroupRowContext}; /// Using polymorphism to provides the customs action for different group controller. /// @@ -119,11 +116,6 @@ pub trait GroupControllerOperation: Send + Sync { &mut self, changeset: &GroupChangesets, ) -> FlowyResult; - - fn apply_group_configuration_setting_changeset( - &mut self, - changeset: GroupSettingChangeset, - ) -> FlowyResult>; } #[derive(Debug)] diff --git a/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs b/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs index aca56b5495cd..f8f1aa8c2137 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs @@ -18,7 +18,6 @@ use crate::entities::{GroupChangesPB, GroupPB, InsertedGroupPB}; use crate::services::field::RowSingleCellData; use crate::services::group::{ default_group_setting, GeneratedGroups, Group, GroupChangeset, GroupData, GroupSetting, - GroupSettingChangeset, }; pub trait GroupSettingReader: Send + Sync + 'static { @@ -390,20 +389,6 @@ where Ok(()) } - pub(crate) fn update_configuration( - &mut self, - changeset: GroupSettingChangeset, - ) -> FlowyResult> { - self.mut_configuration(|configuration| match changeset.hide_ungrouped { - Some(value) if value != configuration.hide_ungrouped => { - configuration.hide_ungrouped = value; - true - }, - _ => false, - })?; - Ok(Some(GroupSetting::clone(&self.setting))) - } - pub(crate) async fn get_all_cells(&self) -> Vec { self .reader @@ -432,9 +417,7 @@ where let view_id = self.view_id.clone(); tokio::spawn(async move { match writer.save_configuration(&view_id, configuration).await { - Ok(_) => { - tracing::trace!("SUCCESSFULLY SAVED CONFIGURATION"); // TODO(richard): remove this - }, + Ok(_) => {}, Err(e) => { tracing::error!("Save group configuration failed: {}", e); }, diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller.rs index 77845f73380c..ab847755b21e 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller.rs @@ -19,10 +19,8 @@ use crate::services::group::action::{ DidMoveGroupRowResult, DidUpdateGroupRowResult, GroupControllerOperation, GroupCustomize, }; use crate::services::group::configuration::GroupContext; -use crate::services::group::entities::{GroupData, GroupSetting}; -use crate::services::group::{ - GroupChangeset, GroupChangesets, GroupSettingChangeset, GroupsBuilder, MoveGroupRowContext, -}; +use crate::services::group::entities::GroupData; +use crate::services::group::{GroupChangeset, GroupChangesets, GroupsBuilder, MoveGroupRowContext}; // use collab_database::views::Group; @@ -361,13 +359,6 @@ where } Ok(type_option_data) } - - fn apply_group_configuration_setting_changeset( - &mut self, - changeset: GroupSettingChangeset, - ) -> FlowyResult> { - self.context.update_configuration(changeset) - } } struct GroupedRow { diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs index 49c3c101471b..19d02c028520 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs @@ -10,10 +10,7 @@ use crate::entities::GroupChangesPB; use crate::services::group::action::{ DidMoveGroupRowResult, DidUpdateGroupRowResult, GroupControllerOperation, }; -use crate::services::group::{ - GroupChangesets, GroupController, GroupData, GroupSetting, GroupSettingChangeset, - MoveGroupRowContext, -}; +use crate::services::group::{GroupChangesets, GroupController, GroupData, MoveGroupRowContext}; /// A [DefaultGroupController] is used to handle the group actions for the [FieldType] that doesn't /// implement its own group controller. The default group controller only contains one group, which @@ -110,13 +107,6 @@ impl GroupControllerOperation for DefaultGroupController { ) -> FlowyResult { Ok(TypeOptionData::default()) } - - fn apply_group_configuration_setting_changeset( - &mut self, - _changeset: GroupSettingChangeset, - ) -> FlowyResult> { - Ok(None) - } } impl GroupController for DefaultGroupController { diff --git a/frontend/rust-lib/flowy-database2/src/services/group/entities.rs b/frontend/rust-lib/flowy-database2/src/services/group/entities.rs index f7427f95102c..0badcc1e49e1 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/entities.rs @@ -12,11 +12,6 @@ pub struct GroupSetting { pub field_type: i64, pub groups: Vec, pub content: String, - pub hide_ungrouped: bool, -} - -pub struct GroupSettingChangeset { - pub hide_ungrouped: Option, } pub struct GroupChangesets { @@ -38,14 +33,13 @@ pub struct GroupChangeset { } impl GroupSetting { - pub fn new(field_id: String, field_type: i64, content: String, hide_ungrouped: bool) -> Self { + pub fn new(field_id: String, field_type: i64, content: String) -> Self { Self { id: gen_database_group_id(), field_id, field_type, groups: vec![], content, - hide_ungrouped, } } } @@ -55,7 +49,6 @@ const FIELD_ID: &str = "field_id"; const FIELD_TYPE: &str = "ty"; const GROUPS: &str = "groups"; const CONTENT: &str = "content"; -const HIDE_UNGROUPED: &str = "hide_ungrouped"; impl TryFrom for GroupSetting { type Error = anyhow::Error; @@ -65,9 +58,8 @@ impl TryFrom for GroupSetting { value.get_str_value(GROUP_ID), value.get_str_value(FIELD_ID), value.get_i64_value(FIELD_TYPE), - value.get_bool_value(HIDE_UNGROUPED), ) { - (Some(id), Some(field_id), Some(field_type), Some(hide_ungrouped)) => { + (Some(id), Some(field_id), Some(field_type)) => { let content = value.get_str_value(CONTENT).unwrap_or_default(); let groups = value.try_get_array(GROUPS); Ok(Self { @@ -76,7 +68,6 @@ impl TryFrom for GroupSetting { field_type, groups, content, - hide_ungrouped, }) }, _ => { @@ -94,7 +85,6 @@ impl From for GroupSettingMap { .insert_i64_value(FIELD_TYPE, setting.field_type) .insert_maps(GROUPS, setting.groups) .insert_str_value(CONTENT, setting.content) - .insert_bool_value(HIDE_UNGROUPED, setting.hide_ungrouped) .build() } } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/group_builder.rs b/frontend/rust-lib/flowy-database2/src/services/group/group_builder.rs index 36f0c5b568d8..c221c7fdaf1e 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/group_builder.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/group_builder.rs @@ -234,7 +234,7 @@ pub fn find_new_grouping_field( /// pub fn default_group_setting(field: &Field) -> GroupSetting { let field_id = field.id.clone(); - GroupSetting::new(field_id, field.field_type, "".to_owned(), false) + GroupSetting::new(field_id, field.field_type, "".to_owned()) } pub fn make_no_status_group(field: &Field) -> Group { diff --git a/frontend/rust-lib/flowy-database2/src/services/group/mod.rs b/frontend/rust-lib/flowy-database2/src/services/group/mod.rs index fd11447bb801..c9f9e91b655a 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/mod.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/mod.rs @@ -8,5 +8,5 @@ mod group_builder; pub(crate) use configuration::*; pub(crate) use controller::*; pub(crate) use controller_impls::*; -pub use entities::*; +pub(crate) use entities::*; pub(crate) use group_builder::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/setting/entities.rs b/frontend/rust-lib/flowy-database2/src/services/setting/entities.rs index 808d2f7a7529..2e0ee02935b3 100644 --- a/frontend/rust-lib/flowy-database2/src/services/setting/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/setting/entities.rs @@ -89,3 +89,32 @@ impl CalendarLayout { pub const DEFAULT_FIRST_DAY_OF_WEEK: i32 = 0; pub const DEFAULT_SHOW_WEEKENDS: bool = true; pub const DEFAULT_SHOW_WEEK_NUMBERS: bool = true; + +#[derive(Debug, Clone, Default)] +pub struct BoardLayoutSetting { + pub hide_ungrouped_column: bool, +} + +impl BoardLayoutSetting { + pub fn new() -> Self { + Self::default() + } +} + +impl From for BoardLayoutSetting { + fn from(setting: LayoutSetting) -> Self { + Self { + hide_ungrouped_column: setting + .get_bool_value("hide_ungrouped_column") + .unwrap_or_default(), + } + } +} + +impl From for LayoutSetting { + fn from(setting: BoardLayoutSetting) -> Self { + LayoutSettingBuilder::new() + .insert_bool_value("hide_ungrouped_column", setting.hide_ungrouped_column) + .build() + } +} diff --git a/frontend/rust-lib/flowy-database2/src/template.rs b/frontend/rust-lib/flowy-database2/src/template.rs index 4f819ac43aa1..23a217638c8a 100644 --- a/frontend/rust-lib/flowy-database2/src/template.rs +++ b/frontend/rust-lib/flowy-database2/src/template.rs @@ -8,7 +8,7 @@ use crate::services::field::{ FieldBuilder, SelectOption, SelectOptionColor, SingleSelectTypeOption, }; use crate::services::field_settings::DatabaseFieldSettingsMapBuilder; -use crate::services::setting::CalendarLayoutSetting; +use crate::services::setting::{BoardLayoutSetting, CalendarLayoutSetting}; pub fn make_default_grid(view_id: &str, name: &str) -> CreateDatabaseParams { let text_field = FieldBuilder::from_field_type(FieldType::RichText) @@ -93,12 +93,15 @@ pub fn make_default_board(view_id: &str, name: &str) -> CreateDatabaseParams { let field_settings = DatabaseFieldSettingsMapBuilder::new(fields.clone(), DatabaseLayout::Board).build(); + let mut layout_settings = LayoutSettings::default(); + layout_settings.insert(DatabaseLayout::Board, BoardLayoutSetting::new().into()); + CreateDatabaseParams { database_id: gen_database_id(), view_id: view_id.to_string(), name: name.to_string(), layout: DatabaseLayout::Board, - layout_settings: Default::default(), + layout_settings, filters: vec![], groups: vec![], sorts: vec![], diff --git a/frontend/rust-lib/flowy-database2/tests/database/group_test/test.rs b/frontend/rust-lib/flowy-database2/tests/database/group_test/test.rs index fb37bbea6aac..d9fb97a86518 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/group_test/test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/group_test/test.rs @@ -28,23 +28,6 @@ async fn group_init_test() { test.run_scripts(scripts).await; } -// #[tokio::test] -// async fn group_configuration_setting_test() { -// let mut test = DatabaseGroupTest::new().await; -// let scripts = vec![ -// AssertGroupConfiguration { -// hide_ungrouped: false, -// }, -// UpdateGroupConfiguration { -// hide_ungrouped: Some(true), -// }, -// AssertGroupConfiguration { -// hide_ungrouped: true, -// }, -// ]; -// test.run_scripts(scripts).await; -// } - #[tokio::test] async fn group_move_row_test() { let mut test = DatabaseGroupTest::new().await; diff --git a/frontend/rust-lib/flowy-database2/tests/database/layout_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/layout_test/script.rs index 02103f81b7cd..6800a7e4db42 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/layout_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/layout_test/script.rs @@ -1,13 +1,15 @@ use collab_database::fields::Field; use collab_database::views::DatabaseLayout; -use flowy_database2::entities::FieldType; -use flowy_database2::services::setting::CalendarLayoutSetting; +use flowy_database2::entities::{FieldType, LayoutSettingChangeset, LayoutSettingParams}; +use flowy_database2::services::setting::{BoardLayoutSetting, CalendarLayoutSetting}; use crate::database::database_editor::DatabaseEditorTest; pub enum LayoutScript { + AssertBoardLayoutSetting { expected: BoardLayoutSetting }, AssertCalendarLayoutSetting { expected: CalendarLayoutSetting }, + UpdateBoardLayoutSetting { new_setting: BoardLayoutSetting }, AssertDefaultAllCalendarEvents, AssertAllCalendarEventsCount { expected: usize }, UpdateDatabaseLayout { layout: DatabaseLayout }, @@ -23,21 +25,39 @@ impl DatabaseLayoutTest { Self { database_test } } + pub async fn new_board() -> Self { + let database_test = DatabaseEditorTest::new_board().await; + Self { database_test } + } + pub async fn new_calendar() -> Self { let database_test = DatabaseEditorTest::new_calendar().await; Self { database_test } } + pub async fn get_first_date_field(&self) -> Field { + self.database_test.get_first_field(FieldType::DateTime) + } + + async fn get_layout_setting( + &self, + view_id: &str, + layout_ty: DatabaseLayout, + ) -> LayoutSettingParams { + self + .database_test + .editor + .get_layout_setting(view_id, layout_ty) + .await + .unwrap() + } + pub async fn run_scripts(&mut self, scripts: Vec) { for script in scripts { self.run_script(script).await; } } - pub async fn get_first_date_field(&self) -> Field { - self.database_test.get_first_field(FieldType::DateTime) - } - pub async fn run_script(&mut self, script: LayoutScript) { match script { LayoutScript::UpdateDatabaseLayout { layout } => { @@ -56,19 +76,27 @@ impl DatabaseLayoutTest { .await; assert_eq!(events.len(), expected); }, + LayoutScript::AssertBoardLayoutSetting { expected } => { + let view_id = self.database_test.view_id.clone(); + let layout_ty = DatabaseLayout::Board; + + let layout_settings = self.get_layout_setting(&view_id, layout_ty).await; + + assert!(layout_settings.calendar.is_none()); + assert_eq!( + layout_settings.board.unwrap().hide_ungrouped_column, + expected.hide_ungrouped_column + ); + }, LayoutScript::AssertCalendarLayoutSetting { expected } => { let view_id = self.database_test.view_id.clone(); let layout_ty = DatabaseLayout::Calendar; - let calendar_setting = self - .database_test - .editor - .get_layout_setting(&view_id, layout_ty) - .await - .unwrap() - .calendar - .unwrap(); + let layout_settings = self.get_layout_setting(&view_id, layout_ty).await; + assert!(layout_settings.board.is_none()); + + let calendar_setting = layout_settings.calendar.unwrap(); assert_eq!(calendar_setting.layout_ty, expected.layout_ty); assert_eq!( calendar_setting.first_day_of_week, @@ -76,6 +104,20 @@ impl DatabaseLayoutTest { ); assert_eq!(calendar_setting.show_weekends, expected.show_weekends); }, + LayoutScript::UpdateBoardLayoutSetting { new_setting } => { + let changeset = LayoutSettingChangeset { + view_id: self.database_test.view_id.clone(), + layout_type: DatabaseLayout::Board, + board: Some(new_setting), + calendar: None, + }; + self + .database_test + .editor + .set_layout_setting(&self.database_test.view_id, changeset) + .await + .unwrap() + }, LayoutScript::AssertDefaultAllCalendarEvents => { let events = self .database_test diff --git a/frontend/rust-lib/flowy-database2/tests/database/layout_test/test.rs b/frontend/rust-lib/flowy-database2/tests/database/layout_test/test.rs index a380e4b51186..7a8d5749eb27 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/layout_test/test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/layout_test/test.rs @@ -1,9 +1,31 @@ use collab_database::views::DatabaseLayout; +use flowy_database2::services::setting::BoardLayoutSetting; use flowy_database2::services::setting::CalendarLayoutSetting; use crate::database::layout_test::script::DatabaseLayoutTest; use crate::database::layout_test::script::LayoutScript::*; +#[tokio::test] +async fn board_layout_setting_test() { + let mut test = DatabaseLayoutTest::new_board().await; + let default_board_setting = BoardLayoutSetting::new(); + let new_board_setting = BoardLayoutSetting { + hide_ungrouped_column: true, + }; + let scripts = vec![ + AssertBoardLayoutSetting { + expected: default_board_setting, + }, + UpdateBoardLayoutSetting { + new_setting: new_board_setting.clone(), + }, + AssertBoardLayoutSetting { + expected: new_board_setting, + }, + ]; + test.run_scripts(scripts).await; +} + #[tokio::test] async fn calendar_initial_layout_setting_test() { let mut test = DatabaseLayoutTest::new_calendar().await; diff --git a/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs b/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs index a6fd10cce7b9..d26f3bd59f6b 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs @@ -1,6 +1,7 @@ use collab_database::database::{gen_database_id, gen_database_view_id, gen_row_id, DatabaseData}; -use collab_database::views::{DatabaseLayout, DatabaseView}; +use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting, LayoutSettings}; use flowy_database2::services::field_settings::DatabaseFieldSettingsMapBuilder; +use flowy_database2::services::setting::BoardLayoutSetting; use strum::IntoEnumIterator; use flowy_database2::entities::FieldType; @@ -128,6 +129,8 @@ pub fn make_test_board() -> DatabaseData { } } + let board_setting: LayoutSetting = BoardLayoutSetting::new().into(); + let field_settings = DatabaseFieldSettingsMapBuilder::new(fields.clone(), DatabaseLayout::Board).build(); @@ -238,12 +241,15 @@ pub fn make_test_board() -> DatabaseData { rows.push(row); } + let mut layout_settings = LayoutSettings::new(); + layout_settings.insert(DatabaseLayout::Board, board_setting); + let view = DatabaseView { id: gen_database_view_id(), database_id: gen_database_id(), name: "".to_string(), layout: DatabaseLayout::Board, - layout_settings: Default::default(), + layout_settings, filters: vec![], group_settings: vec![], sorts: vec![], From 18bd91936c3a911b3e55091ff9e8515c588c2227 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Sun, 29 Oct 2023 12:51:34 +0800 Subject: [PATCH 06/56] fix: more board fixes (#3821) * fix: null check value * refactor: remove unnecessary blocbuilder * fix: missing checkbox icon at top of board column --- .../board/application/board_bloc.dart | 12 +- .../widgets/board_column_header.dart | 127 +++++++++--------- 2 files changed, 69 insertions(+), 70 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart index bd5decdd8d43..517700b8a228 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart @@ -235,11 +235,13 @@ class BoardBloc extends Bloc { if (isClosed || !layoutSettings.hasBoard()) { return; } - if (layoutSettings.board.hideUngroupedColumn) { - boardController.removeGroup(ungroupedGroup!.fieldId); - } else if (ungroupedGroup != null) { - final newGroup = initializeGroupData(ungroupedGroup!); - boardController.addGroup(newGroup); + if (ungroupedGroup != null) { + if (layoutSettings.board.hideUngroupedColumn) { + boardController.removeGroup(ungroupedGroup!.fieldId); + } else { + final newGroup = initializeGroupData(ungroupedGroup!); + boardController.addGroup(newGroup); + } } add(BoardEvent.didUpdateLayoutSettings(layoutSettings.board)); }, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/widgets/board_column_header.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/widgets/board_column_header.dart index 60f624c05934..2c86a1d4315d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/widgets/board_column_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/widgets/board_column_header.dart @@ -62,77 +62,74 @@ class _BoardColumnHeaderState extends State { Widget build(BuildContext context) { final boardCustomData = widget.groupData.customData as GroupData; - return BlocProvider.value( - value: context.read(), - child: BlocBuilder( - builder: (context, state) { - if (state.isEditingHeader) { - WidgetsBinding.instance.addPostFrameCallback((_) { - _focusNode.requestFocus(); - }); - } - - Widget title = Expanded( - child: FlowyText.medium( - widget.groupData.headerData.groupName, - fontSize: 14, - overflow: TextOverflow.clip, - ), - ); + return BlocBuilder( + builder: (context, state) { + if (state.isEditingHeader) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _focusNode.requestFocus(); + }); + } + + Widget title = Expanded( + child: FlowyText.medium( + widget.groupData.headerData.groupName, + fontSize: 14, + overflow: TextOverflow.clip, + ), + ); - if (!boardCustomData.group.isDefault && - boardCustomData.fieldType.canEditHeader) { - title = Flexible( - fit: FlexFit.tight, - child: FlowyTooltip( - message: LocaleKeys.board_column_renameGroupTooltip.tr(), - child: FlowyHover( - style: HoverStyle( - hoverColor: Colors.transparent, - foregroundColorOnHover: - AFThemeExtension.of(context).textColor, - ), - child: GestureDetector( - onTap: () => context.read().add( - BoardEvent.startEditingHeader( - widget.groupData.id, - ), + if (!boardCustomData.group.isDefault && + boardCustomData.fieldType.canEditHeader) { + title = Flexible( + fit: FlexFit.tight, + child: FlowyTooltip( + message: LocaleKeys.board_column_renameGroupTooltip.tr(), + child: FlowyHover( + style: HoverStyle( + hoverColor: Colors.transparent, + foregroundColorOnHover: + AFThemeExtension.of(context).textColor, + ), + child: GestureDetector( + onTap: () => context.read().add( + BoardEvent.startEditingHeader( + widget.groupData.id, ), - child: FlowyText.medium( - widget.groupData.headerData.groupName, - fontSize: 14, - overflow: TextOverflow.clip, - ), + ), + child: FlowyText.medium( + widget.groupData.headerData.groupName, + fontSize: 14, + overflow: TextOverflow.clip, ), ), ), - ); - } - - if (state.isEditingHeader && - state.editingHeaderId == widget.groupData.id) { - title = _buildTextField(context); - } - - return AppFlowyGroupHeader( - title: title, - icon: _buildHeaderIcon(boardCustomData), - addIcon: SizedBox( - height: 20, - width: 20, - child: FlowySvg( - FlowySvgs.add_s, - color: Theme.of(context).iconTheme.color, - ), ), - onAddButtonClick: () => context - .read() - .add(BoardEvent.createHeaderRow(widget.groupData.id)), - height: 50, - margin: widget.margin ?? EdgeInsets.zero, ); - }, - ), + } + + if (state.isEditingHeader && + state.editingHeaderId == widget.groupData.id) { + title = _buildTextField(context); + } + + return AppFlowyGroupHeader( + title: title, + icon: _buildHeaderIcon(boardCustomData), + addIcon: SizedBox( + height: 20, + width: 20, + child: FlowySvg( + FlowySvgs.add_s, + color: Theme.of(context).iconTheme.color, + ), + ), + onAddButtonClick: () => context + .read() + .add(BoardEvent.createHeaderRow(widget.groupData.id)), + height: 50, + margin: widget.margin ?? EdgeInsets.zero, + ); + }, ); } @@ -224,5 +221,5 @@ Widget? _buildHeaderIcon(GroupData customData) { ); } - return null; + return widget; } From 7f4e7e6aa051cdbdb0f0872f543314dabcb2d39e Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 30 Oct 2023 10:13:47 +0800 Subject: [PATCH 07/56] chore: bump af board ver (#3825) * chore: bump af board ver * fix: select card cell popover opening --- .../card/cells/select_option_card_cell.dart | 45 ++++--------------- frontend/appflowy_flutter/pubspec.lock | 6 +-- frontend/appflowy_flutter/pubspec.yaml | 2 +- 3 files changed, 12 insertions(+), 41 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/select_option_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/select_option_card_cell.dart index 7d9bfded9f95..bf199f5b7dc5 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/select_option_card_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/select_option_card_cell.dart @@ -1,10 +1,7 @@ import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_cell_bloc.dart'; -import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; -import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -35,11 +32,9 @@ class SelectOptionCardCell class _SelectOptionCellState extends State { late SelectOptionCellBloc _cellBloc; - late PopoverController _popover; @override void initState() { - _popover = PopoverController(); final cellController = widget.cellControllerBuilder.build() as SelectOptionCellController; _cellBloc = SelectOptionCellBloc(cellController: cellController) @@ -65,16 +60,14 @@ class _SelectOptionCellState extends State { return custom; } - final children = state.selectedOptions.map( - (option) { - final tag = SelectOptionTag.fromOption( - context: context, - option: option, - onSelected: () => _popover.show(), - ); - return _wrapPopover(tag); - }, - ).toList(); + final children = state.selectedOptions + .map( + (option) => SelectOptionTag.fromOption( + context: context, + option: option, + ), + ) + .toList(); return IntrinsicHeight( child: Padding( @@ -89,28 +82,6 @@ class _SelectOptionCellState extends State { ); } - Widget _wrapPopover(Widget child) { - final constraints = BoxConstraints.loose( - Size( - SelectOptionCellEditor.editorPanelWidth, - 300, - ), - ); - return AppFlowyPopover( - controller: _popover, - constraints: constraints, - direction: PopoverDirection.bottomWithLeftAligned, - popupBuilder: (BuildContext context) { - return SelectOptionCellEditor( - cellController: widget.cellControllerBuilder.build() - as SelectOptionCellController, - ); - }, - onClose: () {}, - child: child, - ); - } - @override Future dispose() async { _cellBloc.close(); diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 9fb28d10c94d..36b53f40a104 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -45,11 +45,11 @@ packages: dependency: "direct main" description: path: "." - ref: a183c57 - resolved-ref: a183c57013071cb3192fcf3c9b1eeb89462179b7 + ref: "6aba8dd" + resolved-ref: "6aba8ddd86839ca09b997cb2457f013236e0c337" url: "https://github.com/AppFlowy-IO/appflowy-board.git" source: git - version: "0.0.9" + version: "0.1.0" appflowy_editor: dependency: "direct main" description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 9c63eb4dd8af..06765d5b580a 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -43,7 +43,7 @@ dependencies: # path: packages/appflowy_board git: url: https://github.com/AppFlowy-IO/appflowy-board.git - ref: a183c57 + ref: 6aba8dd appflowy_editor: ^1.5.0 appflowy_popover: path: packages/appflowy_popover From e08a1a6974d021b3a2bfec031815bc1de510c5f8 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Mon, 30 Oct 2023 12:35:06 +0800 Subject: [PATCH 08/56] feat: enable dispatch event using single thread (#3828) * refactor: lib dispatch * chore: type def * chore: type def * fix: local set spawn * chore: replace tokio spawn * chore: update log * chore: boxed event * chore: tauri lock --- frontend/appflowy_tauri/src-tauri/Cargo.lock | 81 ++---- frontend/rust-lib/Cargo.lock | 131 +++++---- .../collab-integrate/src/collab_builder.rs | 5 +- frontend/rust-lib/dart-ffi/src/lib.rs | 53 ++-- .../rust-lib/event-integration/Cargo.toml | 3 +- .../event-integration/src/event_builder.rs | 7 - .../rust-lib/event-integration/src/lib.rs | 36 ++- .../event-integration/src/user_event.rs | 13 +- .../tests/database/supabase_test/helper.rs | 4 +- .../tests/database/supabase_test/test.rs | 8 +- .../tests/document/af_cloud_test/edit_test.rs | 6 +- .../tests/document/af_cloud_test/util.rs | 2 +- .../tests/document/supabase_test/edit_test.rs | 10 +- .../tests/document/supabase_test/helper.rs | 2 +- .../tests/folder/local_test/script.rs | 2 +- .../folder/local_test/subscription_test.rs | 37 ++- .../tests/folder/local_test/test.rs | 2 +- .../tests/folder/supabase_test/helper.rs | 2 +- .../tests/folder/supabase_test/test.rs | 16 +- .../tests/user/af_cloud_test/auth_test.rs | 4 +- .../tests/user/af_cloud_test/member_test.rs | 8 +- .../tests/user/local_test/auth_test.rs | 8 +- .../user/local_test/user_awareness_test.rs | 4 +- .../user/local_test/user_profile_test.rs | 43 +-- .../user/migration_test/document_test.rs | 3 +- .../tests/user/migration_test/version_test.rs | 3 +- .../tests/user/supabase_test/auth_test.rs | 23 +- .../user/supabase_test/workspace_test.rs | 2 +- .../rust-lib/event-integration/tests/util.rs | 16 +- frontend/rust-lib/flowy-core/Cargo.toml | 3 +- .../rust-lib/flowy-core/src/integrate/log.rs | 1 + frontend/rust-lib/flowy-core/src/lib.rs | 46 ++-- .../flowy-database2/src/event_handler.rs | 4 +- .../rust-lib/flowy-database2/src/manager.rs | 3 +- .../src/services/database/database_editor.rs | 5 +- .../src/services/database_view/view_editor.rs | 7 +- .../src/services/group/configuration.rs | 3 +- .../tests/database/database_editor.rs | 8 +- .../tests/database/filter_test/script.rs | 3 +- .../rust-lib/flowy-document2/src/document.rs | 5 +- .../rust-lib/flowy-folder2/src/manager.rs | 9 +- frontend/rust-lib/flowy-server/Cargo.toml | 1 + .../flowy-server/src/af_cloud/server.rs | 7 +- .../flowy-server/src/supabase/api/database.rs | 5 +- .../flowy-server/src/supabase/api/document.rs | 5 +- .../flowy-server/src/supabase/api/folder.rs | 3 +- .../flowy-server/src/supabase/api/user.rs | 7 +- .../rust-lib/flowy-user/src/event_handler.rs | 2 +- frontend/rust-lib/flowy-user/src/manager.rs | 9 +- .../flowy-user/src/services/user_workspace.rs | 3 +- frontend/rust-lib/lib-dispatch/Cargo.toml | 5 +- .../rust-lib/lib-dispatch/src/byte_trait.rs | 18 +- .../rust-lib/lib-dispatch/src/dispatcher.rs | 257 ++++++++++++++---- .../lib-dispatch/src/errors/errors.rs | 14 +- .../lib-dispatch/src/module/container.rs | 23 +- .../rust-lib/lib-dispatch/src/module/data.rs | 18 +- .../rust-lib/lib-dispatch/src/module/mod.rs | 1 + .../lib-dispatch/src/module/module.rs | 28 +- .../lib-dispatch/src/request/payload.rs | 4 +- .../lib-dispatch/src/request/request.rs | 31 ++- frontend/rust-lib/lib-dispatch/src/runtime.rs | 105 ++++++- .../lib-dispatch/src/service/boxed.rs | 52 ++-- .../lib-dispatch/src/service/handler.rs | 27 +- .../rust-lib/lib-dispatch/tests/api/module.rs | 7 +- frontend/rust-lib/lib-log/Cargo.toml | 8 +- frontend/rust-lib/lib-log/src/layer.rs | 31 +-- frontend/rust-lib/lib-log/src/lib.rs | 42 +-- 67 files changed, 806 insertions(+), 538 deletions(-) diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 545b6438e437..38f1f5f53782 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -129,15 +129,6 @@ dependencies = [ "libc", ] -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - [[package]] name = "anyhow" version = "1.0.75" @@ -454,7 +445,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" dependencies = [ "borsh-derive", - "hashbrown 0.12.3", + "hashbrown 0.13.2", ] [[package]] @@ -1939,6 +1930,7 @@ dependencies = [ "flowy-task", "flowy-user", "flowy-user-deps", + "futures", "futures-core", "lib-dispatch", "lib-infra", @@ -2201,6 +2193,7 @@ dependencies = [ "hex", "hyper", "lazy_static", + "lib-dispatch", "lib-infra", "mime_guess", "parking_lot", @@ -3484,8 +3477,8 @@ dependencies = [ "tracing-appender", "tracing-bunyan-formatter", "tracing-core", - "tracing-log", - "tracing-subscriber 0.2.25", + "tracing-log 0.2.0", + "tracing-subscriber", ] [[package]] @@ -3607,7 +3600,7 @@ dependencies = [ "serde", "serde_json", "tracing", - "tracing-subscriber 0.3.17", + "tracing-subscriber", ] [[package]] @@ -3662,15 +3655,6 @@ dependencies = [ "tendril", ] -[[package]] -name = "matchers" -version = "0.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f099785f7595cc4b4553a174ce30dd7589ef93391ff414dbb67f62392b9e0ce1" -dependencies = [ - "regex-automata", -] - [[package]] name = "matchers" version = "0.1.0" @@ -6653,13 +6637,13 @@ dependencies = [ [[package]] name = "tracing-appender" -version = "0.1.2" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9965507e507f12c8901432a33e31131222abac31edd90cabbcf85cf544b7127a" +checksum = "09d48f71a791638519505cefafe162606f706c25592e4bde4d97600c0195312e" dependencies = [ - "chrono", "crossbeam-channel", - "tracing-subscriber 0.2.25", + "time", + "tracing-subscriber", ] [[package]] @@ -6675,19 +6659,20 @@ dependencies = [ [[package]] name = "tracing-bunyan-formatter" -version = "0.2.6" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c408910c9b7eabc0215fe2b4a89f8ec95581a91cea1f7619f7c78caf14cbc2a1" +checksum = "b5c266b9ac83dedf0e0385ad78514949e6d89491269e7065bee51d2bb8ec7373" dependencies = [ - "chrono", + "ahash 0.8.3", "gethostname", "log", "serde", "serde_json", + "time", "tracing", "tracing-core", - "tracing-log", - "tracing-subscriber 0.2.25", + "tracing-log 0.1.3", + "tracing-subscriber", ] [[package]] @@ -6712,35 +6697,24 @@ dependencies = [ ] [[package]] -name = "tracing-serde" -version = "0.1.3" +name = "tracing-log" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ - "serde", + "log", + "once_cell", "tracing-core", ] [[package]] -name = "tracing-subscriber" -version = "0.2.25" +name = "tracing-serde" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" dependencies = [ - "ansi_term", - "chrono", - "lazy_static", - "matchers 0.0.1", - "regex", "serde", - "serde_json", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", "tracing-core", - "tracing-log", - "tracing-serde", ] [[package]] @@ -6749,16 +6723,19 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" dependencies = [ - "matchers 0.1.0", + "matchers", "nu-ansi-term", "once_cell", "regex", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", - "tracing-log", + "tracing-log 0.1.3", + "tracing-serde", ] [[package]] diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index e94ab9c83239..036d08feffa9 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -115,15 +115,6 @@ dependencies = [ "libc", ] -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - [[package]] name = "anyhow" version = "1.0.75" @@ -461,7 +452,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" dependencies = [ "borsh-derive", - "hashbrown 0.12.3", + "hashbrown 0.13.2", ] [[package]] @@ -979,7 +970,7 @@ dependencies = [ "tonic", "tracing", "tracing-core", - "tracing-subscriber 0.3.17", + "tracing-subscriber", ] [[package]] @@ -1138,7 +1129,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.8.0", + "phf 0.11.2", "smallvec", ] @@ -1759,6 +1750,7 @@ dependencies = [ "flowy-task", "flowy-user", "flowy-user-deps", + "futures", "futures-core", "lib-dispatch", "lib-infra", @@ -1900,7 +1892,7 @@ dependencies = [ "tokio", "tokio-stream", "tracing", - "tracing-subscriber 0.3.17", + "tracing-subscriber", "uuid", ] @@ -2026,6 +2018,7 @@ dependencies = [ "hex", "hyper", "lazy_static", + "lib-dispatch", "lib-infra", "mime_guess", "parking_lot", @@ -2040,7 +2033,7 @@ dependencies = [ "tokio-stream", "tokio-util", "tracing", - "tracing-subscriber 0.3.17", + "tracing-subscriber", "url", "uuid", "yrs", @@ -2242,9 +2235,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" dependencies = [ "futures-core", "futures-sink", @@ -2252,9 +2245,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" [[package]] name = "futures-executor" @@ -2280,15 +2273,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" [[package]] name = "futures-macro" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", @@ -2297,15 +2290,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" [[package]] name = "futures-timer" @@ -2315,9 +2308,9 @@ checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" dependencies = [ "futures-channel", "futures-core", @@ -2984,8 +2977,8 @@ dependencies = [ "tracing-appender", "tracing-bunyan-formatter", "tracing-core", - "tracing-log", - "tracing-subscriber 0.2.25", + "tracing-log 0.2.0", + "tracing-subscriber", ] [[package]] @@ -3115,15 +3108,6 @@ dependencies = [ "tendril", ] -[[package]] -name = "matchers" -version = "0.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f099785f7595cc4b4553a174ce30dd7589ef93391ff414dbb67f62392b9e0ce1" -dependencies = [ - "regex-automata 0.1.10", -] - [[package]] name = "matchers" version = "0.1.0" @@ -3626,7 +3610,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros", + "phf_macros 0.8.0", "phf_shared 0.8.0", "proc-macro-hack", ] @@ -3646,6 +3630,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ + "phf_macros 0.11.2", "phf_shared 0.11.2", ] @@ -3713,6 +3698,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn 2.0.31", +] + [[package]] name = "phf_shared" version = "0.8.0" @@ -5604,13 +5602,13 @@ dependencies = [ [[package]] name = "tracing-appender" -version = "0.1.2" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9965507e507f12c8901432a33e31131222abac31edd90cabbcf85cf544b7127a" +checksum = "09d48f71a791638519505cefafe162606f706c25592e4bde4d97600c0195312e" dependencies = [ - "chrono", "crossbeam-channel", - "tracing-subscriber 0.2.25", + "time", + "tracing-subscriber", ] [[package]] @@ -5626,19 +5624,20 @@ dependencies = [ [[package]] name = "tracing-bunyan-formatter" -version = "0.2.6" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c408910c9b7eabc0215fe2b4a89f8ec95581a91cea1f7619f7c78caf14cbc2a1" +checksum = "b5c266b9ac83dedf0e0385ad78514949e6d89491269e7065bee51d2bb8ec7373" dependencies = [ - "chrono", + "ahash 0.8.3", "gethostname", "log", "serde", "serde_json", + "time", "tracing", "tracing-core", - "tracing-log", - "tracing-subscriber 0.2.25", + "tracing-log 0.1.3", + "tracing-subscriber", ] [[package]] @@ -5663,35 +5662,24 @@ dependencies = [ ] [[package]] -name = "tracing-serde" -version = "0.1.3" +name = "tracing-log" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ - "serde", + "log", + "once_cell", "tracing-core", ] [[package]] -name = "tracing-subscriber" -version = "0.2.25" +name = "tracing-serde" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" dependencies = [ - "ansi_term", - "chrono", - "lazy_static", - "matchers 0.0.1", - "regex", "serde", - "serde_json", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", "tracing-core", - "tracing-log", - "tracing-serde", ] [[package]] @@ -5700,16 +5688,19 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" dependencies = [ - "matchers 0.1.0", + "matchers", "nu-ansi-term", "once_cell", "regex", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", - "tracing-log", + "tracing-log 0.1.3", + "tracing-serde", ] [[package]] diff --git a/frontend/rust-lib/collab-integrate/src/collab_builder.rs b/frontend/rust-lib/collab-integrate/src/collab_builder.rs index 42d8eee6256a..9a0fedfdd9c6 100644 --- a/frontend/rust-lib/collab-integrate/src/collab_builder.rs +++ b/frontend/rust-lib/collab-integrate/src/collab_builder.rs @@ -41,10 +41,7 @@ pub enum CollabPluginContext { pub trait CollabStorageProvider: Send + Sync + 'static { fn storage_source(&self) -> CollabSource; - fn get_plugins( - &self, - context: CollabPluginContext, - ) -> Fut>>; + fn get_plugins(&self, context: CollabPluginContext) -> Fut>>; fn is_sync_enabled(&self) -> bool; } diff --git a/frontend/rust-lib/dart-ffi/src/lib.rs b/frontend/rust-lib/dart-ffi/src/lib.rs index 256ad1d8f216..53e2795f8396 100644 --- a/frontend/rust-lib/dart-ffi/src/lib.rs +++ b/frontend/rust-lib/dart-ffi/src/lib.rs @@ -1,9 +1,12 @@ #![allow(clippy::not_unsafe_ptr_arg_deref)] +use std::sync::Arc; use std::{ffi::CStr, os::raw::c_char}; use lazy_static::lazy_static; -use parking_lot::RwLock; +use log::error; +use parking_lot::Mutex; +use tracing::trace; use flowy_core::*; use flowy_notification::{register_notification_sender, unregister_all_notification_sender}; @@ -25,9 +28,26 @@ mod protobuf; mod util; lazy_static! { - static ref APPFLOWY_CORE: RwLock> = RwLock::new(None); + static ref APPFLOWY_CORE: MutexAppFlowyCore = MutexAppFlowyCore::new(); } +struct MutexAppFlowyCore(Arc>>); + +impl MutexAppFlowyCore { + fn new() -> Self { + Self(Arc::new(Mutex::new(None))) + } + + fn dispatcher(&self) -> Option> { + let binding = self.0.lock(); + let core = binding.as_ref(); + core.map(|core| core.event_dispatcher.clone()) + } +} + +unsafe impl Sync for MutexAppFlowyCore {} +unsafe impl Send for MutexAppFlowyCore {} + #[no_mangle] pub extern "C" fn init_sdk(path: *mut c_char) -> i64 { let c_str: &CStr = unsafe { CStr::from_ptr(path) }; @@ -36,32 +56,33 @@ pub extern "C" fn init_sdk(path: *mut c_char) -> i64 { let log_crates = vec!["flowy-ffi".to_string()]; let config = AppFlowyCoreConfig::new(path, DEFAULT_NAME.to_string()).log_filter("info", log_crates); - *APPFLOWY_CORE.write() = Some(AppFlowyCore::new(config)); + *APPFLOWY_CORE.0.lock() = Some(AppFlowyCore::new(config)); 0 } #[no_mangle] +#[allow(clippy::let_underscore_future)] pub extern "C" fn async_event(port: i64, input: *const u8, len: usize) { let request: AFPluginRequest = FFIRequest::from_u8_pointer(input, len).into(); - log::trace!( + trace!( "[FFI]: {} Async Event: {:?} with {} port", &request.id, &request.event, port ); - let dispatcher = match APPFLOWY_CORE.read().as_ref() { + let dispatcher = match APPFLOWY_CORE.dispatcher() { None => { - log::error!("sdk not init yet."); + error!("sdk not init yet."); return; }, - Some(e) => e.event_dispatcher.clone(), + Some(dispatcher) => dispatcher, }; - AFPluginDispatcher::async_send_with_callback( + AFPluginDispatcher::boxed_async_send_with_callback( dispatcher, request, move |resp: AFPluginEventResponse| { - log::trace!("[FFI]: Post data to dart through {} port", port); + trace!("[FFI]: Post data to dart through {} port", port); Box::pin(post_to_flutter(resp, port)) }, ); @@ -70,14 +91,14 @@ pub extern "C" fn async_event(port: i64, input: *const u8, len: usize) { #[no_mangle] pub extern "C" fn sync_event(input: *const u8, len: usize) -> *const u8 { let request: AFPluginRequest = FFIRequest::from_u8_pointer(input, len).into(); - log::trace!("[FFI]: {} Sync Event: {:?}", &request.id, &request.event,); + trace!("[FFI]: {} Sync Event: {:?}", &request.id, &request.event,); - let dispatcher = match APPFLOWY_CORE.read().as_ref() { + let dispatcher = match APPFLOWY_CORE.dispatcher() { None => { - log::error!("sdk not init yet."); + error!("sdk not init yet."); return forget_rust(Vec::default()); }, - Some(e) => e.event_dispatcher.clone(), + Some(dispatcher) => dispatcher, }; let _response = AFPluginDispatcher::sync_send(dispatcher, request); @@ -110,13 +131,13 @@ async fn post_to_flutter(response: AFPluginEventResponse, port: i64) { .await { Ok(_success) => { - log::trace!("[FFI]: Post data to dart success"); + trace!("[FFI]: Post data to dart success"); }, Err(e) => { if let Some(msg) = e.downcast_ref::<&str>() { - log::error!("[FFI]: {:?}", msg); + error!("[FFI]: {:?}", msg); } else { - log::error!("[FFI]: allo_isolate post panic"); + error!("[FFI]: allo_isolate post panic"); } }, } diff --git a/frontend/rust-lib/event-integration/Cargo.toml b/frontend/rust-lib/event-integration/Cargo.toml index 3c35b0ff82aa..f571986ffc93 100644 --- a/frontend/rust-lib/event-integration/Cargo.toml +++ b/frontend/rust-lib/event-integration/Cargo.toml @@ -53,4 +53,5 @@ zip = "0.6.6" [features] default = ["supabase_cloud_test"] dart = ["flowy-core/dart"] -supabase_cloud_test = [] \ No newline at end of file +supabase_cloud_test = [] +single_thread = ["flowy-core/single_thread"] \ No newline at end of file diff --git a/frontend/rust-lib/event-integration/src/event_builder.rs b/frontend/rust-lib/event-integration/src/event_builder.rs index a6c0209622f2..afa4590add7f 100644 --- a/frontend/rust-lib/event-integration/src/event_builder.rs +++ b/frontend/rust-lib/event-integration/src/event_builder.rs @@ -48,13 +48,6 @@ impl EventBuilder { self } - pub fn sync_send(mut self) -> Self { - let request = self.get_request(); - let resp = AFPluginDispatcher::sync_send(self.dispatch(), request); - self.context.response = Some(resp); - self - } - pub async fn async_send(mut self) -> Self { let request = self.get_request(); let resp = AFPluginDispatcher::async_send(self.dispatch(), request).await; diff --git a/frontend/rust-lib/event-integration/src/lib.rs b/frontend/rust-lib/event-integration/src/lib.rs index f99304eba93e..1a9d9cb7a14e 100644 --- a/frontend/rust-lib/event-integration/src/lib.rs +++ b/frontend/rust-lib/event-integration/src/lib.rs @@ -27,30 +27,23 @@ pub struct EventIntegrationTest { pub notification_sender: TestNotificationSender, } -impl Default for EventIntegrationTest { - fn default() -> Self { +impl EventIntegrationTest { + pub async fn new() -> Self { let temp_dir = temp_dir().join(nanoid!(6)); std::fs::create_dir_all(&temp_dir).unwrap(); - Self::new_with_user_data_path(temp_dir, nanoid!(6)) - } -} - -impl EventIntegrationTest { - pub fn new() -> Self { - Self::default() + Self::new_with_user_data_path(temp_dir, nanoid!(6)).await } - pub fn new_with_user_data_path(path: PathBuf, name: String) -> Self { + pub async fn new_with_user_data_path(path: PathBuf, name: String) -> Self { let config = AppFlowyCoreConfig::new(path.to_str().unwrap(), name).log_filter( "trace", vec![ "flowy_test".to_string(), - // "lib_dispatch".to_string() + "tokio".to_string(), + "lib_dispatch".to_string(), ], ); - let inner = std::thread::spawn(|| AppFlowyCore::new(config)) - .join() - .unwrap(); + let inner = init_core(config).await; let notification_sender = TestNotificationSender::new(); let auth_type = Arc::new(RwLock::new(AuthTypePB::Local)); register_notification_sender(notification_sender.clone()); @@ -64,6 +57,21 @@ impl EventIntegrationTest { } } +#[cfg(feature = "single_thread")] +async fn init_core(config: AppFlowyCoreConfig) -> AppFlowyCore { + // let runtime = tokio::runtime::Runtime::new().unwrap(); + // let local_set = tokio::task::LocalSet::new(); + // runtime.block_on(AppFlowyCore::new(config)) + AppFlowyCore::new(config).await +} + +#[cfg(not(feature = "single_thread"))] +async fn init_core(config: AppFlowyCoreConfig) -> AppFlowyCore { + std::thread::spawn(|| AppFlowyCore::new(config)) + .join() + .unwrap() +} + impl std::ops::Deref for EventIntegrationTest { type Target = AppFlowyCore; diff --git a/frontend/rust-lib/event-integration/src/user_event.rs b/frontend/rust-lib/event-integration/src/user_event.rs index 9d03b8f1e8f4..8a180c76e3a0 100644 --- a/frontend/rust-lib/event-integration/src/user_event.rs +++ b/frontend/rust-lib/event-integration/src/user_event.rs @@ -6,6 +6,7 @@ use bytes::Bytes; use nanoid::nanoid; use protobuf::ProtobufError; use tokio::sync::broadcast::{channel, Sender}; +use tracing::error; use uuid::Uuid; use flowy_notification::entities::SubscribeObject; @@ -17,7 +18,7 @@ use flowy_user::entities::{ }; use flowy_user::errors::{FlowyError, FlowyResult}; use flowy_user::event_map::UserEvent::*; -use lib_dispatch::prelude::{AFPluginDispatcher, AFPluginRequest, ToBytes}; +use lib_dispatch::prelude::{af_spawn, AFPluginDispatcher, AFPluginRequest, ToBytes}; use crate::event_builder::EventBuilder; use crate::EventIntegrationTest; @@ -44,7 +45,7 @@ impl EventIntegrationTest { } pub async fn new_with_guest_user() -> Self { - let test = Self::default(); + let test = Self::new().await; test.sign_up_as_guest().await; test } @@ -213,7 +214,7 @@ impl TestNotificationSender { let (tx, rx) = tokio::sync::mpsc::channel::(10); let mut receiver = self.sender.subscribe(); let ty = ty.into(); - tokio::spawn(async move { + af_spawn(async move { // DatabaseNotification::DidUpdateDatabaseSnapshotState while let Ok(value) = receiver.recv().await { if value.id == id && value.ty == ty { @@ -245,7 +246,7 @@ impl TestNotificationSender { let id = id.to_string(); let (tx, rx) = tokio::sync::mpsc::channel::(10); let mut receiver = self.sender.subscribe(); - tokio::spawn(async move { + af_spawn(async move { while let Ok(value) = receiver.recv().await { if value.id == id { if let Some(payload) = value.payload { @@ -263,7 +264,9 @@ impl TestNotificationSender { } impl NotificationSender for TestNotificationSender { fn send_subject(&self, subject: SubscribeObject) -> Result<(), String> { - let _ = self.sender.send(subject); + if let Err(err) = self.sender.send(subject) { + error!("Failed to send notification: {:?}", err); + } Ok(()) } } diff --git a/frontend/rust-lib/event-integration/tests/database/supabase_test/helper.rs b/frontend/rust-lib/event-integration/tests/database/supabase_test/helper.rs index c146644da2c8..5599f78cdec2 100644 --- a/frontend/rust-lib/event-integration/tests/database/supabase_test/helper.rs +++ b/frontend/rust-lib/event-integration/tests/database/supabase_test/helper.rs @@ -22,13 +22,13 @@ pub struct FlowySupabaseDatabaseTest { impl FlowySupabaseDatabaseTest { #[allow(dead_code)] pub async fn new_with_user(uuid: String) -> Option { - let inner = FlowySupabaseTest::new()?; + let inner = FlowySupabaseTest::new().await?; inner.supabase_sign_up_with_uuid(&uuid, None).await.unwrap(); Some(Self { uuid, inner }) } pub async fn new_with_new_user() -> Option { - let inner = FlowySupabaseTest::new()?; + let inner = FlowySupabaseTest::new().await?; let uuid = uuid::Uuid::new_v4().to_string(); let _ = inner.supabase_sign_up_with_uuid(&uuid, None).await.unwrap(); Some(Self { uuid, inner }) diff --git a/frontend/rust-lib/event-integration/tests/database/supabase_test/test.rs b/frontend/rust-lib/event-integration/tests/database/supabase_test/test.rs index f5867c34f7f2..6877e511c228 100644 --- a/frontend/rust-lib/event-integration/tests/database/supabase_test/test.rs +++ b/frontend/rust-lib/event-integration/tests/database/supabase_test/test.rs @@ -14,11 +14,11 @@ use crate::util::receive_with_timeout; async fn supabase_initial_database_snapshot_test() { if let Some(test) = FlowySupabaseDatabaseTest::new_with_new_user().await { let (view, database) = test.create_database().await; - let mut rx = test + let rx = test .notification_sender .subscribe::(&database.id, DidUpdateDatabaseSnapshotState); - receive_with_timeout(&mut rx, Duration::from_secs(30)) + receive_with_timeout(rx, Duration::from_secs(30)) .await .unwrap(); @@ -51,10 +51,10 @@ async fn supabase_edit_database_test() { .await; // wait all updates are send to the remote - let mut rx = test + let rx = test .notification_sender .subscribe_with_condition::(&database.id, |pb| pb.is_finish); - receive_with_timeout(&mut rx, Duration::from_secs(30)) + receive_with_timeout(rx, Duration::from_secs(30)) .await .unwrap(); diff --git a/frontend/rust-lib/event-integration/tests/document/af_cloud_test/edit_test.rs b/frontend/rust-lib/event-integration/tests/document/af_cloud_test/edit_test.rs index d261cc26f07b..e5a08c8506e9 100644 --- a/frontend/rust-lib/event-integration/tests/document/af_cloud_test/edit_test.rs +++ b/frontend/rust-lib/event-integration/tests/document/af_cloud_test/edit_test.rs @@ -12,17 +12,17 @@ async fn af_cloud_edit_document_test() { let document_id = test.create_document().await; let cloned_test = test.clone(); let cloned_document_id = document_id.clone(); - tokio::spawn(async move { + test.inner.dispatcher().spawn(async move { cloned_test .insert_document_text(&cloned_document_id, "hello world", 0) .await; }); // wait all update are send to the remote - let mut rx = test + let rx = test .notification_sender .subscribe_with_condition::(&document_id, |pb| pb.is_finish); - receive_with_timeout(&mut rx, Duration::from_secs(15)) + receive_with_timeout(rx, Duration::from_secs(15)) .await .unwrap(); diff --git a/frontend/rust-lib/event-integration/tests/document/af_cloud_test/util.rs b/frontend/rust-lib/event-integration/tests/document/af_cloud_test/util.rs index 4abf56fa070c..eef1023f48b0 100644 --- a/frontend/rust-lib/event-integration/tests/document/af_cloud_test/util.rs +++ b/frontend/rust-lib/event-integration/tests/document/af_cloud_test/util.rs @@ -8,7 +8,7 @@ pub struct AFCloudDocumentTest { impl AFCloudDocumentTest { pub async fn new() -> Option { - let inner = AFCloudTest::new()?; + let inner = AFCloudTest::new().await?; let email = generate_test_email(); let _ = inner.af_cloud_sign_in_with_email(&email).await.unwrap(); Some(Self { inner }) diff --git a/frontend/rust-lib/event-integration/tests/document/supabase_test/edit_test.rs b/frontend/rust-lib/event-integration/tests/document/supabase_test/edit_test.rs index a4ad36d350f2..01f3ed5c215b 100644 --- a/frontend/rust-lib/event-integration/tests/document/supabase_test/edit_test.rs +++ b/frontend/rust-lib/event-integration/tests/document/supabase_test/edit_test.rs @@ -14,17 +14,17 @@ async fn supabase_document_edit_sync_test() { let cloned_test = test.clone(); let cloned_document_id = document_id.clone(); - tokio::spawn(async move { + test.inner.dispatcher().spawn(async move { cloned_test .insert_document_text(&cloned_document_id, "hello world", 0) .await; }); // wait all update are send to the remote - let mut rx = test + let rx = test .notification_sender .subscribe_with_condition::(&document_id, |pb| pb.is_finish); - receive_with_timeout(&mut rx, Duration::from_secs(30)) + receive_with_timeout(rx, Duration::from_secs(30)) .await .unwrap(); @@ -47,10 +47,10 @@ async fn supabase_document_edit_sync_test2() { } // wait all update are send to the remote - let mut rx = test + let rx = test .notification_sender .subscribe_with_condition::(&document_id, |pb| pb.is_finish); - receive_with_timeout(&mut rx, Duration::from_secs(30)) + receive_with_timeout(rx, Duration::from_secs(30)) .await .unwrap(); diff --git a/frontend/rust-lib/event-integration/tests/document/supabase_test/helper.rs b/frontend/rust-lib/event-integration/tests/document/supabase_test/helper.rs index 7244a33a34f4..5ddd359a3c4a 100644 --- a/frontend/rust-lib/event-integration/tests/document/supabase_test/helper.rs +++ b/frontend/rust-lib/event-integration/tests/document/supabase_test/helper.rs @@ -13,7 +13,7 @@ pub struct FlowySupabaseDocumentTest { impl FlowySupabaseDocumentTest { pub async fn new() -> Option { - let inner = FlowySupabaseTest::new()?; + let inner = FlowySupabaseTest::new().await?; let uuid = uuid::Uuid::new_v4().to_string(); let _ = inner.supabase_sign_up_with_uuid(&uuid, None).await; Some(Self { inner }) diff --git a/frontend/rust-lib/event-integration/tests/folder/local_test/script.rs b/frontend/rust-lib/event-integration/tests/folder/local_test/script.rs index 09a11a94f865..82d0db03d0d4 100644 --- a/frontend/rust-lib/event-integration/tests/folder/local_test/script.rs +++ b/frontend/rust-lib/event-integration/tests/folder/local_test/script.rs @@ -75,7 +75,7 @@ pub struct FolderTest { impl FolderTest { pub async fn new() -> Self { - let sdk = EventIntegrationTest::new(); + let sdk = EventIntegrationTest::new().await; let _ = sdk.init_anon_user().await; let workspace = create_workspace(&sdk, "FolderWorkspace", "Folder test workspace").await; let parent_view = create_app(&sdk, &workspace.id, "Folder App", "Folder test app").await; diff --git a/frontend/rust-lib/event-integration/tests/folder/local_test/subscription_test.rs b/frontend/rust-lib/event-integration/tests/folder/local_test/subscription_test.rs index 2df246b212ac..0ad2deaa0f1f 100644 --- a/frontend/rust-lib/event-integration/tests/folder/local_test/subscription_test.rs +++ b/frontend/rust-lib/event-integration/tests/folder/local_test/subscription_test.rs @@ -18,19 +18,19 @@ use crate::util::receive_with_timeout; async fn create_child_view_in_workspace_subscription_test() { let test = EventIntegrationTest::new_with_guest_user().await; let workspace = test.get_current_workspace().await.workspace; - let mut rx = test + let rx = test .notification_sender .subscribe::(&workspace.id, FolderNotification::DidUpdateWorkspaceViews); let cloned_test = test.clone(); let cloned_workspace_id = workspace.id.clone(); - tokio::spawn(async move { + test.inner.dispatcher().spawn(async move { cloned_test .create_view(&cloned_workspace_id, "workspace child view".to_string()) .await; }); - let views = receive_with_timeout(&mut rx, Duration::from_secs(30)) + let views = receive_with_timeout(rx, Duration::from_secs(30)) .await .unwrap() .items; @@ -43,14 +43,14 @@ async fn create_child_view_in_view_subscription_test() { let test = EventIntegrationTest::new_with_guest_user().await; let mut workspace = test.get_current_workspace().await.workspace; let workspace_child_view = workspace.views.pop().unwrap(); - let mut rx = test.notification_sender.subscribe::( + let rx = test.notification_sender.subscribe::( &workspace_child_view.id, FolderNotification::DidUpdateChildViews, ); let cloned_test = test.clone(); let child_view_id = workspace_child_view.id.clone(); - tokio::spawn(async move { + test.inner.dispatcher().spawn(async move { cloned_test .create_view( &child_view_id, @@ -59,7 +59,7 @@ async fn create_child_view_in_view_subscription_test() { .await; }); - let update = receive_with_timeout(&mut rx, Duration::from_secs(30)) + let update = receive_with_timeout(rx, Duration::from_secs(30)) .await .unwrap(); @@ -74,20 +74,29 @@ async fn create_child_view_in_view_subscription_test() { async fn delete_view_subscription_test() { let test = EventIntegrationTest::new_with_guest_user().await; let workspace = test.get_current_workspace().await.workspace; - let mut rx = test + let rx = test .notification_sender .subscribe::(&workspace.id, FolderNotification::DidUpdateChildViews); let cloned_test = test.clone(); let delete_view_id = workspace.views.first().unwrap().id.clone(); let cloned_delete_view_id = delete_view_id.clone(); - tokio::spawn(async move { - cloned_test.delete_view(&cloned_delete_view_id).await; - }); + test + .inner + .dispatcher() + .spawn(async move { + cloned_test.delete_view(&cloned_delete_view_id).await; + }) + .await + .unwrap(); - let update = receive_with_timeout(&mut rx, Duration::from_secs(30)) + let update = test + .inner + .dispatcher() + .run_until(receive_with_timeout(rx, Duration::from_secs(30))) .await .unwrap(); + assert_eq!(update.delete_child_views.len(), 1); assert_eq!(update.delete_child_views[0], delete_view_id); } @@ -96,7 +105,7 @@ async fn delete_view_subscription_test() { async fn update_view_subscription_test() { let test = EventIntegrationTest::new_with_guest_user().await; let mut workspace = test.get_current_workspace().await.workspace; - let mut rx = test + let rx = test .notification_sender .subscribe::(&workspace.id, FolderNotification::DidUpdateChildViews); @@ -105,7 +114,7 @@ async fn update_view_subscription_test() { assert!(!view.is_favorite); let update_view_id = view.id.clone(); - tokio::spawn(async move { + test.inner.dispatcher().spawn(async move { cloned_test .update_view(UpdateViewPayloadPB { view_id: update_view_id, @@ -116,7 +125,7 @@ async fn update_view_subscription_test() { .await; }); - let update = receive_with_timeout(&mut rx, Duration::from_secs(30)) + let update = receive_with_timeout(rx, Duration::from_secs(30)) .await .unwrap(); assert_eq!(update.update_child_views.len(), 1); diff --git a/frontend/rust-lib/event-integration/tests/folder/local_test/test.rs b/frontend/rust-lib/event-integration/tests/folder/local_test/test.rs index 87cc6b82d9e6..82215040d620 100644 --- a/frontend/rust-lib/event-integration/tests/folder/local_test/test.rs +++ b/frontend/rust-lib/event-integration/tests/folder/local_test/test.rs @@ -466,7 +466,7 @@ async fn move_view_event_after_delete_view_test2() { #[tokio::test] async fn create_parent_view_with_invalid_name() { for (name, code) in invalid_workspace_name_test_case() { - let sdk = EventIntegrationTest::new(); + let sdk = EventIntegrationTest::new().await; let request = CreateWorkspacePayloadPB { name, desc: "".to_owned(), diff --git a/frontend/rust-lib/event-integration/tests/folder/supabase_test/helper.rs b/frontend/rust-lib/event-integration/tests/folder/supabase_test/helper.rs index e86257c32d0c..eac5cb418a3f 100644 --- a/frontend/rust-lib/event-integration/tests/folder/supabase_test/helper.rs +++ b/frontend/rust-lib/event-integration/tests/folder/supabase_test/helper.rs @@ -19,7 +19,7 @@ pub struct FlowySupabaseFolderTest { impl FlowySupabaseFolderTest { pub async fn new() -> Option { - let inner = FlowySupabaseTest::new()?; + let inner = FlowySupabaseTest::new().await?; let uuid = uuid::Uuid::new_v4().to_string(); let _ = inner.supabase_sign_up_with_uuid(&uuid, None).await; Some(Self { inner }) diff --git a/frontend/rust-lib/event-integration/tests/folder/supabase_test/test.rs b/frontend/rust-lib/event-integration/tests/folder/supabase_test/test.rs index fb4ea0f361c3..d04937d70dad 100644 --- a/frontend/rust-lib/event-integration/tests/folder/supabase_test/test.rs +++ b/frontend/rust-lib/event-integration/tests/folder/supabase_test/test.rs @@ -34,11 +34,11 @@ async fn supabase_decrypt_folder_data_test() { .create_view(&workspace_id, "encrypt view".to_string()) .await; - let mut rx = test + let rx = test .notification_sender .subscribe_with_condition::(&workspace_id, |pb| pb.is_finish); - receive_with_timeout(&mut rx, Duration::from_secs(10)) + receive_with_timeout(rx, Duration::from_secs(10)) .await .unwrap(); let folder_data = get_folder_data_from_server(&workspace_id, secret) @@ -59,10 +59,10 @@ async fn supabase_decrypt_with_invalid_secret_folder_data_test() { test .create_view(&workspace_id, "encrypt view".to_string()) .await; - let mut rx = test + let rx = test .notification_sender .subscribe_with_condition::(&workspace_id, |pb| pb.is_finish); - receive_with_timeout(&mut rx, Duration::from_secs(10)) + receive_with_timeout(rx, Duration::from_secs(10)) .await .unwrap(); @@ -75,10 +75,10 @@ async fn supabase_decrypt_with_invalid_secret_folder_data_test() { async fn supabase_folder_snapshot_test() { if let Some(test) = FlowySupabaseFolderTest::new().await { let workspace_id = test.get_current_workspace().await.workspace.id; - let mut rx = test + let rx = test .notification_sender .subscribe::(&workspace_id, DidUpdateFolderSnapshotState); - receive_with_timeout(&mut rx, Duration::from_secs(10)) + receive_with_timeout(rx, Duration::from_secs(10)) .await .unwrap(); @@ -104,11 +104,11 @@ async fn supabase_initial_folder_snapshot_test2() { .create_view(&workspace_id, "supabase test view3".to_string()) .await; - let mut rx = test + let rx = test .notification_sender .subscribe_with_condition::(&workspace_id, |pb| pb.is_finish); - receive_with_timeout(&mut rx, Duration::from_secs(10)) + receive_with_timeout(rx, Duration::from_secs(10)) .await .unwrap(); diff --git a/frontend/rust-lib/event-integration/tests/user/af_cloud_test/auth_test.rs b/frontend/rust-lib/event-integration/tests/user/af_cloud_test/auth_test.rs index a37eaa1276a9..04c4666e4f00 100644 --- a/frontend/rust-lib/event-integration/tests/user/af_cloud_test/auth_test.rs +++ b/frontend/rust-lib/event-integration/tests/user/af_cloud_test/auth_test.rs @@ -6,7 +6,7 @@ use crate::util::{generate_test_email, get_af_cloud_config}; #[tokio::test] async fn af_cloud_sign_up_test() { if get_af_cloud_config().is_some() { - let test = EventIntegrationTest::new(); + let test = EventIntegrationTest::new().await; let email = generate_test_email(); let user = test.af_cloud_sign_in_with_email(&email).await.unwrap(); assert_eq!(user.email, email); @@ -16,7 +16,7 @@ async fn af_cloud_sign_up_test() { #[tokio::test] async fn af_cloud_update_user_metadata() { if get_af_cloud_config().is_some() { - let test = EventIntegrationTest::new(); + let test = EventIntegrationTest::new().await; let user = test.af_cloud_sign_up().await; let old_profile = test.get_user_profile().await.unwrap(); diff --git a/frontend/rust-lib/event-integration/tests/user/af_cloud_test/member_test.rs b/frontend/rust-lib/event-integration/tests/user/af_cloud_test/member_test.rs index 6020c8a56ae8..577323eae34c 100644 --- a/frontend/rust-lib/event-integration/tests/user/af_cloud_test/member_test.rs +++ b/frontend/rust-lib/event-integration/tests/user/af_cloud_test/member_test.rs @@ -5,10 +5,10 @@ use crate::util::get_af_cloud_config; #[tokio::test] async fn af_cloud_add_workspace_member_test() { if get_af_cloud_config().is_some() { - let test_1 = EventIntegrationTest::new(); + let test_1 = EventIntegrationTest::new().await; let user_1 = test_1.af_cloud_sign_up().await; - let test_2 = EventIntegrationTest::new(); + let test_2 = EventIntegrationTest::new().await; let user_2 = test_2.af_cloud_sign_up().await; let members = test_1.get_workspace_members(&user_1.workspace_id).await; @@ -29,10 +29,10 @@ async fn af_cloud_add_workspace_member_test() { #[tokio::test] async fn af_cloud_delete_workspace_member_test() { if get_af_cloud_config().is_some() { - let test_1 = EventIntegrationTest::new(); + let test_1 = EventIntegrationTest::new().await; let user_1 = test_1.af_cloud_sign_up().await; - let test_2 = EventIntegrationTest::new(); + let test_2 = EventIntegrationTest::new().await; let user_2 = test_2.af_cloud_sign_up().await; test_1 diff --git a/frontend/rust-lib/event-integration/tests/user/local_test/auth_test.rs b/frontend/rust-lib/event-integration/tests/user/local_test/auth_test.rs index c5ee9c556061..9aa35dd344b0 100644 --- a/frontend/rust-lib/event-integration/tests/user/local_test/auth_test.rs +++ b/frontend/rust-lib/event-integration/tests/user/local_test/auth_test.rs @@ -9,7 +9,7 @@ use crate::user::local_test::helper::*; #[tokio::test] async fn sign_up_with_invalid_email() { for email in invalid_email_test_case() { - let sdk = EventIntegrationTest::new(); + let sdk = EventIntegrationTest::new().await; let request = SignUpPayloadPB { email: email.to_string(), name: valid_name(), @@ -33,7 +33,7 @@ async fn sign_up_with_invalid_email() { } #[tokio::test] async fn sign_up_with_long_password() { - let sdk = EventIntegrationTest::new(); + let sdk = EventIntegrationTest::new().await; let request = SignUpPayloadPB { email: unique_email(), name: valid_name(), @@ -58,7 +58,7 @@ async fn sign_up_with_long_password() { #[tokio::test] async fn sign_in_with_invalid_email() { for email in invalid_email_test_case() { - let sdk = EventIntegrationTest::new(); + let sdk = EventIntegrationTest::new().await; let request = SignInPayloadPB { email: email.to_string(), password: login_password(), @@ -84,7 +84,7 @@ async fn sign_in_with_invalid_email() { #[tokio::test] async fn sign_in_with_invalid_password() { for password in invalid_password_test_case() { - let sdk = EventIntegrationTest::new(); + let sdk = EventIntegrationTest::new().await; let request = SignInPayloadPB { email: unique_email(), diff --git a/frontend/rust-lib/event-integration/tests/user/local_test/user_awareness_test.rs b/frontend/rust-lib/event-integration/tests/user/local_test/user_awareness_test.rs index 68bae5c7a4b7..8e1223f56642 100644 --- a/frontend/rust-lib/event-integration/tests/user/local_test/user_awareness_test.rs +++ b/frontend/rust-lib/event-integration/tests/user/local_test/user_awareness_test.rs @@ -6,8 +6,8 @@ use flowy_user::entities::{ReminderPB, RepeatedReminderPB}; use flowy_user::event_map::UserEvent::*; #[tokio::test] -async fn user_update_with_name() { - let sdk = EventIntegrationTest::new(); +async fn user_update_with_reminder() { + let sdk = EventIntegrationTest::new().await; let _ = sdk.sign_up_as_guest().await; let mut meta = HashMap::new(); meta.insert("object_id".to_string(), "".to_string()); diff --git a/frontend/rust-lib/event-integration/tests/user/local_test/user_profile_test.rs b/frontend/rust-lib/event-integration/tests/user/local_test/user_profile_test.rs index 60f5cc84ccf2..7418b267afd4 100644 --- a/frontend/rust-lib/event-integration/tests/user/local_test/user_profile_test.rs +++ b/frontend/rust-lib/event-integration/tests/user/local_test/user_profile_test.rs @@ -10,7 +10,7 @@ use crate::user::local_test::helper::*; #[tokio::test] async fn user_profile_get_failed() { - let sdk = EventIntegrationTest::new(); + let sdk = EventIntegrationTest::new().await; let result = EventBuilder::new(sdk) .event(GetUserProfile) .async_send() @@ -21,11 +21,12 @@ async fn user_profile_get_failed() { #[tokio::test] async fn anon_user_profile_get() { - let test = EventIntegrationTest::new(); + let test = EventIntegrationTest::new().await; let user_profile = test.init_anon_user().await; let user = EventBuilder::new(test.clone()) .event(GetUserProfile) - .sync_send() + .async_send() + .await .parse::(); assert_eq!(user_profile.id, user.id); assert_eq!(user_profile.openai_key, user.openai_key); @@ -36,18 +37,20 @@ async fn anon_user_profile_get() { #[tokio::test] async fn user_update_with_name() { - let sdk = EventIntegrationTest::new(); + let sdk = EventIntegrationTest::new().await; let user = sdk.init_anon_user().await; let new_name = "hello_world".to_owned(); let request = UpdateUserProfilePayloadPB::new(user.id).name(&new_name); let _ = EventBuilder::new(sdk.clone()) .event(UpdateUserProfile) .payload(request) - .sync_send(); + .async_send() + .await; let user_profile = EventBuilder::new(sdk.clone()) .event(GetUserProfile) - .sync_send() + .async_send() + .await .parse::(); assert_eq!(user_profile.name, new_name,); @@ -55,7 +58,7 @@ async fn user_update_with_name() { #[tokio::test] async fn user_update_with_ai_key() { - let sdk = EventIntegrationTest::new(); + let sdk = EventIntegrationTest::new().await; let user = sdk.init_anon_user().await; let openai_key = "openai_key".to_owned(); let stability_ai_key = "stability_ai_key".to_owned(); @@ -65,11 +68,13 @@ async fn user_update_with_ai_key() { let _ = EventBuilder::new(sdk.clone()) .event(UpdateUserProfile) .payload(request) - .sync_send(); + .async_send() + .await; let user_profile = EventBuilder::new(sdk.clone()) .event(GetUserProfile) - .sync_send() + .async_send() + .await .parse::(); assert_eq!(user_profile.openai_key, openai_key,); @@ -78,17 +83,19 @@ async fn user_update_with_ai_key() { #[tokio::test] async fn anon_user_update_with_email() { - let sdk = EventIntegrationTest::new(); + let sdk = EventIntegrationTest::new().await; let user = sdk.init_anon_user().await; let new_email = format!("{}@gmail.com", nanoid!(6)); let request = UpdateUserProfilePayloadPB::new(user.id).email(&new_email); let _ = EventBuilder::new(sdk.clone()) .event(UpdateUserProfile) .payload(request) - .sync_send(); + .async_send() + .await; let user_profile = EventBuilder::new(sdk.clone()) .event(GetUserProfile) - .sync_send() + .async_send() + .await .parse::(); // When the user is anonymous, the email is empty no matter what you set @@ -97,7 +104,7 @@ async fn anon_user_update_with_email() { #[tokio::test] async fn user_update_with_invalid_email() { - let test = EventIntegrationTest::new(); + let test = EventIntegrationTest::new().await; let user = test.init_anon_user().await; for email in invalid_email_test_case() { let request = UpdateUserProfilePayloadPB::new(user.id).email(&email); @@ -105,7 +112,8 @@ async fn user_update_with_invalid_email() { EventBuilder::new(test.clone()) .event(UpdateUserProfile) .payload(request) - .sync_send() + .async_send() + .await .error() .unwrap() .code, @@ -116,7 +124,7 @@ async fn user_update_with_invalid_email() { #[tokio::test] async fn user_update_with_invalid_password() { - let test = EventIntegrationTest::new(); + let test = EventIntegrationTest::new().await; let user = test.init_anon_user().await; for password in invalid_password_test_case() { let request = UpdateUserProfilePayloadPB::new(user.id).password(&password); @@ -133,13 +141,14 @@ async fn user_update_with_invalid_password() { #[tokio::test] async fn user_update_with_invalid_name() { - let test = EventIntegrationTest::new(); + let test = EventIntegrationTest::new().await; let user = test.init_anon_user().await; let request = UpdateUserProfilePayloadPB::new(user.id).name(""); assert!(EventBuilder::new(test.clone()) .event(UpdateUserProfile) .payload(request) - .sync_send() + .async_send() + .await .error() .is_some()) } diff --git a/frontend/rust-lib/event-integration/tests/user/migration_test/document_test.rs b/frontend/rust-lib/event-integration/tests/user/migration_test/document_test.rs index 9f3100b7ca4b..f15b5c3fddc4 100644 --- a/frontend/rust-lib/event-integration/tests/user/migration_test/document_test.rs +++ b/frontend/rust-lib/event-integration/tests/user/migration_test/document_test.rs @@ -11,7 +11,8 @@ async fn migrate_historical_empty_document_test() { "historical_empty_document", ) .unwrap(); - let test = EventIntegrationTest::new_with_user_data_path(user_db_path, DEFAULT_NAME.to_string()); + let test = + EventIntegrationTest::new_with_user_data_path(user_db_path, DEFAULT_NAME.to_string()).await; let views = test.get_all_workspace_views().await; assert_eq!(views.len(), 3); diff --git a/frontend/rust-lib/event-integration/tests/user/migration_test/version_test.rs b/frontend/rust-lib/event-integration/tests/user/migration_test/version_test.rs index ccd1b25e8c4a..dcaed72a0dbd 100644 --- a/frontend/rust-lib/event-integration/tests/user/migration_test/version_test.rs +++ b/frontend/rust-lib/event-integration/tests/user/migration_test/version_test.rs @@ -11,7 +11,8 @@ async fn migrate_020_historical_empty_document_test() { "020_historical_user_data", ) .unwrap(); - let test = EventIntegrationTest::new_with_user_data_path(user_db_path, DEFAULT_NAME.to_string()); + let test = + EventIntegrationTest::new_with_user_data_path(user_db_path, DEFAULT_NAME.to_string()).await; let mut views = test.get_all_workspace_views().await; assert_eq!(views.len(), 1); diff --git a/frontend/rust-lib/event-integration/tests/user/supabase_test/auth_test.rs b/frontend/rust-lib/event-integration/tests/user/supabase_test/auth_test.rs index d2bf889a8351..5375d5f33952 100644 --- a/frontend/rust-lib/event-integration/tests/user/supabase_test/auth_test.rs +++ b/frontend/rust-lib/event-integration/tests/user/supabase_test/auth_test.rs @@ -13,7 +13,7 @@ use event_integration::event_builder::EventBuilder; use event_integration::EventIntegrationTest; use flowy_core::DEFAULT_NAME; use flowy_encrypt::decrypt_text; -use flowy_server::supabase::define::{USER_EMAIL, USER_UUID}; +use flowy_server::supabase::define::{USER_DEVICE_ID, USER_EMAIL, USER_UUID}; use flowy_user::entities::{AuthTypePB, OauthSignInPB, UpdateUserProfilePayloadPB, UserProfilePB}; use flowy_user::errors::ErrorCode; use flowy_user::event_map::UserEvent::*; @@ -23,13 +23,14 @@ use crate::util::*; #[tokio::test] async fn third_party_sign_up_test() { if get_supabase_config().is_some() { - let test = EventIntegrationTest::new(); + let test = EventIntegrationTest::new().await; let mut map = HashMap::new(); map.insert(USER_UUID.to_string(), uuid::Uuid::new_v4().to_string()); map.insert( USER_EMAIL.to_string(), format!("{}@appflowy.io", nanoid!(6)), ); + map.insert(USER_DEVICE_ID.to_string(), uuid::Uuid::new_v4().to_string()); let payload = OauthSignInPB { map, auth_type: AuthTypePB::Supabase, @@ -48,7 +49,7 @@ async fn third_party_sign_up_test() { #[tokio::test] async fn third_party_sign_up_with_encrypt_test() { if get_supabase_config().is_some() { - let test = EventIntegrationTest::new(); + let test = EventIntegrationTest::new().await; test.supabase_party_sign_up().await; let user_profile = test.get_user_profile().await.unwrap(); assert!(user_profile.encryption_sign.is_empty()); @@ -65,11 +66,12 @@ async fn third_party_sign_up_with_encrypt_test() { #[tokio::test] async fn third_party_sign_up_with_duplicated_uuid() { if get_supabase_config().is_some() { - let test = EventIntegrationTest::new(); + let test = EventIntegrationTest::new().await; let email = format!("{}@appflowy.io", nanoid!(6)); let mut map = HashMap::new(); map.insert(USER_UUID.to_string(), uuid::Uuid::new_v4().to_string()); map.insert(USER_EMAIL.to_string(), email.clone()); + map.insert(USER_DEVICE_ID.to_string(), uuid::Uuid::new_v4().to_string()); let response_1 = EventBuilder::new(test.clone()) .event(OauthSignIn) @@ -98,7 +100,7 @@ async fn third_party_sign_up_with_duplicated_uuid() { #[tokio::test] async fn third_party_sign_up_with_duplicated_email() { if get_supabase_config().is_some() { - let test = EventIntegrationTest::new(); + let test = EventIntegrationTest::new().await; let email = format!("{}@appflowy.io", nanoid!(6)); test .supabase_sign_up_with_uuid(&uuid::Uuid::new_v4().to_string(), Some(email.clone())) @@ -138,7 +140,6 @@ async fn sign_up_as_guest_and_then_update_to_new_cloud_user_test() { assert_eq!(old_workspace.views.len(), new_workspace.views.len()); for (index, view) in old_views.iter().enumerate() { assert_eq!(view.name, new_views[index].name); - assert_eq!(view.id, new_views[index].id); assert_eq!(view.layout, new_views[index].layout); assert_eq!(view.create_time, new_views[index].create_time); } @@ -196,7 +197,7 @@ async fn sign_up_as_guest_and_then_update_to_existing_cloud_user_test() { #[tokio::test] async fn get_user_profile_test() { - if let Some(test) = FlowySupabaseTest::new() { + if let Some(test) = FlowySupabaseTest::new().await { let uuid = uuid::Uuid::new_v4().to_string(); test.supabase_sign_up_with_uuid(&uuid, None).await.unwrap(); @@ -207,7 +208,7 @@ async fn get_user_profile_test() { #[tokio::test] async fn update_user_profile_test() { - if let Some(test) = FlowySupabaseTest::new() { + if let Some(test) = FlowySupabaseTest::new().await { let uuid = uuid::Uuid::new_v4().to_string(); let profile = test.supabase_sign_up_with_uuid(&uuid, None).await.unwrap(); test @@ -221,7 +222,7 @@ async fn update_user_profile_test() { #[tokio::test] async fn update_user_profile_with_existing_email_test() { - if let Some(test) = FlowySupabaseTest::new() { + if let Some(test) = FlowySupabaseTest::new().await { let email = format!("{}@appflowy.io", nanoid!(6)); let _ = test .supabase_sign_up_with_uuid(&uuid::Uuid::new_v4().to_string(), Some(email.clone())) @@ -249,7 +250,7 @@ async fn update_user_profile_with_existing_email_test() { #[tokio::test] async fn migrate_anon_document_on_cloud_signup() { if get_supabase_config().is_some() { - let test = EventIntegrationTest::new(); + let test = EventIntegrationTest::new().await; let user_profile = test.sign_up_as_guest().await.user_profile; let view = test @@ -295,7 +296,7 @@ async fn migrate_anon_data_on_cloud_signup() { ) .unwrap(); let test = - EventIntegrationTest::new_with_user_data_path(user_db_path, DEFAULT_NAME.to_string()); + EventIntegrationTest::new_with_user_data_path(user_db_path, DEFAULT_NAME.to_string()).await; let user_profile = test.supabase_party_sign_up().await; // Get the folder data from remote diff --git a/frontend/rust-lib/event-integration/tests/user/supabase_test/workspace_test.rs b/frontend/rust-lib/event-integration/tests/user/supabase_test/workspace_test.rs index 49e4e289c7c2..2a971ef63635 100644 --- a/frontend/rust-lib/event-integration/tests/user/supabase_test/workspace_test.rs +++ b/frontend/rust-lib/event-integration/tests/user/supabase_test/workspace_test.rs @@ -12,7 +12,7 @@ use crate::util::*; #[tokio::test] async fn initial_workspace_test() { if get_supabase_config().is_some() { - let test = EventIntegrationTest::new(); + let test = EventIntegrationTest::new().await; let mut map = HashMap::new(); map.insert(USER_UUID.to_string(), uuid::Uuid::new_v4().to_string()); map.insert( diff --git a/frontend/rust-lib/event-integration/tests/util.rs b/frontend/rust-lib/event-integration/tests/util.rs index 37a2b20616d8..7653b39c4c1e 100644 --- a/frontend/rust-lib/event-integration/tests/util.rs +++ b/frontend/rust-lib/event-integration/tests/util.rs @@ -40,9 +40,9 @@ pub struct FlowySupabaseTest { } impl FlowySupabaseTest { - pub fn new() -> Option { + pub async fn new() -> Option { let _ = get_supabase_config()?; - let test = EventIntegrationTest::new(); + let test = EventIntegrationTest::new().await; test.set_auth_type(AuthTypePB::Supabase); test.server_provider.set_auth_type(AuthType::Supabase); @@ -71,12 +71,10 @@ impl Deref for FlowySupabaseTest { } pub async fn receive_with_timeout( - receiver: &mut Receiver, + mut receiver: Receiver, duration: Duration, -) -> Result> { - let res = timeout(duration, receiver.recv()) - .await? - .ok_or(anyhow::anyhow!("recv timeout"))?; +) -> Result> { + let res = timeout(duration, receiver.recv()).await.unwrap().unwrap(); Ok(res) } @@ -206,9 +204,9 @@ pub struct AFCloudTest { } impl AFCloudTest { - pub fn new() -> Option { + pub async fn new() -> Option { let _ = get_af_cloud_config()?; - let test = EventIntegrationTest::new(); + let test = EventIntegrationTest::new().await; test.set_auth_type(AuthTypePB::AFCloud); test.server_provider.set_auth_type(AuthType::AFCloud); diff --git a/frontend/rust-lib/flowy-core/Cargo.toml b/frontend/rust-lib/flowy-core/Cargo.toml index a843c8ecabb2..1dbc66e0117a 100644 --- a/frontend/rust-lib/flowy-core/Cargo.toml +++ b/frontend/rust-lib/flowy-core/Cargo.toml @@ -46,6 +46,7 @@ lib-infra = { path = "../../../shared-lib/lib-infra" } serde = "1.0" serde_json = "1.0" serde_repr = "0.1" +futures = "0.3.28" [features] default = ["rev-sqlite"] @@ -71,4 +72,4 @@ ts = [ ] rev-sqlite = ["flowy-user/rev-sqlite"] openssl_vendored = ["flowy-sqlite/openssl_vendored"] - +single_thread = ["lib-dispatch/single_thread"] diff --git a/frontend/rust-lib/flowy-core/src/integrate/log.rs b/frontend/rust-lib/flowy-core/src/integrate/log.rs index cdcacfe940b8..c6c606a00b1c 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/log.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/log.rs @@ -34,6 +34,7 @@ pub(crate) fn create_log_filter(level: String, with_crates: Vec) -> Stri filters.push(format!("flowy_notification={}", "info")); filters.push(format!("lib_infra={}", level)); filters.push(format!("flowy_task={}", level)); + // filters.push(format!("lib_dispatch={}", level)); filters.push(format!("dart_ffi={}", "info")); filters.push(format!("flowy_sqlite={}", "info")); diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index de97c12a51b8..4a7ade197299 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -5,7 +5,7 @@ use std::time::Duration; use std::{fmt, sync::Arc}; use tokio::sync::RwLock; -use tracing::error; +use tracing::{error, event, instrument}; use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabSource}; use flowy_database2::DatabaseManager; @@ -17,7 +17,7 @@ use flowy_task::{TaskDispatcher, TaskRunner}; use flowy_user::event_map::UserCloudServiceProvider; use flowy_user::manager::{UserManager, UserSessionConfig}; use lib_dispatch::prelude::*; -use lib_dispatch::runtime::tokio_default_runtime; +use lib_dispatch::runtime::AFPluginRuntime; use module::make_plugins; pub use module::*; @@ -82,7 +82,21 @@ pub struct AppFlowyCore { } impl AppFlowyCore { + #[cfg(feature = "single_thread")] + pub async fn new(config: AppFlowyCoreConfig) -> Self { + let runtime = Arc::new(AFPluginRuntime::new().unwrap()); + Self::init(config, runtime).await + } + + #[cfg(not(feature = "single_thread"))] pub fn new(config: AppFlowyCoreConfig) -> Self { + let runtime = Arc::new(AFPluginRuntime::new().unwrap()); + let cloned_runtime = runtime.clone(); + runtime.block_on(Self::init(config, cloned_runtime)) + } + + #[instrument(skip(config, runtime))] + async fn init(config: AppFlowyCoreConfig, runtime: Arc) -> Self { /// The profiling can be used to tracing the performance of the application. /// Check out the [Link](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/backend/profiling) /// for more information. @@ -95,8 +109,8 @@ impl AppFlowyCore { // Init the key value database let store_preference = Arc::new(StorePreferences::new(&config.storage_path).unwrap()); - tracing::info!("🔥 {:?}", &config); - let runtime = tokio_default_runtime().unwrap(); + tracing::info!("🔥db {:?}", &config); + tracing::debug!("🔥{}", runtime); let task_scheduler = TaskDispatcher::new(Duration::from_secs(2)); let task_dispatcher = Arc::new(RwLock::new(task_scheduler)); runtime.spawn(TaskRunner::run(task_dispatcher.clone())); @@ -108,6 +122,7 @@ impl AppFlowyCore { Arc::downgrade(&store_preference), )); + event!(tracing::Level::DEBUG, "Init managers",); let ( user_manager, folder_manager, @@ -115,7 +130,7 @@ impl AppFlowyCore { database_manager, document_manager, collab_builder, - ) = runtime.block_on(async { + ) = async { /// The shared collab builder is used to build the [Collab] instance. The plugins will be loaded /// on demand based on the [CollabPluginConfig]. let collab_builder = Arc::new(AppFlowyCollabBuilder::new(server_provider.clone())); @@ -162,7 +177,8 @@ impl AppFlowyCore { document_manager, collab_builder, ) - }); + } + .await; let user_status_callback = UserStatusCallbackImpl { collab_builder, @@ -179,17 +195,15 @@ impl AppFlowyCore { }; let cloned_user_session = Arc::downgrade(&user_manager); - runtime.block_on(async move { - if let Some(user_manager) = cloned_user_session.upgrade() { - if let Err(err) = user_manager - .init(user_status_callback, collab_interact_impl) - .await - { - error!("Init user failed: {}", err) - } + if let Some(user_session) = cloned_user_session.upgrade() { + event!(tracing::Level::DEBUG, "init user session",); + if let Err(err) = user_session + .init(user_status_callback, collab_interact_impl) + .await + { + error!("Init user failed: {}", err) } - }); - + } let event_dispatcher = Arc::new(AFPluginDispatcher::construct(runtime, || { make_plugins( Arc::downgrade(&folder_manager), diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index 06547c720ff3..b680364f1e2c 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -5,7 +5,7 @@ use collab_database::rows::RowId; use tokio::sync::oneshot; use flowy_error::{FlowyError, FlowyResult}; -use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; +use lib_dispatch::prelude::{af_spawn, data_result_ok, AFPluginData, AFPluginState, DataResult}; use lib_infra::util::timestamp; use crate::entities::*; @@ -697,7 +697,7 @@ pub(crate) async fn update_group_handler( let database_editor = manager.get_database_with_view_id(&view_id).await?; let group_changeset = GroupChangeset::from(params); let (tx, rx) = oneshot::channel(); - tokio::spawn(async move { + af_spawn(async move { let result = database_editor .update_group(&view_id, vec![group_changeset].into()) .await; diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index 10734ba8a50a..ec5d1c28092f 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -20,6 +20,7 @@ use collab_integrate::{CollabPersistenceConfig, RocksCollabDB}; use flowy_database_deps::cloud::DatabaseCloudService; use flowy_error::{internal_error, FlowyError, FlowyResult}; use flowy_task::TaskDispatcher; +use lib_dispatch::prelude::af_spawn; use crate::entities::{ DatabaseDescriptionPB, DatabaseLayoutPB, DatabaseSnapshotPB, DidFetchRowPB, @@ -361,7 +362,7 @@ impl DatabaseManager { /// Send notification to all clients that are listening to the given object. fn subscribe_block_event(workspace_database: &WorkspaceDatabase) { let mut block_event_rx = workspace_database.subscribe_block_event(); - tokio::spawn(async move { + af_spawn(async move { while let Ok(event) = block_event_rx.recv().await { match event { BlockEvent::DidFetchRow(row_details) => { diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index e5343693de23..767542d407a0 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -12,6 +12,7 @@ use tracing::{event, warn}; use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult}; use flowy_task::TaskDispatcher; +use lib_dispatch::prelude::af_spawn; use lib_infra::future::{to_fut, Fut, FutureResult}; use crate::entities::*; @@ -56,7 +57,7 @@ impl DatabaseEditor { // Receive database sync state and send to frontend via the notification let mut sync_state = database.lock().subscribe_sync_state(); let cloned_database_id = database_id.clone(); - tokio::spawn(async move { + af_spawn(async move { while let Some(sync_state) = sync_state.next().await { send_notification( &cloned_database_id, @@ -69,7 +70,7 @@ impl DatabaseEditor { // Receive database snapshot state and send to frontend via the notification let mut snapshot_state = database.lock().subscribe_snapshot_state(); - tokio::spawn(async move { + af_spawn(async move { while let Some(snapshot_state) = snapshot_state.next().await { if let Some(new_snapshot_id) = snapshot_state.snapshot_id() { tracing::debug!( diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs index 8dfc39416b18..276ef4827373 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs @@ -9,6 +9,7 @@ use collab_database::views::{DatabaseLayout, DatabaseView}; use tokio::sync::{broadcast, RwLock}; use flowy_error::{FlowyError, FlowyResult}; +use lib_dispatch::prelude::af_spawn; use crate::entities::{ CalendarEventPB, DatabaseLayoutMetaPB, DatabaseLayoutSettingPB, DeleteFilterParams, @@ -60,7 +61,7 @@ impl DatabaseViewEditor { cell_cache: CellCache, ) -> FlowyResult { let (notifier, _) = broadcast::channel(100); - tokio::spawn(DatabaseViewChangedReceiverRunner(Some(notifier.subscribe())).run()); + af_spawn(DatabaseViewChangedReceiverRunner(Some(notifier.subscribe())).run()); // Group let group_controller = Arc::new(RwLock::new( new_group_controller(view_id.clone(), delegate.clone()).await?, @@ -237,7 +238,7 @@ impl DatabaseViewEditor { let row_id = row_detail.row.id.clone(); let weak_filter_controller = Arc::downgrade(&self.filter_controller); let weak_sort_controller = Arc::downgrade(&self.sort_controller); - tokio::spawn(async move { + af_spawn(async move { if let Some(filter_controller) = weak_filter_controller.upgrade() { filter_controller .did_receive_row_changed(row_id.clone()) @@ -645,7 +646,7 @@ impl DatabaseViewEditor { let filter_type = UpdatedFilterType::new(Some(old), new); let filter_changeset = FilterChangeset::from_update(filter_type); let filter_controller = self.filter_controller.clone(); - tokio::spawn(async move { + af_spawn(async move { if let Some(notification) = filter_controller .did_receive_changes(filter_changeset) .await diff --git a/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs b/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs index f8f1aa8c2137..b71cdddf378b 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs @@ -12,6 +12,7 @@ use serde::Serialize; use tracing::event; use flowy_error::{FlowyError, FlowyResult}; +use lib_dispatch::prelude::af_spawn; use lib_infra::future::Fut; use crate::entities::{GroupChangesPB, GroupPB, InsertedGroupPB}; @@ -415,7 +416,7 @@ where let configuration = (*self.setting).clone(); let writer = self.writer.clone(); let view_id = self.view_id.clone(); - tokio::spawn(async move { + af_spawn(async move { match writer.save_configuration(&view_id, configuration).await { Ok(_) => {}, Err(e) => { diff --git a/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs b/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs index 852b7ae1e856..0a52c1538e1b 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs @@ -37,7 +37,7 @@ pub struct DatabaseEditorTest { impl DatabaseEditorTest { pub async fn new_grid() -> Self { - let sdk = EventIntegrationTest::new(); + let sdk = EventIntegrationTest::new().await; let _ = sdk.init_anon_user().await; let params = make_test_grid(); @@ -46,7 +46,7 @@ impl DatabaseEditorTest { } pub async fn new_no_date_grid() -> Self { - let sdk = EventIntegrationTest::new(); + let sdk = EventIntegrationTest::new().await; let _ = sdk.init_anon_user().await; let params = make_no_date_test_grid(); @@ -55,7 +55,7 @@ impl DatabaseEditorTest { } pub async fn new_board() -> Self { - let sdk = EventIntegrationTest::new(); + let sdk = EventIntegrationTest::new().await; let _ = sdk.init_anon_user().await; let params = make_test_board(); @@ -64,7 +64,7 @@ impl DatabaseEditorTest { } pub async fn new_calendar() -> Self { - let sdk = EventIntegrationTest::new(); + let sdk = EventIntegrationTest::new().await; let _ = sdk.init_anon_user().await; let params = make_test_calendar(); diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/script.rs index de07820b6c43..423761d17652 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/filter_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/script.rs @@ -14,6 +14,7 @@ use flowy_database2::entities::{CheckboxFilterConditionPB, CheckboxFilterPB, Che use flowy_database2::services::database_view::DatabaseViewChanged; use flowy_database2::services::field::SelectOption; use flowy_database2::services::filter::FilterType; +use lib_dispatch::prelude::af_spawn; use crate::database::database_editor::DatabaseEditorTest; @@ -278,7 +279,7 @@ impl DatabaseFilterTest { if change.is_none() {return;} let change = change.unwrap(); let mut receiver = self.recv.take().unwrap(); - tokio::spawn(async move { + af_spawn(async move { match tokio::time::timeout(Duration::from_secs(2), receiver.recv()).await { Ok(changed) => { match changed.unwrap() { DatabaseViewChanged::FilterNotification(notification) => { diff --git a/frontend/rust-lib/flowy-document2/src/document.rs b/frontend/rust-lib/flowy-document2/src/document.rs index bfa90980a742..2f56b28bdee6 100644 --- a/frontend/rust-lib/flowy-document2/src/document.rs +++ b/frontend/rust-lib/flowy-document2/src/document.rs @@ -9,6 +9,7 @@ use futures::StreamExt; use parking_lot::Mutex; use flowy_error::FlowyResult; +use lib_dispatch::prelude::af_spawn; use crate::entities::{DocEventPB, DocumentSnapshotStatePB, DocumentSyncStatePB}; use crate::notification::{send_notification, DocumentNotification}; @@ -61,7 +62,7 @@ fn subscribe_document_changed(doc_id: &str, document: &MutexDocument) { fn subscribe_document_snapshot_state(collab: &Arc) { let document_id = collab.lock().object_id.clone(); let mut snapshot_state = collab.lock().subscribe_snapshot_state(); - tokio::spawn(async move { + af_spawn(async move { while let Some(snapshot_state) = snapshot_state.next().await { if let Some(new_snapshot_id) = snapshot_state.snapshot_id() { tracing::debug!("Did create document remote snapshot: {}", new_snapshot_id); @@ -79,7 +80,7 @@ fn subscribe_document_snapshot_state(collab: &Arc) { fn subscribe_document_sync_state(collab: &Arc) { let document_id = collab.lock().object_id.clone(); let mut sync_state_stream = collab.lock().subscribe_sync_state(); - tokio::spawn(async move { + af_spawn(async move { while let Some(sync_state) = sync_state_stream.next().await { send_notification( &document_id, diff --git a/frontend/rust-lib/flowy-folder2/src/manager.rs b/frontend/rust-lib/flowy-folder2/src/manager.rs index b15ce1b2c56c..7a2b37c8c224 100644 --- a/frontend/rust-lib/flowy-folder2/src/manager.rs +++ b/frontend/rust-lib/flowy-folder2/src/manager.rs @@ -18,6 +18,7 @@ use collab_integrate::collab_builder::AppFlowyCollabBuilder; use collab_integrate::{CollabPersistenceConfig, RocksCollabDB, YrsDocAction}; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_folder_deps::cloud::{gen_view_id, FolderCloudService}; +use lib_dispatch::prelude::af_spawn; use crate::entities::icon::UpdateViewIconParams; use crate::entities::{ @@ -1027,7 +1028,7 @@ fn subscribe_folder_view_changed( weak_mutex_folder: &Weak, ) { let weak_mutex_folder = weak_mutex_folder.clone(); - tokio::spawn(async move { + af_spawn(async move { while let Ok(value) = rx.recv().await { if let Some(folder) = weak_mutex_folder.upgrade() { tracing::trace!("Did receive view change: {:?}", value); @@ -1065,7 +1066,7 @@ fn subscribe_folder_snapshot_state_changed( weak_mutex_folder: &Weak, ) { let weak_mutex_folder = weak_mutex_folder.clone(); - tokio::spawn(async move { + af_spawn(async move { if let Some(mutex_folder) = weak_mutex_folder.upgrade() { let stream = mutex_folder .lock() @@ -1093,7 +1094,7 @@ fn subscribe_folder_sync_state_changed( mut folder_sync_state_rx: WatchStream, _weak_mutex_folder: &Weak, ) { - tokio::spawn(async move { + af_spawn(async move { while let Some(state) = folder_sync_state_rx.next().await { send_notification(&workspace_id, FolderNotification::DidUpdateFolderSyncUpdate) .payload(FolderSyncStatePB::from(state)) @@ -1108,7 +1109,7 @@ fn subscribe_folder_trash_changed( weak_mutex_folder: &Weak, ) { let weak_mutex_folder = weak_mutex_folder.clone(); - tokio::spawn(async move { + af_spawn(async move { while let Ok(value) = rx.recv().await { if let Some(folder) = weak_mutex_folder.upgrade() { let mut unique_ids = HashSet::new(); diff --git a/frontend/rust-lib/flowy-server/Cargo.toml b/frontend/rust-lib/flowy-server/Cargo.toml index ccdc0a7b5681..cfa3007ef057 100644 --- a/frontend/rust-lib/flowy-server/Cargo.toml +++ b/frontend/rust-lib/flowy-server/Cargo.toml @@ -44,6 +44,7 @@ url = "2.4" tokio-util = "0.7" tokio-stream = { version = "0.1.14", features = ["sync"] } client-api = { version = "0.1.0", features = ["collab-sync"] } +lib-dispatch = { workspace = true } [dev-dependencies] uuid = { version = "1.3.3", features = ["v4"] } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs index f2257586b828..09542ee9c190 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs @@ -19,6 +19,7 @@ use flowy_server_config::af_cloud_config::AFCloudConfiguration; use flowy_storage::FileStorageService; use flowy_user_deps::cloud::UserCloudService; use flowy_user_deps::entities::UserTokenState; +use lib_dispatch::prelude::af_spawn; use lib_infra::future::FutureResult; use crate::af_cloud::impls::{ @@ -94,7 +95,7 @@ impl AppFlowyServer for AFCloudServer { let mut token_state_rx = self.client.subscribe_token_state(); let (watch_tx, watch_rx) = watch::channel(UserTokenState::Invalid); let weak_client = Arc::downgrade(&self.client); - tokio::spawn(async move { + af_spawn(async move { while let Ok(token_state) = token_state_rx.recv().await { if let Some(client) = weak_client.upgrade() { match token_state { @@ -185,7 +186,7 @@ fn spawn_ws_conn( let weak_api_client = Arc::downgrade(api_client); let enable_sync = enable_sync.clone(); - tokio::spawn(async move { + af_spawn(async move { if let Some(ws_client) = weak_ws_client.upgrade() { let mut state_recv = ws_client.subscribe_connect_state(); while let Ok(state) = state_recv.recv().await { @@ -215,7 +216,7 @@ fn spawn_ws_conn( let weak_device_id = Arc::downgrade(device_id); let weak_ws_client = Arc::downgrade(ws_client); let weak_api_client = Arc::downgrade(api_client); - tokio::spawn(async move { + af_spawn(async move { while let Ok(token_state) = token_state_rx.recv().await { match token_state { TokenState::Refresh => { diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/database.rs b/frontend/rust-lib/flowy-server/src/supabase/api/database.rs index 963978c2fca2..8b38ad62fda9 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/database.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/database.rs @@ -5,6 +5,7 @@ use tokio::sync::oneshot::channel; use flowy_database_deps::cloud::{ CollabObjectUpdate, CollabObjectUpdateByOid, DatabaseCloudService, DatabaseSnapshot, }; +use lib_dispatch::prelude::af_spawn; use lib_infra::future::FutureResult; use crate::supabase::api::request::{ @@ -35,7 +36,7 @@ where let try_get_postgrest = self.server.try_get_weak_postgrest(); let object_id = object_id.to_string(); let (tx, rx) = channel(); - tokio::spawn(async move { + af_spawn(async move { tx.send( async move { let postgrest = try_get_postgrest?; @@ -58,7 +59,7 @@ where ) -> FutureResult { let try_get_postgrest = self.server.try_get_weak_postgrest(); let (tx, rx) = channel(); - tokio::spawn(async move { + af_spawn(async move { tx.send( async move { let postgrest = try_get_postgrest?; diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/document.rs b/frontend/rust-lib/flowy-server/src/supabase/api/document.rs index 9968a1c44b2d..baa140d9cc94 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/document.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/document.rs @@ -7,6 +7,7 @@ use tokio::sync::oneshot::channel; use flowy_document_deps::cloud::{DocumentCloudService, DocumentSnapshot}; use flowy_error::FlowyError; +use lib_dispatch::prelude::af_spawn; use lib_infra::future::FutureResult; use crate::supabase::api::request::{get_snapshots_from_server, FetchObjectUpdateAction}; @@ -35,7 +36,7 @@ where let try_get_postgrest = self.server.try_get_weak_postgrest(); let document_id = document_id.to_string(); let (tx, rx) = channel(); - tokio::spawn(async move { + af_spawn(async move { tx.send( async move { let postgrest = try_get_postgrest?; @@ -85,7 +86,7 @@ where let try_get_postgrest = self.server.try_get_weak_postgrest(); let document_id = document_id.to_string(); let (tx, rx) = channel(); - tokio::spawn(async move { + af_spawn(async move { tx.send( async move { let postgrest = try_get_postgrest?; diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs b/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs index a8147792c31c..c3e15678c164 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs @@ -10,6 +10,7 @@ use tokio::sync::oneshot::channel; use flowy_folder_deps::cloud::{ gen_workspace_id, Folder, FolderCloudService, FolderData, FolderSnapshot, Workspace, }; +use lib_dispatch::prelude::af_spawn; use lib_infra::future::FutureResult; use crate::response::ExtendedResponse; @@ -116,7 +117,7 @@ where let try_get_postgrest = self.server.try_get_weak_postgrest(); let workspace_id = workspace_id.to_string(); let (tx, rx) = channel(); - tokio::spawn(async move { + af_spawn(async move { tx.send( async move { let postgrest = try_get_postgrest?; diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs index a08f6f3ef0e7..3f446bfcf8b6 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs @@ -21,6 +21,7 @@ use flowy_folder_deps::cloud::{Folder, Workspace}; use flowy_user_deps::cloud::*; use flowy_user_deps::entities::*; use flowy_user_deps::DEFAULT_USER_NAME; +use lib_dispatch::prelude::af_spawn; use lib_infra::box_any::BoxAny; use lib_infra::future::FutureResult; use lib_infra::util::timestamp; @@ -238,7 +239,7 @@ where let try_get_postgrest = self.server.try_get_weak_postgrest(); let awareness_id = uid.to_string(); let (tx, rx) = channel(); - tokio::spawn(async move { + af_spawn(async move { tx.send( async move { let postgrest = try_get_postgrest?; @@ -278,7 +279,7 @@ where let try_get_postgrest = self.server.try_get_weak_postgrest(); let (tx, rx) = channel(); let init_update = empty_workspace_update(&collab_object); - tokio::spawn(async move { + af_spawn(async move { tx.send( async move { let postgrest = try_get_postgrest? @@ -316,7 +317,7 @@ where let try_get_postgrest = self.server.try_get_weak_postgrest(); let cloned_collab_object = collab_object.clone(); let (tx, rx) = channel(); - tokio::spawn(async move { + af_spawn(async move { tx.send( async move { CreateCollabAction::new(cloned_collab_object, try_get_postgrest?, update) diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index e83320b219f6..35bc71bbdc2d 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -93,7 +93,7 @@ pub async fn get_user_profile_handler( let cloned_user_profile = user_profile.clone(); // Refresh the user profile in the background - tokio::spawn(async move { + af_spawn(async move { if let Some(manager) = weak_manager.upgrade() { let _ = manager.refresh_user_profile(&cloned_user_profile).await; } diff --git a/frontend/rust-lib/flowy-user/src/manager.rs b/frontend/rust-lib/flowy-user/src/manager.rs index 4fb483e6e9a9..be9ae8a99269 100644 --- a/frontend/rust-lib/flowy-user/src/manager.rs +++ b/frontend/rust-lib/flowy-user/src/manager.rs @@ -16,6 +16,7 @@ use flowy_sqlite::ConnectionPool; use flowy_sqlite::{query_dsl::*, DBConnection, ExpressionMethods}; use flowy_user_deps::cloud::UserUpdate; use flowy_user_deps::entities::*; +use lib_dispatch::prelude::af_spawn; use lib_infra::box_any::BoxAny; use crate::entities::{AuthStateChangedPB, AuthStatePB, UserProfilePB, UserSettingPB}; @@ -93,7 +94,7 @@ impl UserManager { let weak_user_manager = Arc::downgrade(&user_manager); if let Ok(user_service) = user_manager.cloud_services.get_user_service() { if let Some(mut rx) = user_service.subscribe_user_update() { - tokio::spawn(async move { + af_spawn(async move { while let Ok(update) = rx.recv().await { if let Some(user_manager) = weak_user_manager.upgrade() { if let Err(err) = user_manager.handler_user_update(update).await { @@ -133,7 +134,7 @@ impl UserManager { // Subscribe the token state let weak_pool = Arc::downgrade(&self.db_pool(user.uid)?); if let Some(mut token_state_rx) = self.cloud_services.subscribe_token_state() { - tokio::spawn(async move { + af_spawn(async move { while let Some(token_state) = token_state_rx.next().await { match token_state { UserTokenState::Refresh { token } => { @@ -401,7 +402,7 @@ impl UserManager { self.set_session(None)?; let server = self.cloud_services.get_user_service()?; - tokio::spawn(async move { + af_spawn(async move { if let Err(err) = server.sign_out(None).await { event!(tracing::Level::ERROR, "{:?}", err); } @@ -536,7 +537,7 @@ impl UserManager { params: UpdateUserProfileParams, ) -> Result<(), FlowyError> { let server = self.cloud_services.get_user_service()?; - tokio::spawn(async move { + af_spawn(async move { let credentials = UserCredentials::new(Some(token), Some(uid), None); server.update_user(credentials, params).await }) diff --git a/frontend/rust-lib/flowy-user/src/services/user_workspace.rs b/frontend/rust-lib/flowy-user/src/services/user_workspace.rs index 6002556d9cad..4112c5438e19 100644 --- a/frontend/rust-lib/flowy-user/src/services/user_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/services/user_workspace.rs @@ -7,6 +7,7 @@ use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::schema::user_workspace_table; use flowy_sqlite::{query_dsl::*, ConnectionPool, ExpressionMethods}; use flowy_user_deps::entities::{Role, UserWorkspace, WorkspaceMember}; +use lib_dispatch::prelude::af_spawn; use crate::entities::{RepeatedUserWorkspacePB, ResetWorkspacePB}; use crate::manager::UserManager; @@ -99,7 +100,7 @@ impl UserManager { if let Ok(service) = self.cloud_services.get_user_service() { if let Ok(pool) = self.db_pool(uid) { - tokio::spawn(async move { + af_spawn(async move { if let Ok(new_user_workspaces) = service.get_all_user_workspaces(uid).await { let _ = save_user_workspaces(uid, pool, &new_user_workspaces); let repeated_workspace_pbs = RepeatedUserWorkspacePB::from(new_user_workspaces); diff --git a/frontend/rust-lib/lib-dispatch/Cargo.toml b/frontend/rust-lib/lib-dispatch/Cargo.toml index 99d22d7448a2..0ae9fa340ef8 100644 --- a/frontend/rust-lib/lib-dispatch/Cargo.toml +++ b/frontend/rust-lib/lib-dispatch/Cargo.toml @@ -22,17 +22,18 @@ serde_json = {version = "1.0", optional = true } serde = { version = "1.0", features = ["derive"], optional = true } serde_repr = { version = "0.1", optional = true } validator = "0.16.1" +tracing = { version = "0.1"} #optional crate bincode = { version = "1.3", optional = true} protobuf = {version = "2.28.0", optional = true} -tracing = { version = "0.1"} [dev-dependencies] tokio = { version = "1.26", features = ["full"] } futures-util = "0.3.26" [features] -default = ["use_protobuf"] +default = ["use_protobuf", ] use_serde = ["bincode", "serde_json", "serde", "serde_repr"] use_protobuf= ["protobuf"] +single_thread = [] diff --git a/frontend/rust-lib/lib-dispatch/src/byte_trait.rs b/frontend/rust-lib/lib-dispatch/src/byte_trait.rs index 4548fd3cab6e..6e8996e824e6 100644 --- a/frontend/rust-lib/lib-dispatch/src/byte_trait.rs +++ b/frontend/rust-lib/lib-dispatch/src/byte_trait.rs @@ -1,6 +1,7 @@ -use crate::errors::{DispatchError, InternalError}; use bytes::Bytes; +use crate::errors::{DispatchError, InternalError}; + // To bytes pub trait ToBytes { fn into_bytes(self) -> Result; @@ -26,21 +27,6 @@ where } } -// #[cfg(feature = "use_serde")] -// impl ToBytes for T -// where -// T: serde::Serialize, -// { -// fn into_bytes(self) -> Result { -// match serde_json::to_string(&self.0) { -// Ok(s) => Ok(Bytes::from(s)), -// Err(e) => Err(InternalError::SerializeToBytes(format!("{:?}", e)).into()), -// } -// } -// } - -// From bytes - pub trait AFPluginFromBytes: Sized { fn parse_from_bytes(bytes: Bytes) -> Result; } diff --git a/frontend/rust-lib/lib-dispatch/src/dispatcher.rs b/frontend/rust-lib/lib-dispatch/src/dispatcher.rs index dfd0d1dcc62d..a7a05c7b92e7 100644 --- a/frontend/rust-lib/lib-dispatch/src/dispatcher.rs +++ b/frontend/rust-lib/lib-dispatch/src/dispatcher.rs @@ -1,3 +1,13 @@ +use std::any::Any; +use std::pin::Pin; +use std::task::{Context, Poll}; +use std::{future::Future, sync::Arc}; + +use derivative::*; +use pin_project::pin_project; +use tracing::event; + +use crate::module::AFPluginStateMap; use crate::runtime::AFPluginRuntime; use crate::{ errors::{DispatchError, Error, InternalError}, @@ -5,20 +15,76 @@ use crate::{ response::AFPluginEventResponse, service::{AFPluginServiceFactory, Service}, }; -use derivative::*; -use futures_core::future::BoxFuture; -use futures_util::task::Context; -use pin_project::pin_project; -use std::{future::Future, sync::Arc}; -use tokio::macros::support::{Pin, Poll}; + +#[cfg(feature = "single_thread")] +pub trait AFConcurrent {} + +#[cfg(feature = "single_thread")] +impl AFConcurrent for T where T: ?Sized {} + +#[cfg(not(feature = "single_thread"))] +pub trait AFConcurrent: Send + Sync {} + +#[cfg(not(feature = "single_thread"))] +impl AFConcurrent for T where T: Send + Sync {} + +#[cfg(feature = "single_thread")] +pub type AFBoxFuture<'a, T> = futures_core::future::LocalBoxFuture<'a, T>; + +#[cfg(not(feature = "single_thread"))] +pub type AFBoxFuture<'a, T> = futures_core::future::BoxFuture<'a, T>; + +pub type AFStateMap = std::sync::Arc; + +#[cfg(feature = "single_thread")] +pub(crate) fn downcast_owned(boxed: AFBox) -> Option { + boxed.downcast().ok().map(|boxed| *boxed) +} + +#[cfg(not(feature = "single_thread"))] +pub(crate) fn downcast_owned(boxed: AFBox) -> Option { + boxed.downcast().ok().map(|boxed| *boxed) +} + +#[cfg(feature = "single_thread")] +pub(crate) type AFBox = Box; + +#[cfg(not(feature = "single_thread"))] +pub(crate) type AFBox = Box; + +#[cfg(feature = "single_thread")] +pub type BoxFutureCallback = + Box AFBoxFuture<'static, ()> + 'static>; + +#[cfg(not(feature = "single_thread"))] +pub type BoxFutureCallback = + Box AFBoxFuture<'static, ()> + Send + Sync + 'static>; + +#[cfg(feature = "single_thread")] +pub fn af_spawn(future: T) -> tokio::task::JoinHandle +where + T: Future + Send + 'static, + T::Output: Send + 'static, +{ + tokio::spawn(future) +} + +#[cfg(not(feature = "single_thread"))] +pub fn af_spawn(future: T) -> tokio::task::JoinHandle +where + T: Future + Send + 'static, + T::Output: Send + 'static, +{ + tokio::spawn(future) +} pub struct AFPluginDispatcher { plugins: AFPluginMap, - runtime: AFPluginRuntime, + runtime: Arc, } impl AFPluginDispatcher { - pub fn construct(runtime: AFPluginRuntime, module_factory: F) -> AFPluginDispatcher + pub fn construct(runtime: Arc, module_factory: F) -> AFPluginDispatcher where F: FnOnce() -> Vec, { @@ -30,24 +96,77 @@ impl AFPluginDispatcher { } } - pub fn async_send( + pub async fn async_send( + dispatch: Arc, + request: Req, + ) -> AFPluginEventResponse + where + Req: Into, + { + AFPluginDispatcher::async_send_with_callback(dispatch, request, |_| Box::pin(async {})).await + } + + pub async fn async_send_with_callback( + dispatch: Arc, + request: Req, + callback: Callback, + ) -> AFPluginEventResponse + where + Req: Into, + Callback: FnOnce(AFPluginEventResponse) -> AFBoxFuture<'static, ()> + AFConcurrent + 'static, + { + let request: AFPluginRequest = request.into(); + let plugins = dispatch.plugins.clone(); + let service = Box::new(DispatchService { plugins }); + tracing::trace!("Async event: {:?}", &request.event); + let service_ctx = DispatchContext { + request, + callback: Some(Box::new(callback)), + }; + + // Spawns a future onto the runtime. + // + // This spawns the given future onto the runtime's executor, usually a + // thread pool. The thread pool is then responsible for polling the future + // until it completes. + // + // The provided future will start running in the background immediately + // when `spawn` is called, even if you don't await the returned + // `JoinHandle`. + let handle = dispatch.runtime.spawn(async move { + service.call(service_ctx).await.unwrap_or_else(|e| { + tracing::error!("Dispatch runtime error: {:?}", e); + InternalError::Other(format!("{:?}", e)).as_response() + }) + }); + + let result = dispatch.runtime.run_until(handle).await; + result.unwrap_or_else(|e| { + let msg = format!("EVENT_DISPATCH join error: {:?}", e); + tracing::error!("{}", msg); + let error = InternalError::JoinError(msg); + error.as_response() + }) + } + + pub fn box_async_send( dispatch: Arc, request: Req, ) -> DispatchFuture where - Req: std::convert::Into, + Req: Into + 'static, { - AFPluginDispatcher::async_send_with_callback(dispatch, request, |_| Box::pin(async {})) + AFPluginDispatcher::boxed_async_send_with_callback(dispatch, request, |_| Box::pin(async {})) } - pub fn async_send_with_callback( + pub fn boxed_async_send_with_callback( dispatch: Arc, request: Req, callback: Callback, ) -> DispatchFuture where - Req: std::convert::Into, - Callback: FnOnce(AFPluginEventResponse) -> BoxFuture<'static, ()> + 'static + Send + Sync, + Req: Into + 'static, + Callback: FnOnce(AFPluginEventResponse) -> AFBoxFuture<'static, ()> + AFConcurrent + 'static, { let request: AFPluginRequest = request.into(); let plugins = dispatch.plugins.clone(); @@ -57,7 +176,17 @@ impl AFPluginDispatcher { request, callback: Some(Box::new(callback)), }; - let join_handle = dispatch.runtime.spawn(async move { + + // Spawns a future onto the runtime. + // + // This spawns the given future onto the runtime's executor, usually a + // thread pool. The thread pool is then responsible for polling the future + // until it completes. + // + // The provided future will start running in the background immediately + // when `spawn` is called, even if you don't await the returned + // `JoinHandle`. + let handle = dispatch.runtime.spawn(async move { service.call(service_ctx).await.unwrap_or_else(|e| { tracing::error!("Dispatch runtime error: {:?}", e); InternalError::Other(format!("{:?}", e)).as_response() @@ -66,7 +195,8 @@ impl AFPluginDispatcher { DispatchFuture { fut: Box::pin(async move { - join_handle.await.unwrap_or_else(|e| { + let result = dispatch.runtime.run_until(handle).await; + result.unwrap_or_else(|e| { let msg = format!("EVENT_DISPATCH join error: {:?}", e); tracing::error!("{}", msg); let error = InternalError::JoinError(msg); @@ -76,44 +206,56 @@ impl AFPluginDispatcher { } } + #[cfg(not(feature = "single_thread"))] pub fn sync_send( dispatch: Arc, request: AFPluginRequest, ) -> AFPluginEventResponse { - futures::executor::block_on(async { - AFPluginDispatcher::async_send_with_callback(dispatch, request, |_| Box::pin(async {})).await - }) + futures::executor::block_on(AFPluginDispatcher::async_send_with_callback( + dispatch, + request, + |_| Box::pin(async {}), + )) } - pub fn spawn(&self, f: F) + #[cfg(feature = "single_thread")] + #[track_caller] + pub fn spawn(&self, future: F) -> tokio::task::JoinHandle where - F: Future + Send + 'static, + F: Future + 'static, { - self.runtime.spawn(f); + self.runtime.spawn(future) } -} -#[pin_project] -pub struct DispatchFuture { - #[pin] - pub fut: Pin + Sync + Send>>, -} + #[cfg(not(feature = "single_thread"))] + #[track_caller] + pub fn spawn(&self, future: F) -> tokio::task::JoinHandle + where + F: Future + Send + 'static, + ::Output: Send + 'static, + { + self.runtime.spawn(future) + } -impl Future for DispatchFuture -where - T: Send + Sync, -{ - type Output = T; + #[cfg(feature = "single_thread")] + pub async fn run_until(&self, future: F) -> F::Output + where + F: Future + 'static, + { + let handle = self.runtime.spawn(future); + self.runtime.run_until(handle).await.unwrap() + } - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let this = self.as_mut().project(); - Poll::Ready(futures_core::ready!(this.fut.poll(cx))) + #[cfg(not(feature = "single_thread"))] + pub async fn run_until<'a, F>(&self, future: F) -> F::Output + where + F: Future + Send + 'a, + ::Output: Send + 'a, + { + self.runtime.run_until(future).await } } -pub type BoxFutureCallback = - Box BoxFuture<'static, ()> + 'static + Send + Sync>; - #[derive(Derivative)] #[derivative(Debug)] pub struct DispatchContext { @@ -136,36 +278,37 @@ pub(crate) struct DispatchService { impl Service for DispatchService { type Response = AFPluginEventResponse; type Error = DispatchError; - type Future = BoxFuture<'static, Result>; + type Future = AFBoxFuture<'static, Result>; - #[cfg_attr( - feature = "use_tracing", - tracing::instrument(name = "DispatchService", level = "debug", skip(self, ctx)) - )] + #[tracing::instrument(name = "DispatchService", level = "debug", skip(self, ctx))] fn call(&self, ctx: DispatchContext) -> Self::Future { let module_map = self.plugins.clone(); let (request, callback) = ctx.into_parts(); Box::pin(async move { let result = { - // print_module_map_info(&module_map); match module_map.get(&request.event) { Some(module) => { - tracing::trace!("Handle event: {:?} by {:?}", &request.event, module.name); + event!( + tracing::Level::TRACE, + "Handle event: {:?} by {:?}", + &request.event, + module.name + ); let fut = module.new_service(()); let service_fut = fut.await?.call(request); service_fut.await }, None => { let msg = format!("Can not find the event handler. {:?}", request); - tracing::error!("{}", msg); + event!(tracing::Level::ERROR, "{}", msg); Err(InternalError::HandleNotFound(msg).into()) }, } }; let response = result.unwrap_or_else(|e| e.into()); - tracing::trace!("Dispatch result: {:?}", response); + event!(tracing::Level::TRACE, "Dispatch result: {:?}", response); if let Some(callback) = callback { callback(response.clone()).await; } @@ -190,3 +333,21 @@ fn print_plugins(plugins: &AFPluginMap) { tracing::info!("Event: {:?} plugin : {:?}", k, v.name); }) } + +#[pin_project] +pub struct DispatchFuture { + #[pin] + pub fut: Pin + 'static>>, +} + +impl Future for DispatchFuture +where + T: AFConcurrent + 'static, +{ + type Output = T; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.as_mut().project(); + Poll::Ready(futures_core::ready!(this.fut.poll(cx))) + } +} diff --git a/frontend/rust-lib/lib-dispatch/src/errors/errors.rs b/frontend/rust-lib/lib-dispatch/src/errors/errors.rs index cbfd8542ecf6..19f0e336c160 100644 --- a/frontend/rust-lib/lib-dispatch/src/errors/errors.rs +++ b/frontend/rust-lib/lib-dispatch/src/errors/errors.rs @@ -1,15 +1,17 @@ +use std::fmt; + +use bytes::Bytes; +use dyn_clone::DynClone; +use tokio::{sync::mpsc::error::SendError, task::JoinError}; + +use crate::prelude::AFConcurrent; use crate::{ byte_trait::AFPluginFromBytes, request::AFPluginEventRequest, response::{AFPluginEventResponse, ResponseBuilder}, }; -use bytes::Bytes; -use dyn_clone::DynClone; - -use std::fmt; -use tokio::{sync::mpsc::error::SendError, task::JoinError}; -pub trait Error: fmt::Debug + DynClone + Send + Sync { +pub trait Error: fmt::Debug + DynClone + AFConcurrent { fn as_response(&self) -> AFPluginEventResponse; } diff --git a/frontend/rust-lib/lib-dispatch/src/module/container.rs b/frontend/rust-lib/lib-dispatch/src/module/container.rs index a95b3b702ec5..d6fdf24d6795 100644 --- a/frontend/rust-lib/lib-dispatch/src/module/container.rs +++ b/frontend/rust-lib/lib-dispatch/src/module/container.rs @@ -1,10 +1,9 @@ -use std::{ - any::{Any, TypeId}, - collections::HashMap, -}; +use std::{any::TypeId, collections::HashMap}; + +use crate::prelude::{downcast_owned, AFBox, AFConcurrent}; #[derive(Default, Debug)] -pub struct AFPluginStateMap(HashMap>); +pub struct AFPluginStateMap(HashMap); impl AFPluginStateMap { #[inline] @@ -14,7 +13,7 @@ impl AFPluginStateMap { pub fn insert(&mut self, val: T) -> Option where - T: 'static + Send + Sync, + T: 'static + AFConcurrent, { self .0 @@ -24,14 +23,14 @@ impl AFPluginStateMap { pub fn remove(&mut self) -> Option where - T: 'static + Send + Sync, + T: 'static + AFConcurrent, { self.0.remove(&TypeId::of::()).and_then(downcast_owned) } pub fn get(&self) -> Option<&T> where - T: 'static + Send + Sync, + T: 'static, { self .0 @@ -41,7 +40,7 @@ impl AFPluginStateMap { pub fn get_mut(&mut self) -> Option<&mut T> where - T: 'static + Send + Sync, + T: 'static + AFConcurrent, { self .0 @@ -51,7 +50,7 @@ impl AFPluginStateMap { pub fn contains(&self) -> bool where - T: 'static + Send + Sync, + T: 'static + AFConcurrent, { self.0.contains_key(&TypeId::of::()) } @@ -60,7 +59,3 @@ impl AFPluginStateMap { self.0.extend(other.0); } } - -fn downcast_owned(boxed: Box) -> Option { - boxed.downcast().ok().map(|boxed| *boxed) -} diff --git a/frontend/rust-lib/lib-dispatch/src/module/data.rs b/frontend/rust-lib/lib-dispatch/src/module/data.rs index 809954668893..1d7129927462 100644 --- a/frontend/rust-lib/lib-dispatch/src/module/data.rs +++ b/frontend/rust-lib/lib-dispatch/src/module/data.rs @@ -1,15 +1,17 @@ +use std::{any::type_name, ops::Deref, sync::Arc}; + +use crate::prelude::AFConcurrent; use crate::{ errors::{DispatchError, InternalError}, request::{payload::Payload, AFPluginEventRequest, FromAFPluginRequest}, util::ready::{ready, Ready}, }; -use std::{any::type_name, ops::Deref, sync::Arc}; -pub struct AFPluginState(Arc); +pub struct AFPluginState(Arc); impl AFPluginState where - T: Send + Sync, + T: AFConcurrent, { pub fn new(data: T) -> Self { AFPluginState(Arc::new(data)) @@ -22,7 +24,7 @@ where impl Deref for AFPluginState where - T: ?Sized + Send + Sync, + T: ?Sized + AFConcurrent, { type Target = Arc; @@ -33,7 +35,7 @@ where impl Clone for AFPluginState where - T: ?Sized + Send + Sync, + T: ?Sized + AFConcurrent, { fn clone(&self) -> AFPluginState { AFPluginState(self.0.clone()) @@ -42,7 +44,7 @@ where impl From> for AFPluginState where - T: ?Sized + Send + Sync, + T: ?Sized + AFConcurrent, { fn from(arc: Arc) -> Self { AFPluginState(arc) @@ -51,7 +53,7 @@ where impl FromAFPluginRequest for AFPluginState where - T: ?Sized + Send + Sync + 'static, + T: ?Sized + AFConcurrent + 'static, { type Error = DispatchError; type Future = Ready>; @@ -59,7 +61,7 @@ where #[inline] fn from_request(req: &AFPluginEventRequest, _: &mut Payload) -> Self::Future { if let Some(state) = req.get_state::>() { - ready(Ok(state.clone())) + ready(Ok(state)) } else { let msg = format!( "Failed to get the plugin state of type: {}", diff --git a/frontend/rust-lib/lib-dispatch/src/module/mod.rs b/frontend/rust-lib/lib-dispatch/src/module/mod.rs index 7c8d1a344068..9527a890b353 100644 --- a/frontend/rust-lib/lib-dispatch/src/module/mod.rs +++ b/frontend/rust-lib/lib-dispatch/src/module/mod.rs @@ -1,4 +1,5 @@ #![allow(clippy::module_inception)] + pub use container::*; pub use data::*; pub use module::*; diff --git a/frontend/rust-lib/lib-dispatch/src/module/module.rs b/frontend/rust-lib/lib-dispatch/src/module/module.rs index 09c72fe245a1..0eb162b51526 100644 --- a/frontend/rust-lib/lib-dispatch/src/module/module.rs +++ b/frontend/rust-lib/lib-dispatch/src/module/module.rs @@ -9,15 +9,15 @@ use std::{ task::{Context, Poll}, }; -use futures_core::future::BoxFuture; use futures_core::ready; use nanoid::nanoid; use pin_project::pin_project; +use crate::dispatcher::AFConcurrent; +use crate::prelude::{AFBoxFuture, AFStateMap}; use crate::service::AFPluginHandler; use crate::{ errors::{DispatchError, InternalError}, - module::{container::AFPluginStateMap, AFPluginState}, request::{payload::Payload, AFPluginEventRequest, FromAFPluginRequest}, response::{AFPluginEventResponse, AFPluginResponder}, service::{ @@ -58,7 +58,7 @@ pub struct AFPlugin { pub name: String, /// a list of `AFPluginState` that the plugin registers. The state can be read by the plugin's handler. - states: Arc, + states: AFStateMap, /// Contains a list of factories that are used to generate the services used to handle the passed-in /// `ServiceRequest`. @@ -72,7 +72,7 @@ impl std::default::Default for AFPlugin { fn default() -> Self { Self { name: "".to_owned(), - states: Arc::new(AFPluginStateMap::new()), + states: Default::default(), event_service_factory: Arc::new(HashMap::new()), } } @@ -88,11 +88,10 @@ impl AFPlugin { self } - pub fn state(mut self, data: D) -> Self { + pub fn state(mut self, data: D) -> Self { Arc::get_mut(&mut self.states) .unwrap() - .insert(AFPluginState::new(data)); - + .insert(crate::module::AFPluginState::new(data)); self } @@ -100,9 +99,9 @@ impl AFPlugin { pub fn event(mut self, event: E, handler: H) -> Self where H: AFPluginHandler, - T: FromAFPluginRequest + 'static + Send + Sync, - ::Future: Sync + Send, - R: Future + 'static + Send + Sync, + T: FromAFPluginRequest + 'static + AFConcurrent, + ::Future: AFConcurrent, + R: Future + AFConcurrent + 'static, R::Output: AFPluginResponder + 'static, E: Eq + Hash + Debug + Clone + Display, { @@ -169,7 +168,7 @@ impl AFPluginServiceFactory for AFPlugin { type Error = DispatchError; type Service = BoxService; type Context = (); - type Future = BoxFuture<'static, Result>; + type Future = AFBoxFuture<'static, Result>; fn new_service(&self, _cfg: Self::Context) -> Self::Future { let services = self.event_service_factory.clone(); @@ -185,13 +184,14 @@ pub struct AFPluginService { services: Arc< HashMap>, >, - states: Arc, + states: AFStateMap, } impl Service for AFPluginService { type Response = AFPluginEventResponse; type Error = DispatchError; - type Future = BoxFuture<'static, Result>; + + type Future = AFBoxFuture<'static, Result>; fn call(&self, request: AFPluginRequest) -> Self::Future { let AFPluginRequest { id, event, payload } = request; @@ -224,7 +224,7 @@ impl Service for AFPluginService { #[pin_project] pub struct AFPluginServiceFuture { #[pin] - fut: BoxFuture<'static, Result>, + fut: AFBoxFuture<'static, Result>, } impl Future for AFPluginServiceFuture { diff --git a/frontend/rust-lib/lib-dispatch/src/request/payload.rs b/frontend/rust-lib/lib-dispatch/src/request/payload.rs index c371537f3674..6fc2a98e992b 100644 --- a/frontend/rust-lib/lib-dispatch/src/request/payload.rs +++ b/frontend/rust-lib/lib-dispatch/src/request/payload.rs @@ -1,9 +1,7 @@ -use bytes::Bytes; use std::{fmt, fmt::Formatter}; -pub enum PayloadError {} +use bytes::Bytes; -// TODO: support stream data #[derive(Clone)] #[cfg_attr(feature = "use_serde", derive(serde::Serialize))] pub enum Payload { diff --git a/frontend/rust-lib/lib-dispatch/src/request/request.rs b/frontend/rust-lib/lib-dispatch/src/request/request.rs index 20af0bbc02c6..45fb6a7d0029 100644 --- a/frontend/rust-lib/lib-dispatch/src/request/request.rs +++ b/frontend/rust-lib/lib-dispatch/src/request/request.rs @@ -1,19 +1,20 @@ use std::future::Future; +use std::{ + fmt::Debug, + pin::Pin, + task::{Context, Poll}, +}; + +use derivative::*; +use futures_core::ready; +use crate::prelude::{AFConcurrent, AFStateMap}; use crate::{ errors::{DispatchError, InternalError}, - module::{AFPluginEvent, AFPluginStateMap}, + module::AFPluginEvent, request::payload::Payload, util::ready::{ready, Ready}, }; -use derivative::*; -use futures_core::ready; -use std::{ - fmt::Debug, - pin::Pin, - sync::Arc, - task::{Context, Poll}, -}; #[derive(Clone, Debug, Derivative)] pub struct AFPluginEventRequest { @@ -21,27 +22,27 @@ pub struct AFPluginEventRequest { pub(crate) id: String, pub(crate) event: AFPluginEvent, #[derivative(Debug = "ignore")] - pub(crate) states: Arc, + pub(crate) states: AFStateMap, } impl AFPluginEventRequest { - pub fn new(id: String, event: E, module_data: Arc) -> AFPluginEventRequest + pub fn new(id: String, event: E, states: AFStateMap) -> AFPluginEventRequest where E: Into, { Self { id, event: event.into(), - states: module_data, + states, } } - pub fn get_state(&self) -> Option<&T> + pub fn get_state(&self) -> Option where - T: Send + Sync, + T: AFConcurrent + 'static + Clone, { if let Some(data) = self.states.get::() { - return Some(data); + return Some(data.clone()); } None diff --git a/frontend/rust-lib/lib-dispatch/src/runtime.rs b/frontend/rust-lib/lib-dispatch/src/runtime.rs index 656612b359cb..691c86225009 100644 --- a/frontend/rust-lib/lib-dispatch/src/runtime.rs +++ b/frontend/rust-lib/lib-dispatch/src/runtime.rs @@ -1,24 +1,117 @@ -use std::{io, thread}; +use std::fmt::{Display, Formatter}; +use std::future::Future; +use std::io; + use tokio::runtime; +use tokio::runtime::Runtime; +use tokio::task::JoinHandle; + +pub struct AFPluginRuntime { + inner: Runtime, + #[cfg(feature = "single_thread")] + local: tokio::task::LocalSet, +} + +impl Display for AFPluginRuntime { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if cfg!(feature = "single_thread") { + write!(f, "Runtime(single_thread)") + } else { + write!(f, "Runtime(multi_thread)") + } + } +} + +impl AFPluginRuntime { + pub fn new() -> io::Result { + let inner = default_tokio_runtime()?; + Ok(Self { + inner, + #[cfg(feature = "single_thread")] + local: tokio::task::LocalSet::new(), + }) + } + + #[cfg(feature = "single_thread")] + #[track_caller] + pub fn spawn(&self, future: F) -> JoinHandle + where + F: Future + 'static, + { + self.local.spawn_local(future) + } + + #[cfg(not(feature = "single_thread"))] + #[track_caller] + pub fn spawn(&self, future: F) -> JoinHandle + where + F: Future + Send + 'static, + ::Output: Send + 'static, + { + self.inner.spawn(future) + } -pub type AFPluginRuntime = tokio::runtime::Runtime; + #[cfg(feature = "single_thread")] + pub async fn run_until(&self, future: F) -> F::Output + where + F: Future, + { + self.local.run_until(future).await + } + + #[cfg(not(feature = "single_thread"))] + pub async fn run_until(&self, future: F) -> F::Output + where + F: Future, + { + future.await + } + + #[cfg(feature = "single_thread")] + #[track_caller] + pub fn block_on(&self, f: F) -> F::Output + where + F: Future, + { + self.local.block_on(&self.inner, f) + } + + #[cfg(not(feature = "single_thread"))] + #[track_caller] + pub fn block_on(&self, f: F) -> F::Output + where + F: Future, + { + self.inner.block_on(f) + } +} + +#[cfg(feature = "single_thread")] +pub fn default_tokio_runtime() -> io::Result { + runtime::Builder::new_current_thread() + .thread_name("dispatch-rt-st") + .enable_io() + .enable_time() + .build() +} -pub fn tokio_default_runtime() -> io::Result { +#[cfg(not(feature = "single_thread"))] +pub fn default_tokio_runtime() -> io::Result { runtime::Builder::new_multi_thread() - .thread_name("dispatch-rt") + .thread_name("dispatch-rt-mt") .enable_io() .enable_time() .on_thread_start(move || { tracing::trace!( "{:?} thread started: thread_id= {}", - thread::current(), + std::thread::current(), thread_id::get() ); }) .on_thread_stop(move || { tracing::trace!( "{:?} thread stopping: thread_id= {}", - thread::current(), + std::thread::current(), thread_id::get(), ); }) diff --git a/frontend/rust-lib/lib-dispatch/src/service/boxed.rs b/frontend/rust-lib/lib-dispatch/src/service/boxed.rs index 6d2a72e8439b..76780e1d30fe 100644 --- a/frontend/rust-lib/lib-dispatch/src/service/boxed.rs +++ b/frontend/rust-lib/lib-dispatch/src/service/boxed.rs @@ -1,21 +1,33 @@ +use crate::prelude::{AFBoxFuture, AFConcurrent}; use crate::service::{AFPluginServiceFactory, Service}; -use futures_core::future::BoxFuture; pub fn factory(factory: SF) -> BoxServiceFactory where - SF: AFPluginServiceFactory + 'static + Sync + Send, + SF: AFPluginServiceFactory + 'static + AFConcurrent, Req: 'static, SF::Response: 'static, SF::Service: 'static, SF::Future: 'static, - SF::Error: 'static + Send + Sync, - >::Service: Sync + Send, - <>::Service as Service>::Future: Send + Sync, - >::Future: Send + Sync, + SF::Error: 'static, + >::Service: AFConcurrent, + <>::Service as Service>::Future: AFConcurrent, + >::Future: AFConcurrent, { BoxServiceFactory(Box::new(FactoryWrapper(factory))) } +#[cfg(feature = "single_thread")] +type Inner = Box< + dyn AFPluginServiceFactory< + Req, + Context = Cfg, + Response = Res, + Error = Err, + Service = BoxService, + Future = AFBoxFuture<'static, Result, Err>>, + >, +>; +#[cfg(not(feature = "single_thread"))] type Inner = Box< dyn AFPluginServiceFactory< Req, @@ -23,9 +35,9 @@ type Inner = Box< Response = Res, Error = Err, Service = BoxService, - Future = BoxFuture<'static, Result, Err>>, - > + Sync - + Send, + Future = AFBoxFuture<'static, Result, Err>>, + > + Send + + Sync, >; pub struct BoxServiceFactory(Inner); @@ -39,15 +51,21 @@ where type Error = Err; type Service = BoxService; type Context = Cfg; - type Future = BoxFuture<'static, Result>; + type Future = AFBoxFuture<'static, Result>; fn new_service(&self, cfg: Cfg) -> Self::Future { self.0.new_service(cfg) } } +#[cfg(feature = "single_thread")] +pub type BoxService = Box< + dyn Service>>, +>; + +#[cfg(not(feature = "single_thread"))] pub type BoxService = Box< - dyn Service>> + dyn Service>> + Sync + Send, >; @@ -88,11 +106,11 @@ impl ServiceWrapper { impl Service for ServiceWrapper where S: Service, - S::Future: 'static + Send + Sync, + S::Future: 'static + AFConcurrent, { type Response = Res; type Error = Err; - type Future = BoxFuture<'static, Result>; + type Future = AFBoxFuture<'static, Result>; fn call(&self, req: Req) -> Self::Future { Box::pin(self.inner.call(req)) @@ -108,15 +126,15 @@ where Err: 'static, SF: AFPluginServiceFactory, SF::Future: 'static, - SF::Service: 'static + Send + Sync, - <>::Service as Service>::Future: Send + Sync + 'static, - >::Future: Send + Sync, + SF::Service: 'static + AFConcurrent, + <>::Service as Service>::Future: AFConcurrent + 'static, + >::Future: AFConcurrent, { type Response = Res; type Error = Err; type Service = BoxService; type Context = Cfg; - type Future = BoxFuture<'static, Result>; + type Future = AFBoxFuture<'static, Result>; fn new_service(&self, cfg: Cfg) -> Self::Future { let f = self.0.new_service(cfg); diff --git a/frontend/rust-lib/lib-dispatch/src/service/handler.rs b/frontend/rust-lib/lib-dispatch/src/service/handler.rs index c55d4d0b166e..d231ed27f17f 100644 --- a/frontend/rust-lib/lib-dispatch/src/service/handler.rs +++ b/frontend/rust-lib/lib-dispatch/src/service/handler.rs @@ -8,18 +8,19 @@ use std::{ use futures_core::ready; use pin_project::pin_project; +use crate::dispatcher::AFConcurrent; use crate::{ errors::DispatchError, - request::{payload::Payload, AFPluginEventRequest, FromAFPluginRequest}, + request::{AFPluginEventRequest, FromAFPluginRequest}, response::{AFPluginEventResponse, AFPluginResponder}, service::{AFPluginServiceFactory, Service, ServiceRequest, ServiceResponse}, util::ready::*, }; /// A closure that is run every time for the specified plugin event -pub trait AFPluginHandler: Clone + 'static + Sync + Send +pub trait AFPluginHandler: Clone + AFConcurrent + 'static where - R: Future + Send + Sync, + R: Future + AFConcurrent, R::Output: AFPluginResponder, { fn call(&self, param: T) -> R; @@ -29,7 +30,7 @@ pub struct AFPluginHandlerService where H: AFPluginHandler, T: FromAFPluginRequest, - R: Future + Sync + Send, + R: Future + AFConcurrent, R::Output: AFPluginResponder, { handler: H, @@ -40,7 +41,7 @@ impl AFPluginHandlerService where H: AFPluginHandler, T: FromAFPluginRequest, - R: Future + Sync + Send, + R: Future + AFConcurrent, R::Output: AFPluginResponder, { pub fn new(handler: H) -> Self { @@ -55,7 +56,7 @@ impl Clone for AFPluginHandlerService where H: AFPluginHandler, T: FromAFPluginRequest, - R: Future + Sync + Send, + R: Future + AFConcurrent, R::Output: AFPluginResponder, { fn clone(&self) -> Self { @@ -70,7 +71,7 @@ impl AFPluginServiceFactory for AFPluginHandlerService< where F: AFPluginHandler, T: FromAFPluginRequest, - R: Future + Send + Sync, + R: Future + AFConcurrent, R::Output: AFPluginResponder, { type Response = ServiceResponse; @@ -88,7 +89,7 @@ impl Service for AFPluginHandlerService where H: AFPluginHandler, T: FromAFPluginRequest, - R: Future + Sync + Send, + R: Future + AFConcurrent, R::Output: AFPluginResponder, { type Response = ServiceResponse; @@ -107,7 +108,7 @@ pub enum HandlerServiceFuture where H: AFPluginHandler, T: FromAFPluginRequest, - R: Future + Sync + Send, + R: Future + AFConcurrent, R::Output: AFPluginResponder, { Extract(#[pin] T::Future, Option, H), @@ -118,7 +119,7 @@ impl Future for HandlerServiceFuture where F: AFPluginHandler, T: FromAFPluginRequest, - R: Future + Sync + Send, + R: Future + AFConcurrent, R::Output: AFPluginResponder, { type Output = Result; @@ -154,8 +155,8 @@ where macro_rules! factory_tuple ({ $($param:ident)* } => { impl AFPluginHandler<($($param,)*), Res> for Func - where Func: Fn($($param),*) -> Res + Clone + 'static + Sync + Send, - Res: Future + Sync + Send, + where Func: Fn($($param),*) -> Res + Clone + 'static + AFConcurrent, + Res: Future + AFConcurrent, Res::Output: AFPluginResponder, { #[allow(non_snake_case)] @@ -181,7 +182,7 @@ macro_rules! tuple_from_req ({$tuple_type:ident, $(($n:tt, $T:ident)),+} => { type Error = DispatchError; type Future = $tuple_type<$($T),+>; - fn from_request(req: &AFPluginEventRequest, payload: &mut Payload) -> Self::Future { + fn from_request(req: &AFPluginEventRequest, payload: &mut crate::prelude::Payload) -> Self::Future { $tuple_type { items: <($(Option<$T>,)+)>::default(), futs: FromRequestFutures($($T::from_request(req, payload),)+), diff --git a/frontend/rust-lib/lib-dispatch/tests/api/module.rs b/frontend/rust-lib/lib-dispatch/tests/api/module.rs index cf68fa583b48..4e105a8257e6 100644 --- a/frontend/rust-lib/lib-dispatch/tests/api/module.rs +++ b/frontend/rust-lib/lib-dispatch/tests/api/module.rs @@ -1,7 +1,8 @@ -use lib_dispatch::prelude::*; -use lib_dispatch::runtime::tokio_default_runtime; use std::sync::Arc; +use lib_dispatch::prelude::*; +use lib_dispatch::runtime::AFPluginRuntime; + pub async fn hello() -> String { "say hello".to_string() } @@ -9,7 +10,7 @@ pub async fn hello() -> String { #[tokio::test] async fn test() { let event = "1"; - let runtime = tokio_default_runtime().unwrap(); + let runtime = Arc::new(AFPluginRuntime::new().unwrap()); let dispatch = Arc::new(AFPluginDispatcher::construct(runtime, || { vec![AFPlugin::new().event(event, hello)] })); diff --git a/frontend/rust-lib/lib-log/Cargo.toml b/frontend/rust-lib/lib-log/Cargo.toml index a02dcbed73cd..483a4f403e21 100644 --- a/frontend/rust-lib/lib-log/Cargo.toml +++ b/frontend/rust-lib/lib-log/Cargo.toml @@ -7,10 +7,10 @@ edition = "2018" [dependencies] -tracing-log = { version = "0.1.3"} -tracing-subscriber = { version = "0.2.25", features = ["registry", "env-filter", "ansi", "json"] } -tracing-bunyan-formatter = "0.2.6" -tracing-appender = "0.1" +tracing-log = { version = "0.2"} +tracing-subscriber = { version = "0.3.17", features = ["registry", "env-filter", "ansi", "json"] } +tracing-bunyan-formatter = "0.3.9" +tracing-appender = "0.2.2" tracing-core = "0.1" tracing = { version = "0.1", features = ["log"] } log = "0.4.17" diff --git a/frontend/rust-lib/lib-log/src/layer.rs b/frontend/rust-lib/lib-log/src/layer.rs index 870223c0cf0c..b8db7aeb54ca 100644 --- a/frontend/rust-lib/lib-log/src/layer.rs +++ b/frontend/rust-lib/lib-log/src/layer.rs @@ -4,7 +4,7 @@ use serde::ser::{SerializeMap, Serializer}; use serde_json::Value; use tracing::{Event, Id, Subscriber}; use tracing_bunyan_formatter::JsonStorage; -use tracing_core::{metadata::Level, span::Attributes}; +use tracing_core::metadata::Level; use tracing_subscriber::{fmt::MakeWriter, layer::Context, registry::SpanRef, Layer}; const LEVEL: &str = "level"; @@ -17,17 +17,22 @@ const LOG_TARGET_PATH: &str = "log.target"; const RESERVED_FIELDS: [&str; 3] = [LEVEL, TIME, MESSAGE]; const IGNORE_FIELDS: [&str; 2] = [LOG_MODULE_PATH, LOG_TARGET_PATH]; -pub struct FlowyFormattingLayer { +pub struct FlowyFormattingLayer<'a, W: MakeWriter<'static> + 'static> { make_writer: W, with_target: bool, + phantom: std::marker::PhantomData<&'a ()>, } -impl FlowyFormattingLayer { +impl<'a, W> FlowyFormattingLayer<'a, W> +where + W: for<'writer> MakeWriter<'writer> + 'static, +{ #[allow(dead_code)] pub fn new(make_writer: W) -> Self { Self { make_writer, with_target: false, + phantom: std::marker::PhantomData, } } @@ -43,9 +48,9 @@ impl FlowyFormattingLayer { Ok(()) } - fn serialize_span tracing_subscriber::registry::LookupSpan<'a>>( + fn serialize_span tracing_subscriber::registry::LookupSpan<'b>>( &self, - span: &SpanRef, + span: &SpanRef<'a, S>, ty: Type, ctx: &Context<'_, S>, ) -> Result, std::io::Error> { @@ -86,6 +91,7 @@ impl FlowyFormattingLayer { /// The type of record we are dealing with: entering a span, exiting a span, an /// event. +#[allow(dead_code)] #[derive(Clone, Debug)] pub enum Type { EnterSpan, @@ -104,8 +110,8 @@ impl fmt::Display for Type { } } -fn format_span_context tracing_subscriber::registry::LookupSpan<'a>>( - span: &SpanRef, +fn format_span_context<'b, S: Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>>( + span: &SpanRef<'b, S>, ty: Type, context: &Context<'_, S>, ) -> String { @@ -153,10 +159,10 @@ fn format_event_message tracing_subscriber::registry::Lo message } -impl Layer for FlowyFormattingLayer +impl Layer for FlowyFormattingLayer<'static, W> where S: Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>, - W: MakeWriter + 'static, + W: for<'writer> MakeWriter<'writer> + 'static, { fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) { // Events do not necessarily happen in the context of a span, hence @@ -221,13 +227,6 @@ where } } - fn new_span(&self, _attrs: &Attributes, id: &Id, ctx: Context<'_, S>) { - let span = ctx.span(id).expect("Span not found, this is a bug"); - if let Ok(serialized) = self.serialize_span(&span, Type::EnterSpan, &ctx) { - let _ = self.emit(serialized); - } - } - fn on_close(&self, id: Id, ctx: Context<'_, S>) { let span = ctx.span(&id).expect("Span not found, this is a bug"); if let Ok(serialized) = self.serialize_span(&span, Type::ExitSpan, &ctx) { diff --git a/frontend/rust-lib/lib-log/src/lib.rs b/frontend/rust-lib/lib-log/src/lib.rs index 86203e6378ef..69271ebec1dd 100644 --- a/frontend/rust-lib/lib-log/src/lib.rs +++ b/frontend/rust-lib/lib-log/src/lib.rs @@ -1,16 +1,15 @@ use std::sync::RwLock; use lazy_static::lazy_static; -use log::LevelFilter; use tracing::subscriber::set_global_default; use tracing_appender::{non_blocking::WorkerGuard, rolling::RollingFileAppender}; use tracing_bunyan_formatter::JsonStorageLayer; -use tracing_log::LogTracer; use tracing_subscriber::{layer::SubscriberExt, EnvFilter}; use crate::layer::FlowyFormattingLayer; mod layer; + lazy_static! { static ref LOG_GUARD: RwLock> = RwLock::new(None); } @@ -47,48 +46,17 @@ impl Builder { .with_ansi(true) .with_target(true) .with_max_level(tracing::Level::TRACE) + .with_thread_ids(false) + .with_file(false) .with_writer(std::io::stderr) - .with_thread_ids(true) - .json() - .with_current_span(true) - .with_span_list(true) - .compact() + .pretty() + .with_env_filter(env_filter) .finish() - .with(env_filter) .with(JsonStorageLayer) - .with(FlowyFormattingLayer::new(std::io::stdout)) .with(FlowyFormattingLayer::new(non_blocking)); set_global_default(subscriber).map_err(|e| format!("{:?}", e))?; - LogTracer::builder() - .with_max_level(LevelFilter::Trace) - .init() - .map_err(|e| format!("{:?}", e))?; - *LOG_GUARD.write().unwrap() = Some(guard); Ok(()) } } - -#[cfg(test)] -mod tests { - use super::*; - - // run cargo test --features="use_bunyan" or cargo test - #[test] - fn test_log() { - Builder::new("flowy", ".") - .env_filter("debug") - .build() - .unwrap(); - tracing::info!("😁 tracing::info call"); - log::debug!("😁 log::debug call"); - - say("hello world"); - } - - #[tracing::instrument(level = "trace", name = "say")] - fn say(s: &str) { - tracing::info!("{}", s); - } -} From dd9b1fb78f102dac8c625d7b5302d08b0053302d Mon Sep 17 00:00:00 2001 From: "Kilu.He" <108015703+qinluhe@users.noreply.github.com> Date: Mon, 30 Oct 2023 12:50:31 +0800 Subject: [PATCH 09/56] feat: support converting documents to JSON, HTML, or TEXT. (#3811) * feat: support converting documents to JSON, HTML, or TEXT * fix: modify the comment * fix: modify the comment --- .../document/async-actions/turn_to.ts | 8 +- .../src/document/document_event.rs | 16 + .../tests/document/local_test/test.rs | 33 ++ frontend/rust-lib/flowy-document2/Flowy.toml | 2 +- .../flowy-document2/src/event_handler.rs | 47 ++ .../rust-lib/flowy-document2/src/event_map.rs | 47 ++ .../flowy-document2/src/parser/constant.rs | 37 ++ .../src/parser/document_data_parser.rs | 180 +++++++ .../flowy-document2/src/parser/mod.rs | 4 + .../src/parser/parser_entities.rs | 481 ++++++++++++++++++ .../flowy-document2/src/parser/utils.rs | 167 ++++++ .../tests/assets/html/bulleted_list.html | 1 + .../tests/assets/html/callout.html | 6 + .../tests/assets/html/code.html | 5 + .../tests/assets/html/divider.html | 1 + .../tests/assets/html/heading.html | 1 + .../tests/assets/html/image.html | 1 + .../tests/assets/html/math_equation.html | 1 + .../tests/assets/html/numbered_list.html | 1 + .../tests/assets/html/paragraph.html | 6 + .../tests/assets/html/quote.html | 1 + .../tests/assets/html/todo_list.html | 1 + .../tests/assets/html/toggle_list.html | 1 + .../tests/assets/json/bulleted_list.json | 37 ++ .../tests/assets/json/callout.json | 32 ++ .../tests/assets/json/code.json | 14 + .../tests/assets/json/divider.json | 10 + .../tests/assets/json/heading.json | 39 ++ .../tests/assets/json/image.json | 15 + .../tests/assets/json/initial_document.json | 267 ++++++++++ .../tests/assets/json/math_equation.json | 9 + .../tests/assets/json/numbered_list.json | 37 ++ .../tests/assets/json/paragraph.json | 59 +++ .../tests/assets/json/quote.json | 25 + .../tests/assets/json/range_1.json | 100 ++++ .../tests/assets/json/range_2.json | 178 +++++++ .../tests/assets/json/todo_list.json | 39 ++ .../tests/assets/json/toggle_list.json | 25 + .../tests/assets/text/bulleted_list.txt | 3 + .../tests/assets/text/callout.txt | 6 + .../tests/assets/text/code.txt | 5 + .../tests/assets/text/divider.txt | 1 + .../tests/assets/text/heading.txt | 3 + .../tests/assets/text/image.txt | 1 + .../tests/assets/text/math_equation.txt | 1 + .../tests/assets/text/numbered_list.txt | 3 + .../tests/assets/text/paragraph.txt | 8 + .../tests/assets/text/quote.txt | 2 + .../tests/assets/text/todo_list.txt | 3 + .../tests/assets/text/toggle_list.txt | 3 + .../flowy-document2/tests/document/mod.rs | 2 +- .../tests/parser/document_data_parser_test.rs | 105 ++++ .../tests/parser/html_text/mod.rs | 2 + .../tests/parser/html_text/test.rs | 37 ++ .../tests/parser/html_text/utils.rs | 21 + .../flowy-document2/tests/parser/mod.rs | 2 + 56 files changed, 2136 insertions(+), 6 deletions(-) create mode 100644 frontend/rust-lib/flowy-document2/src/parser/constant.rs create mode 100644 frontend/rust-lib/flowy-document2/src/parser/document_data_parser.rs create mode 100644 frontend/rust-lib/flowy-document2/src/parser/parser_entities.rs create mode 100644 frontend/rust-lib/flowy-document2/src/parser/utils.rs create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/html/bulleted_list.html create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/html/callout.html create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/html/code.html create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/html/divider.html create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/html/heading.html create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/html/image.html create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/html/math_equation.html create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/html/numbered_list.html create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/html/paragraph.html create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/html/quote.html create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/html/todo_list.html create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/html/toggle_list.html create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/json/bulleted_list.json create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/json/callout.json create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/json/code.json create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/json/divider.json create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/json/heading.json create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/json/image.json create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/json/initial_document.json create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/json/math_equation.json create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/json/numbered_list.json create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/json/paragraph.json create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/json/quote.json create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/json/range_1.json create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/json/range_2.json create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/json/todo_list.json create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/json/toggle_list.json create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/text/bulleted_list.txt create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/text/callout.txt create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/text/code.txt create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/text/divider.txt create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/text/heading.txt create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/text/image.txt create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/text/math_equation.txt create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/text/numbered_list.txt create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/text/paragraph.txt create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/text/quote.txt create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/text/todo_list.txt create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/text/toggle_list.txt create mode 100644 frontend/rust-lib/flowy-document2/tests/parser/document_data_parser_test.rs create mode 100644 frontend/rust-lib/flowy-document2/tests/parser/html_text/mod.rs create mode 100644 frontend/rust-lib/flowy-document2/tests/parser/html_text/test.rs create mode 100644 frontend/rust-lib/flowy-document2/tests/parser/html_text/utils.rs diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/turn_to.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/turn_to.ts index bdfd1d4fde34..18d368727b08 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/turn_to.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/turn_to.ts @@ -36,7 +36,7 @@ export const turnToBlockThunk = createAsyncThunk( let caretId, caretIndex = caret?.index || 0; const deltaOperator = new BlockDeltaOperator(documentState, controller); - let delta = deltaOperator.getDeltaWithBlockId(node.id); + let delta = deltaOperator.getDeltaWithBlockId(node.id) || new Delta([{ insert: '' }]); // insert new block after current block const insertActions = []; @@ -44,14 +44,14 @@ export const turnToBlockThunk = createAsyncThunk( delta = new Delta([{ insert: node.data.formula }]); } - if (delta && type === BlockType.EquationBlock) { + if (type === BlockType.EquationBlock) { data.formula = deltaOperator.getDeltaText(delta); const block = newBlock(type, parent.id, data); insertActions.push(controller.getInsertAction(block, node.id)); caretId = block.id; caretIndex = 0; - } else if (delta && type === BlockType.DividerBlock) { + } else if (type === BlockType.DividerBlock) { const block = newBlock(type, parent.id, data); insertActions.push(controller.getInsertAction(block, node.id)); @@ -68,7 +68,7 @@ export const turnToBlockThunk = createAsyncThunk( caretId = nodeId; caretIndex = 0; insertActions.push(...actions); - } else if (delta) { + } else { caretId = generateId(); const actions = deltaOperator.getNewTextLineActions({ diff --git a/frontend/rust-lib/event-integration/src/document/document_event.rs b/frontend/rust-lib/event-integration/src/document/document_event.rs index ab20d240bd39..2deb791064f4 100644 --- a/frontend/rust-lib/event-integration/src/document/document_event.rs +++ b/frontend/rust-lib/event-integration/src/document/document_event.rs @@ -4,6 +4,9 @@ use serde_json::Value; use flowy_document2::entities::*; use flowy_document2::event_map::DocumentEvent; +use flowy_document2::parser::parser_entities::{ + ConvertDocumentPayloadPB, ConvertDocumentResponsePB, +}; use flowy_folder2::entities::{CreateViewPayloadPB, ViewLayoutPB, ViewPB}; use flowy_folder2::event_map::FolderEvent; @@ -108,6 +111,19 @@ impl DocumentEventTest { .await; } + pub async fn convert_document( + &self, + payload: ConvertDocumentPayloadPB, + ) -> ConvertDocumentResponsePB { + let core = &self.inner; + EventBuilder::new(core.clone()) + .event(DocumentEvent::ConvertDocument) + .payload(payload) + .async_send() + .await + .parse::() + } + pub async fn create_text(&self, payload: TextDeltaPayloadPB) { let core = &self.inner; EventBuilder::new(core.clone()) diff --git a/frontend/rust-lib/event-integration/tests/document/local_test/test.rs b/frontend/rust-lib/event-integration/tests/document/local_test/test.rs index 0016a9118cd4..b03320f247b8 100644 --- a/frontend/rust-lib/event-integration/tests/document/local_test/test.rs +++ b/frontend/rust-lib/event-integration/tests/document/local_test/test.rs @@ -2,6 +2,7 @@ use collab_document::blocks::json_str_to_hashmap; use event_integration::document::document_event::DocumentEventTest; use event_integration::document::utils::*; use flowy_document2::entities::*; +use flowy_document2::parser::parser_entities::{ConvertDocumentPayloadPB, ExportTypePB}; use serde_json::{json, Value}; use std::collections::HashMap; @@ -120,3 +121,35 @@ async fn apply_text_delta_test() { json!([{ "insert": "Hello! World" }]).to_string() ); } + +macro_rules! generate_convert_document_test_cases { + ($($json:ident, $text:ident, $html:ident),*) => { + [ + $((ExportTypePB { json: $json, text: $text, html: $html }, ($json, $text, $html))),* + ] + }; +} + +#[tokio::test] +async fn convert_document_test() { + let test = DocumentEventTest::new().await; + let view = test.create_document().await; + + let test_cases = generate_convert_document_test_cases! { + true, true, true, + false, true, true, + false, false, false + }; + + for (export_types, (json_assert, text_assert, html_assert)) in test_cases.iter() { + let copy_payload = ConvertDocumentPayloadPB { + document_id: view.id.to_string(), + range: None, + export_types: export_types.clone(), + }; + let result = test.convert_document(copy_payload).await; + assert_eq!(result.json.is_some(), *json_assert); + assert_eq!(result.text.is_some(), *text_assert); + assert_eq!(result.html.is_some(), *html_assert); + } +} diff --git a/frontend/rust-lib/flowy-document2/Flowy.toml b/frontend/rust-lib/flowy-document2/Flowy.toml index a48035cb147c..6ef51c220dd8 100644 --- a/frontend/rust-lib/flowy-document2/Flowy.toml +++ b/frontend/rust-lib/flowy-document2/Flowy.toml @@ -1,3 +1,3 @@ # Check out the FlowyConfig (located in flowy_toml.rs) for more details. -proto_input = ["src/event_map.rs", "src/entities.rs", "src/notification.rs"] +proto_input = ["src/event_map.rs", "src/entities.rs", "src/notification.rs", "src/parser/parser_entities.rs"] event_files = ["src/event_map.rs"] \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/src/event_handler.rs b/frontend/rust-lib/flowy-document2/src/event_handler.rs index c3b10859d03d..a576caf697eb 100644 --- a/frontend/rust-lib/flowy-document2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-document2/src/event_handler.rs @@ -15,6 +15,11 @@ use flowy_error::{FlowyError, FlowyResult}; use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; use crate::entities::*; +use crate::parser::document_data_parser::DocumentDataParser; +use crate::parser::parser_entities::{ + ConvertDocumentParams, ConvertDocumentPayloadPB, ConvertDocumentResponsePB, +}; + use crate::{manager::DocumentManager, parser::json::parser::JsonToDocumentParser}; fn upgrade_document( @@ -303,3 +308,45 @@ impl From<(&Vec, bool)> for DocEventPB { } } } + +/** +* Handler for converting a document to a JSON string, HTML string, or plain text string. + +* @param data: AFPluginData<[ConvertDocumentPayloadPB]> + +* @param manager: AFPluginState> + +* @return DataResult<[ConvertDocumentResponsePB], FlowyError> + */ +pub async fn convert_document( + data: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let manager = upgrade_document(manager)?; + let params: ConvertDocumentParams = data.into_inner().try_into()?; + + let document = manager.get_document(¶ms.document_id).await?; + let document_data = document.lock().get_document_data()?; + let parser = DocumentDataParser::new(Arc::new(document_data), params.range); + + if !params.export_types.any_enabled() { + return data_result_ok(ConvertDocumentResponsePB::default()); + } + + let root = &parser.to_json(); + + data_result_ok(ConvertDocumentResponsePB { + json: params + .export_types + .json + .then(|| serde_json::to_string(root).unwrap_or_default()), + html: params + .export_types + .html + .then(|| parser.to_html_with_json(root)), + text: params + .export_types + .text + .then(|| parser.to_text_with_json(root)), + }) +} diff --git a/frontend/rust-lib/flowy-document2/src/event_map.rs b/frontend/rust-lib/flowy-document2/src/event_map.rs index c9ff9569d6d2..e7c4dcd13ffb 100644 --- a/frontend/rust-lib/flowy-document2/src/event_map.rs +++ b/frontend/rust-lib/flowy-document2/src/event_map.rs @@ -5,6 +5,7 @@ use strum_macros::Display; use flowy_derive::{Flowy_Event, ProtoBuf_Enum}; use lib_dispatch::prelude::AFPlugin; +use crate::event_handler::convert_document; use crate::event_handler::get_snapshot_handler; use crate::{event_handler::*, manager::DocumentManager}; @@ -27,6 +28,7 @@ pub fn init(document_manager: Weak) -> AFPlugin { .event(DocumentEvent::GetDocumentSnapshots, get_snapshot_handler) .event(DocumentEvent::CreateText, create_text_handler) .event(DocumentEvent::ApplyTextDeltaEvent, apply_text_delta_handler) + .event(DocumentEvent::ConvertDocument, convert_document) } #[derive(Debug, Clone, PartialEq, Eq, Hash, Display, ProtoBuf_Enum, Flowy_Event)] @@ -76,4 +78,49 @@ pub enum DocumentEvent { #[event(input = "TextDeltaPayloadPB")] ApplyTextDeltaEvent = 11, + + /// Handler for converting a document to a JSON string, HTML string, or plain text string. + /// + /// ConvertDocumentPayloadPB is the input of this event. + /// ConvertDocumentResponsePB is the output of this event. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```txt + /// // document: [{ "block_id": "1", "type": "paragraph", "data": {"delta": [{ "insert": "Hello World!" }] } }, { "block_id": "2", "type": "paragraph", "data": {"delta": [{ "insert": "Hello World!" }] } + /// let test = DocumentEventTest::new().await; + /// let view = test.create_document().await; + /// let payload = ConvertDocumentPayloadPB { + /// document_id: view.id, + /// range: Some(RangePB { + /// start: SelectionPB { + /// block_id: "1".to_string(), + /// index: 0, + /// length: 5, + /// }, + /// end: SelectionPB { + /// block_id: "2".to_string(), + /// index: 5, + /// length: 7, + /// } + /// }), + /// export_types: ConvertTypePB { + /// json: true, + /// text: true, + /// html: true, + /// }, + /// }; + /// let result = test.convert_document(payload).await; + /// assert_eq!(result.json, Some("[{ \"block_id\": \"1\", \"type\": \"paragraph\", \"data\": {\"delta\": [{ \"insert\": \"Hello\" }] } }, { \"block_id\": \"2\", \"type\": \"paragraph\", \"data\": {\"delta\": [{ \"insert\": \" World!\" }] } }".to_string())); + /// assert_eq!(result.text, Some("Hello\n World!".to_string())); + /// assert_eq!(result.html, Some("

Hello

World!

".to_string())); + /// ``` + /// # + #[event( + input = "ConvertDocumentPayloadPB", + output = "ConvertDocumentResponsePB" + )] + ConvertDocument = 12, } diff --git a/frontend/rust-lib/flowy-document2/src/parser/constant.rs b/frontend/rust-lib/flowy-document2/src/parser/constant.rs new file mode 100644 index 000000000000..d5c4d56e6b74 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/src/parser/constant.rs @@ -0,0 +1,37 @@ +pub const DELTA: &str = "delta"; +pub const LEVEL: &str = "level"; +pub const NUMBER: &str = "number"; +pub const CHECKED: &str = "checked"; + +pub const COLLAPSED: &str = "collapsed"; +pub const LANGUAGE: &str = "language"; + +pub const ICON: &str = "icon"; +pub const WIDTH: &str = "width"; +pub const HEIGHT: &str = "height"; +pub const URL: &str = "url"; +pub const CAPTION: &str = "caption"; +pub const ALIGN: &str = "align"; + +pub const PAGE: &str = "page"; +pub const HEADING: &str = "heading"; +pub const PARAGRAPH: &str = "paragraph"; +pub const NUMBERED_LIST: &str = "numbered_list"; +pub const BULLETED_LIST: &str = "bulleted_list"; +pub const TODO_LIST: &str = "todo_list"; +pub const TOGGLE_LIST: &str = "toggle_list"; +pub const QUOTE: &str = "quote"; +pub const CALLOUT: &str = "callout"; +pub const IMAGE: &str = "image"; +pub const DIVIDER: &str = "divider"; +pub const MATH_EQUATION: &str = "math_equation"; +pub const BOLD: &str = "bold"; +pub const ITALIC: &str = "italic"; +pub const STRIKETHROUGH: &str = "strikethrough"; +pub const CODE: &str = "code"; +pub const UNDERLINE: &str = "underline"; +pub const FONT_COLOR: &str = "font_color"; +pub const BG_COLOR: &str = "bg_color"; +pub const HREF: &str = "href"; +pub const FORMULA: &str = "formula"; +pub const MENTION: &str = "mention"; diff --git a/frontend/rust-lib/flowy-document2/src/parser/document_data_parser.rs b/frontend/rust-lib/flowy-document2/src/parser/document_data_parser.rs new file mode 100644 index 000000000000..5339c7eff30e --- /dev/null +++ b/frontend/rust-lib/flowy-document2/src/parser/document_data_parser.rs @@ -0,0 +1,180 @@ +use crate::parser::parser_entities::{ConvertBlockToHtmlParams, NestedBlock, Range}; +use crate::parser::utils::{ + block_to_nested_json, get_delta_for_block, get_delta_for_selection, get_flat_block_ids, + ConvertBlockToJsonParams, +}; +use collab_document::blocks::DocumentData; +use std::collections::HashMap; +use std::sync::Arc; + +/// DocumentDataParser is a struct for parsing a document's data and converting it to JSON, HTML, or text. +pub struct DocumentDataParser { + /// The document data to parse. + pub document_data: Arc, + /// The range of the document data to parse. If the range is None, the entire document data will be parsed. + pub range: Option, +} + +impl DocumentDataParser { + pub fn new(document_data: Arc, range: Option) -> Self { + Self { + document_data, + range, + } + } + + /// Converts the JSON to an HTML representation. + pub fn to_html_with_json(&self, json: &Option) -> String { + let mut html = String::new(); + html.push_str(""); + if let Some(json) = json { + let params = ConvertBlockToHtmlParams { + prev_block_ty: None, + next_block_ty: None, + }; + html.push_str(json.convert_to_html(params).as_str()); + } + html + } + + /// Converts the JSON to plain text. + pub fn to_text_with_json(&self, json: &Option) -> String { + if let Some(json) = json { + json.convert_to_text() + } else { + String::new() + } + } + + /// Converts the document data to HTML. + pub fn to_html(&self) -> String { + let json = self.to_json(); + self.to_html_with_json(&json) + } + + /// Converts the document data to plain text. + pub fn to_text(&self) -> String { + let json = self.to_json(); + self.to_text_with_json(&json) + } + + /// Converts the document data to a nested JSON structure, considering the optional range. + pub fn to_json(&self) -> Option { + let root_id = &self.document_data.page_id; + // flatten the block id list. + let block_id_list = get_flat_block_ids(root_id, &self.document_data); + + // collect the block ids in the range. + let mut in_range_block_ids = self.collect_in_range_block_ids(&block_id_list); + // insert the root block id if it is not in the in-range block ids. + if !in_range_block_ids.contains(root_id) { + in_range_block_ids.push(root_id.to_string()); + } + + // build the parameters for converting the block to JSON with the in-range block ids. + let convert_params = self.build_convert_json_params(&in_range_block_ids); + // convert the root block to JSON. + let mut root = block_to_nested_json(root_id, &convert_params)?; + + // If the start block's parent is outside the in-range selection, we need to insert the start block. + if self.should_insert_start_block() { + self.insert_start_block_json(&mut root, &convert_params); + } + + Some(root) + } + + /// Collects the block ids in the range. + fn collect_in_range_block_ids(&self, block_id_list: &Vec) -> Vec { + if let Some(range) = &self.range { + // Find the positions of start and end block IDs in the list + let mut start_index = block_id_list + .iter() + .position(|id| id == &range.start.block_id) + .unwrap_or(0); + let mut end_index = block_id_list + .iter() + .position(|id| id == &range.end.block_id) + .unwrap_or(0); + + if start_index > end_index { + // Swap start and end if they are in reverse order + std::mem::swap(&mut start_index, &mut end_index); + } + + // Slice the block IDs based on the positions of start and end + block_id_list[start_index..=end_index].to_vec() + } else { + // If no range is specified, return the entire list + block_id_list.to_owned() + } + } + + /// Builds the parameters for converting the block to JSON. + /// ConvertBlockToJsonParams format: + /// { + /// blocks: HashMap>, // in-range blocks + /// relation_map: HashMap>>, // in-range blocks' children + /// delta_map: HashMap, // in-range blocks' delta + /// } + fn build_convert_json_params(&self, block_id_list: &[String]) -> ConvertBlockToJsonParams { + let mut delta_map = HashMap::new(); + let mut in_range_blocks = HashMap::new(); + let mut relation_map = HashMap::new(); + + for block_id in block_id_list { + if let Some(block) = self.document_data.blocks.get(block_id) { + // Insert the block into the in-range block map. + in_range_blocks.insert(block_id.to_string(), Arc::new(block.to_owned())); + + // If the block has children, insert the children into the relation map. + if let Some(children) = self.document_data.meta.children_map.get(&block.children) { + relation_map.insert(block_id.to_string(), Arc::new(children.to_owned())); + } + + let delta = match &self.range { + Some(range) if block_id == &range.start.block_id => { + get_delta_for_selection(&range.start, &self.document_data) + }, + Some(range) if block_id == &range.end.block_id => { + get_delta_for_selection(&range.end, &self.document_data) + }, + _ => get_delta_for_block(block_id, &self.document_data), + }; + + // If the delta exists, insert it into the delta map. + if let Some(delta) = delta { + delta_map.insert(block_id.to_string(), delta); + } + } + } + + ConvertBlockToJsonParams { + blocks: in_range_blocks, + relation_map, + delta_map, + } + } + + // Checks if the start block should be inserted whether the start block's parent is outside the in-range selection. + fn should_insert_start_block(&self) -> bool { + if let Some(range) = &self.range { + if let Some(start_block) = self.document_data.blocks.get(&range.start.block_id) { + return start_block.parent != self.document_data.page_id; + } + } + false + } + + // Inserts the start block JSON to the root JSON. + fn insert_start_block_json( + &self, + root: &mut NestedBlock, + convert_params: &ConvertBlockToJsonParams, + ) { + let start = &self.range.as_ref().unwrap().start; + if let Some(start_block_json) = block_to_nested_json(&start.block_id, convert_params) { + root.children.insert(0, start_block_json); + } + } +} diff --git a/frontend/rust-lib/flowy-document2/src/parser/mod.rs b/frontend/rust-lib/flowy-document2/src/parser/mod.rs index 22fdbb38c88f..0c040e6e5134 100644 --- a/frontend/rust-lib/flowy-document2/src/parser/mod.rs +++ b/frontend/rust-lib/flowy-document2/src/parser/mod.rs @@ -1 +1,5 @@ +pub mod constant; +pub mod document_data_parser; pub mod json; +pub mod parser_entities; +pub mod utils; diff --git a/frontend/rust-lib/flowy-document2/src/parser/parser_entities.rs b/frontend/rust-lib/flowy-document2/src/parser/parser_entities.rs new file mode 100644 index 000000000000..0fec927dcd11 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/src/parser/parser_entities.rs @@ -0,0 +1,481 @@ +use crate::parse::NotEmptyStr; +use crate::parser::constant::{ + BG_COLOR, BOLD, BULLETED_LIST, CALLOUT, CHECKED, CODE, DELTA, DIVIDER, FONT_COLOR, FORMULA, + HEADING, HREF, ICON, IMAGE, ITALIC, LANGUAGE, LEVEL, MATH_EQUATION, NUMBERED_LIST, PAGE, + PARAGRAPH, QUOTE, STRIKETHROUGH, TODO_LIST, TOGGLE_LIST, UNDERLINE, URL, +}; +use crate::parser::utils::{ + convert_insert_delta_from_json, convert_nested_block_children_to_html, delta_to_html, + delta_to_text, +}; +use flowy_derive::ProtoBuf; +use flowy_error::ErrorCode; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::HashMap; +use std::sync::Arc; + +#[derive(Default, ProtoBuf)] +pub struct SelectionPB { + #[pb(index = 1)] + pub block_id: String, + + #[pb(index = 2)] + pub index: u32, + + #[pb(index = 3)] + pub length: u32, +} + +#[derive(Default, ProtoBuf)] +pub struct RangePB { + #[pb(index = 1)] + pub start: SelectionPB, + + #[pb(index = 2)] + pub end: SelectionPB, +} + +/** +* ExportTypePB + * @field json: bool // export json data + * @field html: bool // export html data + * @field text: bool // export text data + */ +#[derive(Default, ProtoBuf, Debug, Clone)] +pub struct ExportTypePB { + #[pb(index = 1)] + pub json: bool, + + #[pb(index = 2)] + pub html: bool, + + #[pb(index = 3)] + pub text: bool, +} +/** +* ConvertDocumentPayloadPB + * @field document_id: String + * @file range: Option - optional // if range is None, copy the whole document + * @field export_types: [ExportTypePB] + */ +#[derive(Default, ProtoBuf)] +pub struct ConvertDocumentPayloadPB { + #[pb(index = 1)] + pub document_id: String, + + #[pb(index = 2, one_of)] + pub range: Option, + + #[pb(index = 3)] + pub export_types: ExportTypePB, +} + +#[derive(Default, ProtoBuf, Debug)] +pub struct ConvertDocumentResponsePB { + #[pb(index = 1, one_of)] + pub json: Option, + #[pb(index = 2, one_of)] + pub html: Option, + #[pb(index = 3, one_of)] + pub text: Option, +} + +pub struct Selection { + pub block_id: String, + pub index: u32, + pub length: u32, +} + +pub struct Range { + pub start: Selection, + pub end: Selection, +} + +pub struct ExportType { + pub json: bool, + pub html: bool, + pub text: bool, +} + +pub struct ConvertDocumentParams { + pub document_id: String, + pub range: Option, + pub export_types: ExportType, +} + +impl ExportType { + pub fn any_enabled(&self) -> bool { + self.json || self.html || self.text + } +} + +impl From for Selection { + fn from(data: SelectionPB) -> Self { + Selection { + block_id: data.block_id, + index: data.index, + length: data.length, + } + } +} + +impl From for Range { + fn from(data: RangePB) -> Self { + Range { + start: data.start.into(), + end: data.end.into(), + } + } +} + +impl From for ExportType { + fn from(data: ExportTypePB) -> Self { + ExportType { + json: data.json, + html: data.html, + text: data.text, + } + } +} +impl TryInto for ConvertDocumentPayloadPB { + type Error = ErrorCode; + fn try_into(self) -> Result { + let document_id = + NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?; + let range = self.range.map(|data| data.into()); + + Ok(ConvertDocumentParams { + document_id: document_id.0, + range, + export_types: self.export_types.into(), + }) + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct InsertDelta { + #[serde(default)] + pub insert: String, + #[serde(default)] + pub attributes: Option>, +} + +impl InsertDelta { + pub fn to_text(&self) -> String { + self.insert.clone() + } + + pub fn to_html(&self) -> String { + let mut html = String::new(); + let mut style = String::new(); + // If there are attributes, serialize them as a HashMap. + if let Some(attrs) = &self.attributes { + // Serialize the font color attributes. + if let Some(color) = attrs.get(FONT_COLOR) { + style.push_str(&format!( + "color: {};", + color.to_string().replace("0x", "#").trim_matches('\"') + )); + } + // Serialize the background color attributes. + if let Some(color) = attrs.get(BG_COLOR) { + style.push_str(&format!( + "background-color: {};", + color.to_string().replace("0x", "#").trim_matches('\"') + )); + } + // Serialize the href attributes. + if let Some(href) = attrs.get(HREF) { + html.push_str(&format!("", href)); + } + + // Serialize the code attributes. + if let Some(code) = attrs.get(CODE) { + if code.as_bool().unwrap_or(false) { + html.push_str(""); + } + } + // Serialize the italic, underline, strikethrough, bold, formula attributes. + if let Some(italic) = attrs.get(ITALIC) { + if italic.as_bool().unwrap_or(false) { + style.push_str("font-style: italic;"); + } + } + if let Some(underline) = attrs.get(UNDERLINE) { + if underline.as_bool().unwrap_or(false) { + style.push_str("text-decoration: underline;"); + } + } + if let Some(strikethrough) = attrs.get(STRIKETHROUGH) { + if strikethrough.as_bool().unwrap_or(false) { + style.push_str("text-decoration: line-through;"); + } + } + if let Some(bold) = attrs.get(BOLD) { + if bold.as_bool().unwrap_or(false) { + style.push_str("font-weight: bold;"); + } + } + if let Some(formula) = attrs.get(FORMULA) { + if formula.as_bool().unwrap_or(false) { + style.push_str("font-family: fantasy;"); + } + } + } + // Serialize the attributes to style. + if !style.is_empty() { + html.push_str(&format!("", style)); + } + // Serialize the insert field. + html.push_str(&self.insert); + + // Close the style tag. + if !style.is_empty() { + html.push_str(""); + } + // Close the tags: , . + if let Some(attrs) = &self.attributes { + if attrs.contains_key(HREF) { + html.push_str(""); + } + if attrs.contains_key(CODE) { + html.push_str(""); + } + } + html + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NestedBlock { + #[serde(default)] + pub id: String, + #[serde(rename = "type")] + pub ty: String, + #[serde(default)] + pub data: HashMap, + #[serde(default)] + pub children: Vec, +} + +impl Eq for NestedBlock {} + +impl PartialEq for NestedBlock { + // ignore the id field + fn eq(&self, other: &Self) -> bool { + self.ty == other.ty + && self.data.iter().all(|(k, v)| { + let other_v = other.data.get(k).unwrap_or(&Value::Null); + if k == DELTA { + let v = convert_insert_delta_from_json(v); + let other_v = convert_insert_delta_from_json(other_v); + return v == other_v; + } + v == other_v + }) + && self.children == other.children + } +} + +pub struct ConvertBlockToHtmlParams { + pub prev_block_ty: Option, + pub next_block_ty: Option, +} + +impl NestedBlock { + pub fn new( + id: String, + ty: String, + data: HashMap, + children: Vec, + ) -> Self { + Self { + id, + ty, + data, + children, + } + } + + pub fn add_child(&mut self, child: NestedBlock) { + self.children.push(child); + } + + pub fn convert_to_html(&self, params: ConvertBlockToHtmlParams) -> String { + let mut html = String::new(); + + let text_html = self + .data + .get("delta") + .and_then(convert_insert_delta_from_json) + .map(|delta| delta_to_html(&delta)) + .unwrap_or_default(); + + let prev_block_ty = params.prev_block_ty.unwrap_or_default(); + let next_block_ty = params.next_block_ty.unwrap_or_default(); + + match self.ty.as_str() { + HEADING => { + let level = self.data.get(LEVEL).unwrap_or(&Value::Null); + if level.as_u64().unwrap_or(0) > 6 { + html.push_str(&format!("
{}
", text_html)); + } else { + html.push_str(&format!("{}", level, text_html, level)); + } + }, + PARAGRAPH => { + html.push_str(&format!("

{}

", text_html)); + html.push_str(&convert_nested_block_children_to_html(Arc::new( + self.to_owned(), + ))); + }, + CALLOUT => { + html.push_str(&format!( + "

{}{}

", + self + .data + .get(ICON) + .unwrap_or(&Value::Null) + .to_string() + .trim_matches('\"'), + text_html + )); + }, + IMAGE => { + html.push_str(&format!( + "{}", + self.data.get(URL).unwrap(), + "AppFlowy-Image" + )); + }, + DIVIDER => { + html.push_str("
"); + }, + MATH_EQUATION => { + let formula = self.data.get(FORMULA).unwrap_or(&Value::Null); + html.push_str(&format!( + "

{}

", + formula.to_string().trim_matches('\"') + )); + }, + CODE => { + let language = self.data.get(LANGUAGE).unwrap_or(&Value::Null); + html.push_str(&format!( + "
{}
", + language.to_string().trim_matches('\"'), + text_html + )); + }, + BULLETED_LIST | NUMBERED_LIST | TODO_LIST | TOGGLE_LIST => { + let list_type = match self.ty.as_str() { + BULLETED_LIST => "ul", + NUMBERED_LIST => "ol", + TODO_LIST => "ul", + TOGGLE_LIST => "ul", + _ => "ul", // Default to "ul" for unknown types + }; + if prev_block_ty != self.ty { + html.push_str(&format!("<{}>", list_type)); + } + if self.ty == TODO_LIST { + let checked_str = if self + .data + .get(CHECKED) + .and_then(|checked| checked.as_bool()) + .unwrap_or(false) + { + "x" + } else { + " " + }; + html.push_str(&format!("
  • [{}] {}
  • ", checked_str, text_html)); + } else { + html.push_str(&format!("
  • {}
  • ", text_html)); + } + + html.push_str(&convert_nested_block_children_to_html(Arc::new( + self.to_owned(), + ))); + + if next_block_ty != self.ty { + html.push_str(&format!("", list_type)); + } + }, + + QUOTE => { + if prev_block_ty != self.ty { + html.push_str("
    "); + } + html.push_str(&format!("

    {}

    ", text_html)); + html.push_str(&convert_nested_block_children_to_html(Arc::new( + self.to_owned(), + ))); + if next_block_ty != self.ty { + html.push_str("
    "); + } + }, + PAGE => { + if !text_html.is_empty() { + html.push_str(&format!("

    {}

    ", text_html)); + } + html.push_str(&convert_nested_block_children_to_html(Arc::new( + self.to_owned(), + ))); + }, + _ => { + html.push_str(&format!("

    {}

    ", text_html)); + html.push_str(&convert_nested_block_children_to_html(Arc::new( + self.to_owned(), + ))); + }, + }; + + html + } + + pub fn convert_to_text(&self) -> String { + let mut text = String::new(); + + let delta_text = self + .data + .get("delta") + .and_then(convert_insert_delta_from_json) + .map(|delta| delta_to_text(&delta)) + .unwrap_or_default(); + + match self.ty.as_str() { + CALLOUT => { + text.push_str(&format!( + "{}{}\n", + self + .data + .get(ICON) + .unwrap_or(&Value::Null) + .to_string() + .trim_matches('\"'), + delta_text + )); + }, + MATH_EQUATION => { + let formula = self.data.get(FORMULA).unwrap_or(&Value::Null); + text.push_str(&format!("{}\n", formula.to_string().trim_matches('\"'))); + }, + PAGE => { + if !delta_text.is_empty() { + text.push_str(&format!("{}\n", delta_text)); + } + for child in &self.children { + text.push_str(&child.convert_to_text()); + } + }, + _ => { + text.push_str(&format!("{}\n", delta_text)); + for child in &self.children { + text.push_str(&child.convert_to_text()); + } + }, + }; + text + } +} diff --git a/frontend/rust-lib/flowy-document2/src/parser/utils.rs b/frontend/rust-lib/flowy-document2/src/parser/utils.rs new file mode 100644 index 000000000000..0897164e7052 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/src/parser/utils.rs @@ -0,0 +1,167 @@ +use crate::parser::constant::DELTA; +use crate::parser::parser_entities::{ + ConvertBlockToHtmlParams, InsertDelta, NestedBlock, Selection, +}; +use collab_document::blocks::{Block, DocumentData}; +use serde_json::Value; +use std::collections::HashMap; +use std::sync::Arc; + +pub struct ConvertBlockToJsonParams { + pub(crate) blocks: HashMap>, + pub(crate) relation_map: HashMap>>, + pub(crate) delta_map: HashMap>, +} +pub fn block_to_nested_json( + block_id: &str, + convert_params: &ConvertBlockToJsonParams, +) -> Option { + let blocks = &convert_params.blocks; + let relation_map = &convert_params.relation_map; + let delta_map = &convert_params.delta_map; + // Attempt to retrieve the block using the block_id + let block = blocks.get(block_id)?; + + // Retrieve the children for this block from the relation map + let children = relation_map.get(&block.id)?; + + // Recursively convert children blocks to JSON + let children: Vec<_> = children + .iter() + .filter_map(|child_id| block_to_nested_json(child_id, convert_params)) + .collect(); + + // Clone block data + let mut data = block.data.clone(); + + // Insert delta into data if available + if let Some(delta) = delta_map.get(&block.id) { + if let Ok(delta_value) = serde_json::to_value(delta) { + data.insert(DELTA.to_string(), delta_value); + } + } + + // Create and return the NestedBlock + Some(NestedBlock { + id: block.id.to_string(), + ty: block.ty.to_string(), + children, + data, + }) +} + +pub fn get_flat_block_ids(block_id: &str, data: &DocumentData) -> Vec { + let blocks = &data.blocks; + let children_map = &data.meta.children_map; + + if let Some(block) = blocks.get(block_id) { + let mut result = vec![block.id.clone()]; + + if let Some(child_ids) = children_map.get(&block.children) { + for child_id in child_ids { + let child_blocks = get_flat_block_ids(child_id, data); + result.extend(child_blocks); + } + + return result; + } + } + + vec![] +} + +pub fn get_delta_for_block(block_id: &str, data: &DocumentData) -> Option> { + let text_map = data.meta.text_map.as_ref()?; // Retrieve the text_map reference + + data.blocks.get(block_id).and_then(|block| { + let text_id = block.external_id.as_ref()?; + let delta_str = text_map.get(text_id)?; + serde_json::from_str::>(delta_str).ok() + }) +} + +pub fn get_delta_for_selection( + selection: &Selection, + data: &DocumentData, +) -> Option> { + let delta = get_delta_for_block(&selection.block_id, data)?; + let start = selection.index as usize; + let end = (selection.index + selection.length) as usize; + Some(slice_delta(&delta, start, end)) +} + +pub fn slice_delta(delta: &Vec, start: usize, end: usize) -> Vec { + let mut result = vec![]; + let mut index = 0; + for d in delta { + let content = &d.insert; + let text_len = content.len(); + // skip if index is not reached + if index + text_len <= start { + index += text_len; + continue; + } + // break if index is over end + if index >= end { + break; + } + // slice content, and push to result + let start_offset = std::cmp::max(0, start as isize - index as isize) as usize; + let end_offset = std::cmp::min(end - index, text_len); + let content = content[start_offset..end_offset].to_string(); + result.push(InsertDelta { + insert: content, + attributes: d.attributes.clone(), + }); + + index += text_len; + } + result +} +pub fn delta_to_text(delta: &Vec) -> String { + let mut result = String::new(); + for d in delta { + result.push_str(d.to_text().as_str()); + } + result +} + +pub fn delta_to_html(delta: &Vec) -> String { + let mut result = String::new(); + for d in delta { + result.push_str(d.to_html().as_str()); + } + result +} + +pub fn convert_nested_block_children_to_html(block: Arc) -> String { + let children = &block.children; + let mut html = String::new(); + let num_children = children.len(); + + for (i, child) in children.iter().enumerate() { + let prev_block_ty = if i > 0 { + Some(children[i - 1].ty.to_string()) + } else { + None + }; + + let next_block_ty = if i + 1 < num_children { + Some(children[i + 1].ty.to_string()) + } else { + None + }; + + let child_html = child.convert_to_html(ConvertBlockToHtmlParams { + prev_block_ty, + next_block_ty, + }); + + html.push_str(&child_html); + } + html +} + +pub fn convert_insert_delta_from_json(delta_value: &Value) -> Option> { + serde_json::from_value::>(delta_value.to_owned()).ok() +} diff --git a/frontend/rust-lib/flowy-document2/tests/assets/html/bulleted_list.html b/frontend/rust-lib/flowy-document2/tests/assets/html/bulleted_list.html new file mode 100644 index 000000000000..ae621dac0bba --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/html/bulleted_list.html @@ -0,0 +1 @@ +
    • Highlight
    • You can also

      • nest
    \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/html/callout.html b/frontend/rust-lib/flowy-document2/tests/assets/html/callout.html new file mode 100644 index 000000000000..14e7c5d4e7d1 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/html/callout.html @@ -0,0 +1,6 @@ +

    🥰 +Like AppFlowy? Follow us: +GitHub +Twitter: @appflowy +Newsletter +

    \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/html/code.html b/frontend/rust-lib/flowy-document2/tests/assets/html/code.html new file mode 100644 index 000000000000..9d859c1c5fc1 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/html/code.html @@ -0,0 +1,5 @@ +
    // This is the main function.
    +fn main() {
    +    // Print text to the console.
    +    println!("Hello World!");
    +}
    \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/html/divider.html b/frontend/rust-lib/flowy-document2/tests/assets/html/divider.html new file mode 100644 index 000000000000..95ca67339a4e --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/html/divider.html @@ -0,0 +1 @@ +
    \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/html/heading.html b/frontend/rust-lib/flowy-document2/tests/assets/html/heading.html new file mode 100644 index 000000000000..99459dee6e46 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/html/heading.html @@ -0,0 +1 @@ +

    Heading1

    Heading2

    Heading3

    \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/html/image.html b/frontend/rust-lib/flowy-document2/tests/assets/html/image.html new file mode 100644 index 000000000000..24908700a51b --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/html/image.html @@ -0,0 +1 @@ +AppFlowy-Image \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/html/math_equation.html b/frontend/rust-lib/flowy-document2/tests/assets/html/math_equation.html new file mode 100644 index 000000000000..38f572ec822f --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/html/math_equation.html @@ -0,0 +1 @@ +

    E = MC^2

    \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/html/numbered_list.html b/frontend/rust-lib/flowy-document2/tests/assets/html/numbered_list.html new file mode 100644 index 000000000000..7bcc0ec06b98 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/html/numbered_list.html @@ -0,0 +1 @@ +
    1. Highlight
    2. You can also

      1. nest
    \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/html/paragraph.html b/frontend/rust-lib/flowy-document2/tests/assets/html/paragraph.html new file mode 100644 index 000000000000..786d48fa5060 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/html/paragraph.html @@ -0,0 +1,6 @@ +

    +Like AppFlowy? Follow us: +GitHub +Twitter: @appflowy +Newsletter +

    Click ? at the bottom right for help and support.

    Highlight any text, and use the editing menu to style your writing however you like.1+1=2

    \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/html/quote.html b/frontend/rust-lib/flowy-document2/tests/assets/html/quote.html new file mode 100644 index 000000000000..6da59e8aeb27 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/html/quote.html @@ -0,0 +1 @@ +

    This is a quote

    This is a paragraph

    \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/html/todo_list.html b/frontend/rust-lib/flowy-document2/tests/assets/html/todo_list.html new file mode 100644 index 000000000000..19f48f241001 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/html/todo_list.html @@ -0,0 +1 @@ +
    • [x] Highlight
    • You can also

      • [ ] nest
    \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/html/toggle_list.html b/frontend/rust-lib/flowy-document2/tests/assets/html/toggle_list.html new file mode 100644 index 000000000000..a8e93bdf74ea --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/html/toggle_list.html @@ -0,0 +1 @@ +
    • Click ? at the bottom right for help and support.
    • This is a paragraph

      • This is a toggle list
    \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/json/bulleted_list.json b/frontend/rust-lib/flowy-document2/tests/assets/json/bulleted_list.json new file mode 100644 index 000000000000..47080498057c --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/json/bulleted_list.json @@ -0,0 +1,37 @@ +{ + "type": "page", + "children": [ + { + "type": "bulleted_list", + "data": { + "delta": [ + { + "insert": "Highlight" + } + ] + }, + "children": [ + { + "type": "paragraph", + "data": { + "delta": [ + { + "insert": "You can also" + } + ] + } + }, + { + "type": "bulleted_list", + "data": { + "delta": [ + { + "insert": "nest" + } + ] + } + } + ] + } + ] +} \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/json/callout.json b/frontend/rust-lib/flowy-document2/tests/assets/json/callout.json new file mode 100644 index 000000000000..a494982f6472 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/json/callout.json @@ -0,0 +1,32 @@ +{ + "type": "page", + "data": {}, + "children": [ + { + "type": "callout", + "data": { + "delta": [ + { "insert": "\nLike AppFlowy? Follow us:\n" }, + { + "attributes": { + "href": "https://github.com/AppFlowy-IO/AppFlowy" + }, + "insert": "GitHub" + }, + { "insert": "\n" }, + { + "attributes": { "href": "https://twitter.com/appflowy" }, + "insert": "Twitter" + }, + { "insert": ": @appflowy\n" }, + { + "attributes": { "href": "https://blog-appflowy.ghost.io/" }, + "insert": "Newsletter" + }, + { "insert": "\n" } + ], + "icon": "🥰" + } + } + ] +} \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/json/code.json b/frontend/rust-lib/flowy-document2/tests/assets/json/code.json new file mode 100644 index 000000000000..21bf6379077f --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/json/code.json @@ -0,0 +1,14 @@ +{ + "type": "page", + "children": [{ + "type": "code", + "data": { + "language": "rust", + "delta": [ + { + "insert": "// This is the main function.\nfn main() {\n // Print text to the console.\n println!(\"Hello World!\");\n}" + } + ] + } + }] +} \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/json/divider.json b/frontend/rust-lib/flowy-document2/tests/assets/json/divider.json new file mode 100644 index 000000000000..05625723f301 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/json/divider.json @@ -0,0 +1,10 @@ +{ + "type": "page", + "data": {}, + "children": [ + { + "type": "divider", + "data": {} + } + ] +} \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/json/heading.json b/frontend/rust-lib/flowy-document2/tests/assets/json/heading.json new file mode 100644 index 000000000000..a1c9ffcb783f --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/json/heading.json @@ -0,0 +1,39 @@ +{ + "type": "page", + "data": {}, + "children": [ + { + "type": "heading", + "data": { + "level": 1, + "delta": [ + { + "insert": "Heading1" + } + ] + } + }, + { + "type": "heading", + "data": { + "level": 2, + "delta": [ + { + "insert": "Heading2" + } + ] + } + }, + { + "type": "heading", + "data": { + "level": 3, + "delta": [ + { + "insert": "Heading3" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/json/image.json b/frontend/rust-lib/flowy-document2/tests/assets/json/image.json new file mode 100644 index 000000000000..0b88529538a5 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/json/image.json @@ -0,0 +1,15 @@ +{ + "type": "page", + "data": {}, + "children": [ + { + "type": "image", + "data": { + "url": "https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png", + "width": 272, + "height": 92, + "align": "center" + } + } + ] +} \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/json/initial_document.json b/frontend/rust-lib/flowy-document2/tests/assets/json/initial_document.json new file mode 100644 index 000000000000..5ef653ad9528 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/json/initial_document.json @@ -0,0 +1,267 @@ +{ + "type": "page", + "data": {}, + "children": [ + { + "type": "heading", + "data": { "delta": [{ "insert": "Welcome to AppFlowy!" }], "level": 1 } + }, + { + "type": "heading", + "data": { "delta": [{ "insert": "Here are the basics" }], "level": 2 } + }, + { + "type": "heading", + "data": { "delta": [{ "insert": "Here is H3" }], "level": 3 } + }, + { + "type": "todo_list", + "data": { + "delta": [{ "insert": "Click anywhere and just start typing." }], + "checked": false + }, + "children": [ + { + "type": "todo_list", + "data": { + "delta": [ + { "insert": "Click " }, + { "attributes": { "code": true }, "insert": "Enter" }, + { "insert": " to create a new line." } + ], + "checked": false + } + } + ] + }, + { + "type": "todo_list", + "data": { + "checked": false, + "delta": [ + { + "attributes": { "bg_color": "0x4dffeb3b" }, + "insert": "Highlight " + }, + { "insert": "any text, and use the editing menu to " }, + { "attributes": { "italic": true }, "insert": "style" }, + { "insert": " " }, + { "attributes": { "bold": true }, "insert": "your" }, + { "insert": " " }, + { "attributes": { "underline": true }, "insert": "writing" }, + { "insert": " " }, + { "attributes": { "code": true }, "insert": "however" }, + { "insert": " you " }, + { "attributes": { "strikethrough": true }, "insert": "like." } + ] + } + }, + { + "type": "todo_list", + "data": { + "checked": false, + "delta": [ + { "insert": "As soon as you type " }, + { + "attributes": { "code": true, "font_color": "0xff00b5ff" }, + "insert": "/" + }, + { "insert": " a menu will pop up. Select " }, + { + "attributes": { "bg_color": "0x4d9c27b0" }, + "insert": "different types" + }, + { "insert": " of content blocks you can add." } + ] + } + }, + { + "type": "todo_list", + "data": { + "delta": [ + { "insert": "Type " }, + { "attributes": { "code": true }, "insert": "/" }, + { "insert": " followed by " }, + { "attributes": { "code": true }, "insert": "/bullet" }, + { "insert": " or " }, + { "attributes": { "code": true }, "insert": "/num" }, + { "attributes": { "code": false }, "insert": " to create a list." } + ], + "checked": false + } + }, + { + "type": "todo_list", + "data": { + "delta": [ + { "insert": "Click " }, + { "attributes": { "code": true }, "insert": "+ New Page " }, + { + "insert": "button at the bottom of your sidebar to add a new page." + } + ], + "checked": true + } + }, + { + "type": "todo_list", + "data": { + "checked": false, + "delta": [ + { "insert": "Click " }, + { "attributes": { "code": true }, "insert": "+" }, + { "insert": " next to any page title in the sidebar to " }, + { + "attributes": { "font_color": "0xff8427e0" }, + "insert": "quickly" + }, + { "insert": " add a new subpage, " }, + { "attributes": { "code": true }, "insert": "Document" }, + { "attributes": { "code": false }, "insert": ", " }, + { "attributes": { "code": true }, "insert": "Grid" }, + { "attributes": { "code": false }, "insert": ", or " }, + { "attributes": { "code": true }, "insert": "Kanban Board" }, + { "attributes": { "code": false }, "insert": "." } + ] + } + }, + { "type": "paragraph", "data": { "delta": [] } }, + { "type": "divider" }, + { "type": "paragraph", "data": { "delta": [] } }, + { + "type": "heading", + "data": { + "delta": [{ "insert": "Keyboard shortcuts, markdown, and code block" }], + "level": 2 + } + }, + { + "type": "numbered_list", + "data": { + "delta": [ + { "insert": "Keyboard shortcuts " }, + { + "attributes": { + "href": "https://appflowy.gitbook.io/docs/essential-documentation/shortcuts" + }, + "insert": "guide" + } + ] + } + }, + { + "type": "numbered_list", + "data": { + "delta": [ + { "insert": "Markdown " }, + { + "attributes": { + "href": "https://appflowy.gitbook.io/docs/essential-documentation/markdown" + }, + "insert": "reference" + } + ] + } + }, + { + "type": "numbered_list", + "data": { + "delta": [ + { "insert": "Type " }, + { "attributes": { "code": true }, "insert": "/code" }, + { + "attributes": { "code": false }, + "insert": " to insert a code block" + } + ] + } + }, + { + "type": "code", + "data": { + "language": "rust", + "delta": [ + { + "insert": "// This is the main function.\nfn main() {\n // Print text to the console.\n println!(\"Hello World!\");\n}" + } + ] + } + }, + { + "type": "paragraph", + "data": { "delta": [] }, + "children": [{ + "type": "paragraph", + "data": { "delta": [{ "insert": "This is a paragraph" }] }, + "children": [{ + "type": "paragraph", + "data": { "delta": [{ "insert": "This is a paragraph" }] } + }] + }] + }, + { + "type": "heading", + "data": { "level": 2, "delta": [{ "insert": "Have a question❓" }] } + }, + { + "type": "toggle_list", + "data": { + "delta": [ + { "insert": "Click " }, + { "attributes": { "code": true }, "insert": "?" }, + { "insert": " at the bottom right for help and support." } + ] + }, + "children": [ + { + "type": "paragraph", + "data": { "delta": [{ "insert": "This is a paragraph" }] } + }, + { + "type": "paragraph", + "data": { "delta": [{ "insert": "This is a paragraph" }] } + } + ] + }, + { + "type": "quote", + "data": { + "delta": [ + { "insert": "Click " }, + { "attributes": { "code": true }, "insert": "?" }, + { "insert": " at the bottom right for help and support." } + ] + } + }, + { "type": "paragraph", "data": { "delta": [] } }, + { + "type": "callout", + "data": { + "delta": [ + { "insert": "\nLike AppFlowy? Follow us:\n" }, + { + "attributes": { + "href": "https://github.com/AppFlowy-IO/AppFlowy" + }, + "insert": "GitHub" + }, + { "insert": "\n" }, + { + "attributes": { "href": "https://twitter.com/appflowy" }, + "insert": "Twitter" + }, + { "insert": ": @appflowy\n" }, + { + "attributes": { "href": "https://blog-appflowy.ghost.io/" }, + "insert": "Newsletter" + }, + { "insert": "\n" } + ], + "icon": "🥰" + } + }, + { "type": "paragraph", "data": { "delta": [] } }, + { "type": "paragraph", "data": { "delta": [] } }, + { "type": "paragraph", "data": { "delta": [] } } + ] +} diff --git a/frontend/rust-lib/flowy-document2/tests/assets/json/math_equation.json b/frontend/rust-lib/flowy-document2/tests/assets/json/math_equation.json new file mode 100644 index 000000000000..8d1fd5245639 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/json/math_equation.json @@ -0,0 +1,9 @@ +{ + "type": "page", + "children": [{ + "type": "math_equation", + "data": { + "formula": "E = MC^2" + } + }] +} \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/json/numbered_list.json b/frontend/rust-lib/flowy-document2/tests/assets/json/numbered_list.json new file mode 100644 index 000000000000..cdcea264c738 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/json/numbered_list.json @@ -0,0 +1,37 @@ +{ + "type": "page", + "children": [ + { + "type": "numbered_list", + "data": { + "delta": [ + { + "insert": "Highlight" + } + ] + }, + "children": [ + { + "type": "paragraph", + "data": { + "delta": [ + { + "insert": "You can also" + } + ] + } + }, + { + "type": "numbered_list", + "data": { + "delta": [ + { + "insert": "nest" + } + ] + } + } + ] + } + ] +} \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/json/paragraph.json b/frontend/rust-lib/flowy-document2/tests/assets/json/paragraph.json new file mode 100644 index 000000000000..50aac23910d8 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/json/paragraph.json @@ -0,0 +1,59 @@ +{ + "type": "page", + "data": {}, + "children": [ + { + "type": "paragraph", + "data": { "delta": [ + { "insert": "\nLike AppFlowy? Follow us:\n" }, + { + "attributes": { + "href": "https://github.com/AppFlowy-IO/AppFlowy" + }, + "insert": "GitHub" + }, + { "insert": "\n" }, + { + "attributes": { "href": "https://twitter.com/appflowy" }, + "insert": "Twitter" + }, + { "insert": ": @appflowy\n" }, + { + "attributes": { "href": "https://blog-appflowy.ghost.io/" }, + "insert": "Newsletter" + }, + { "insert": "\n" } + ]}, + "children": [{ + "type": "paragraph", + "data": { + "delta": [ + { "insert": "Click " }, + { "attributes": { "code": true }, "insert": "?" }, + { "insert": " at the bottom right for help and support." } + ] + } + }] + }, + { + "type": "paragraph", + "data": { "delta": [ + { + "attributes": { "bg_color": "0x4dffeb3b" }, + "insert": "Highlight " + }, + { "insert": "any text, and use the editing menu to " }, + { "attributes": { "italic": true }, "insert": "style" }, + { "insert": " " }, + { "attributes": { "bold": true }, "insert": "your" }, + { "insert": " " }, + { "attributes": { "underline": true }, "insert": "writing" }, + { "insert": " " }, + { "attributes": { "code": true }, "insert": "however" }, + { "insert": " you ", "attributes": { "font_color": "0x4dffeb3b" } }, + { "attributes": { "strikethrough": true }, "insert": "like." }, + { "attributes": { "formula": true }, "insert": "1+1=2" } + ] } + } + ] +} \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/json/quote.json b/frontend/rust-lib/flowy-document2/tests/assets/json/quote.json new file mode 100644 index 000000000000..a17f3d55b78d --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/json/quote.json @@ -0,0 +1,25 @@ +{ + "type": "page", + "children": [ + { + "type": "quote", + "data": { + "delta": [ + { + "insert": "This is a quote" + } + ] + }, + "children": [{ + "type": "paragraph", + "data": { + "delta": [ + { + "insert": "This is a paragraph" + } + ] + } + }] + } + ] +} \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/json/range_1.json b/frontend/rust-lib/flowy-document2/tests/assets/json/range_1.json new file mode 100644 index 000000000000..778a5e5f1ddd --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/json/range_1.json @@ -0,0 +1,100 @@ +{ + "type": "page", + "data": {}, + "children": [ + { + "type": "heading", + "data": { "delta": [{ "insert": " are the basics" }], "level": 2 } + }, + { + "type": "heading", + "data": { "delta": [{ "insert": "Here is H3" }], "level": 3 } + }, + { + "type": "todo_list", + "data": { + "delta": [{ "insert": "Click anywhere and just start typing." }], + "checked": false + }, + "children": [ + { + "type": "todo_list", + "data": { + "delta": [ + { "insert": "Click " }, + { "attributes": { "code": true }, "insert": "Enter" }, + { "insert": " to create a new line." } + ], + "checked": false + } + } + ] + }, + { + "type": "todo_list", + "data": { + "checked": false, + "delta": [ + { + "attributes": { "bg_color": "0x4dffeb3b" }, + "insert": "Highlight " + }, + { "insert": "any text, and use the editing menu to " }, + { "attributes": { "italic": true }, "insert": "style" }, + { "insert": " " }, + { "attributes": { "bold": true }, "insert": "your" }, + { "insert": " " }, + { "attributes": { "underline": true }, "insert": "writing" }, + { "insert": " " }, + { "attributes": { "code": true }, "insert": "however" }, + { "insert": " you " }, + { "attributes": { "strikethrough": true }, "insert": "like." } + ] + } + }, + { + "type": "todo_list", + "data": { + "checked": false, + "delta": [ + { "insert": "As soon as you type " }, + { + "attributes": { "code": true, "font_color": "0xff00b5ff" }, + "insert": "/" + }, + { "insert": " a menu will pop up. Select " }, + { + "attributes": { "bg_color": "0x4d9c27b0" }, + "insert": "different types" + }, + { "insert": " of content blocks you can add." } + ] + } + }, + { + "type": "todo_list", + "data": { + "delta": [ + { "insert": "Type " }, + { "attributes": { "code": true }, "insert": "/" }, + { "insert": " followed by " }, + { "attributes": { "code": true }, "insert": "/bullet" }, + { "insert": " or " }, + { "attributes": { "code": true }, "insert": "/num" }, + { "attributes": { "code": false }, "insert": " to create a list." } + ], + "checked": false + } + }, + { + "type": "todo_list", + "data": { + "delta": [ + { "insert": "Click " }, + { "attributes": { "code": true }, "insert": "+ New" } + ], + "checked": true + } + } + ] +} diff --git a/frontend/rust-lib/flowy-document2/tests/assets/json/range_2.json b/frontend/rust-lib/flowy-document2/tests/assets/json/range_2.json new file mode 100644 index 000000000000..3e0740427453 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/json/range_2.json @@ -0,0 +1,178 @@ +{ + "type": "page", + "data": {}, + "children": [ + { + "type": "todo_list", + "data": { + "delta": [ + { "attributes": { "code": true }, "insert": "Enter" }, + { "insert": " to create a new line." } + ], + "checked": false + } + }, + { + "type": "todo_list", + "data": { + "checked": false, + "delta": [ + { + "attributes": { "bg_color": "0x4dffeb3b" }, + "insert": "Highlight " + }, + { "insert": "any text, and use the editing menu to " }, + { "attributes": { "italic": true }, "insert": "style" }, + { "insert": " " }, + { "attributes": { "bold": true }, "insert": "your" }, + { "insert": " " }, + { "attributes": { "underline": true }, "insert": "writing" }, + { "insert": " " }, + { "attributes": { "code": true }, "insert": "however" }, + { "insert": " you " }, + { "attributes": { "strikethrough": true }, "insert": "like." } + ] + } + }, + { + "type": "todo_list", + "data": { + "checked": false, + "delta": [ + { "insert": "As soon as you type " }, + { + "attributes": { "code": true, "font_color": "0xff00b5ff" }, + "insert": "/" + }, + { "insert": " a menu will pop up. Select " }, + { + "attributes": { "bg_color": "0x4d9c27b0" }, + "insert": "different types" + }, + { "insert": " of content blocks you can add." } + ] + } + }, + { + "type": "todo_list", + "data": { + "delta": [ + { "insert": "Type " }, + { "attributes": { "code": true }, "insert": "/" }, + { "insert": " followed by " }, + { "attributes": { "code": true }, "insert": "/bullet" }, + { "insert": " or " }, + { "attributes": { "code": true }, "insert": "/num" }, + { "attributes": { "code": false }, "insert": " to create a list." } + ], + "checked": false + } + }, + { + "type": "todo_list", + "data": { + "delta": [ + { "insert": "Click " }, + { "attributes": { "code": true }, "insert": "+ New Page " }, + { + "insert": "button at the bottom of your sidebar to add a new page." + } + ], + "checked": true + } + }, + { + "type": "todo_list", + "data": { + "checked": false, + "delta": [ + { "insert": "Click " }, + { "attributes": { "code": true }, "insert": "+" }, + { "insert": " next to any page title in the sidebar to " }, + { + "attributes": { "font_color": "0xff8427e0" }, + "insert": "quickly" + }, + { "insert": " add a new subpage, " }, + { "attributes": { "code": true }, "insert": "Document" }, + { "attributes": { "code": false }, "insert": ", " }, + { "attributes": { "code": true }, "insert": "Grid" }, + { "attributes": { "code": false }, "insert": ", or " }, + { "attributes": { "code": true }, "insert": "Kanban Board" }, + { "attributes": { "code": false }, "insert": "." } + ] + } + }, + { "type": "paragraph", "data": { "delta": [] } }, + { "type": "divider" }, + { "type": "paragraph", "data": { "delta": [] } }, + { + "type": "heading", + "data": { + "delta": [{ "insert": "Keyboard shortcuts, markdown, and code block" }], + "level": 2 + } + }, + { + "type": "numbered_list", + "data": { + "delta": [ + { "insert": "Keyboard shortcuts " }, + { + "attributes": { + "href": "https://appflowy.gitbook.io/docs/essential-documentation/shortcuts" + }, + "insert": "guide" + } + ] + } + }, + { + "type": "numbered_list", + "data": { + "delta": [ + { "insert": "Markdown " }, + { + "attributes": { + "href": "https://appflowy.gitbook.io/docs/essential-documentation/markdown" + }, + "insert": "reference" + } + ] + } + }, + { + "type": "numbered_list", + "data": { + "delta": [ + { "insert": "Type " }, + { "attributes": { "code": true }, "insert": "/code" }, + { + "attributes": { "code": false }, + "insert": " to insert a code block" + } + ] + } + }, + { + "type": "code", + "data": { + "language": "rust", + "delta": [ + { + "insert": "// This is the main function.\nfn main() {\n // Print text to the console.\n println!(\"Hello World!\");\n}" + } + ] + } + }, + { + "type": "paragraph", + "data": { "delta": [] }, + "children": [{ + "type": "paragraph", + "data": { "delta": [{ "insert": "This is a p" }] }, + "children": [] + }] + } + ] +} \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/json/todo_list.json b/frontend/rust-lib/flowy-document2/tests/assets/json/todo_list.json new file mode 100644 index 000000000000..e37e103af367 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/json/todo_list.json @@ -0,0 +1,39 @@ +{ + "type": "page", + "children": [ + { + "type": "todo_list", + "data": { + "checked": true, + "delta": [ + { + "insert": "Highlight" + } + ] + }, + "children": [ + { + "type": "paragraph", + "data": { + "delta": [ + { + "insert": "You can also" + } + ] + } + }, + { + "type": "todo_list", + "checked": false, + "data": { + "delta": [ + { + "insert": "nest" + } + ] + } + } + ] + } + ] +} \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/json/toggle_list.json b/frontend/rust-lib/flowy-document2/tests/assets/json/toggle_list.json new file mode 100644 index 000000000000..89530afd15e9 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/json/toggle_list.json @@ -0,0 +1,25 @@ +{ + "type": "page", + "children": [ + { + "type": "toggle_list", + "data": { + "delta": [ + { "insert": "Click " }, + { "attributes": { "code": true }, "insert": "?" }, + { "insert": " at the bottom right for help and support." } + ] + }, + "children": [ + { + "type": "paragraph", + "data": { "delta": [{ "insert": "This is a paragraph" }] } + }, + { + "type": "toggle_list", + "data": { "delta": [{ "insert": "This is a toggle list" }] } + } + ] + } + ] +} \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/text/bulleted_list.txt b/frontend/rust-lib/flowy-document2/tests/assets/text/bulleted_list.txt new file mode 100644 index 000000000000..59fc99d7fe1e --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/text/bulleted_list.txt @@ -0,0 +1,3 @@ +Highlight +You can also +nest diff --git a/frontend/rust-lib/flowy-document2/tests/assets/text/callout.txt b/frontend/rust-lib/flowy-document2/tests/assets/text/callout.txt new file mode 100644 index 000000000000..779f4f9f81f0 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/text/callout.txt @@ -0,0 +1,6 @@ +🥰 +Like AppFlowy? Follow us: +GitHub +Twitter: @appflowy +Newsletter + diff --git a/frontend/rust-lib/flowy-document2/tests/assets/text/code.txt b/frontend/rust-lib/flowy-document2/tests/assets/text/code.txt new file mode 100644 index 000000000000..9271ac6c895a --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/text/code.txt @@ -0,0 +1,5 @@ +// This is the main function. +fn main() { + // Print text to the console. + println!("Hello World!"); +} diff --git a/frontend/rust-lib/flowy-document2/tests/assets/text/divider.txt b/frontend/rust-lib/flowy-document2/tests/assets/text/divider.txt new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/text/divider.txt @@ -0,0 +1 @@ + diff --git a/frontend/rust-lib/flowy-document2/tests/assets/text/heading.txt b/frontend/rust-lib/flowy-document2/tests/assets/text/heading.txt new file mode 100644 index 000000000000..45fba2b33070 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/text/heading.txt @@ -0,0 +1,3 @@ +Heading1 +Heading2 +Heading3 diff --git a/frontend/rust-lib/flowy-document2/tests/assets/text/image.txt b/frontend/rust-lib/flowy-document2/tests/assets/text/image.txt new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/text/image.txt @@ -0,0 +1 @@ + diff --git a/frontend/rust-lib/flowy-document2/tests/assets/text/math_equation.txt b/frontend/rust-lib/flowy-document2/tests/assets/text/math_equation.txt new file mode 100644 index 000000000000..ba201486b534 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/text/math_equation.txt @@ -0,0 +1 @@ +E = MC^2 diff --git a/frontend/rust-lib/flowy-document2/tests/assets/text/numbered_list.txt b/frontend/rust-lib/flowy-document2/tests/assets/text/numbered_list.txt new file mode 100644 index 000000000000..59fc99d7fe1e --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/text/numbered_list.txt @@ -0,0 +1,3 @@ +Highlight +You can also +nest diff --git a/frontend/rust-lib/flowy-document2/tests/assets/text/paragraph.txt b/frontend/rust-lib/flowy-document2/tests/assets/text/paragraph.txt new file mode 100644 index 000000000000..893fbd1a7101 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/text/paragraph.txt @@ -0,0 +1,8 @@ + +Like AppFlowy? Follow us: +GitHub +Twitter: @appflowy +Newsletter + +Click ? at the bottom right for help and support. +Highlight any text, and use the editing menu to style your writing however you like.1+1=2 diff --git a/frontend/rust-lib/flowy-document2/tests/assets/text/quote.txt b/frontend/rust-lib/flowy-document2/tests/assets/text/quote.txt new file mode 100644 index 000000000000..e082baf25eee --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/text/quote.txt @@ -0,0 +1,2 @@ +This is a quote +This is a paragraph diff --git a/frontend/rust-lib/flowy-document2/tests/assets/text/todo_list.txt b/frontend/rust-lib/flowy-document2/tests/assets/text/todo_list.txt new file mode 100644 index 000000000000..59fc99d7fe1e --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/text/todo_list.txt @@ -0,0 +1,3 @@ +Highlight +You can also +nest diff --git a/frontend/rust-lib/flowy-document2/tests/assets/text/toggle_list.txt b/frontend/rust-lib/flowy-document2/tests/assets/text/toggle_list.txt new file mode 100644 index 000000000000..30369415c04b --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/text/toggle_list.txt @@ -0,0 +1,3 @@ +Click ? at the bottom right for help and support. +This is a paragraph +This is a toggle list diff --git a/frontend/rust-lib/flowy-document2/tests/document/mod.rs b/frontend/rust-lib/flowy-document2/tests/document/mod.rs index e975a80c55a2..8d724a938beb 100644 --- a/frontend/rust-lib/flowy-document2/tests/document/mod.rs +++ b/frontend/rust-lib/flowy-document2/tests/document/mod.rs @@ -2,4 +2,4 @@ mod document_insert_test; mod document_redo_undo_test; mod document_test; mod event_handler_test; -mod util; +pub mod util; diff --git a/frontend/rust-lib/flowy-document2/tests/parser/document_data_parser_test.rs b/frontend/rust-lib/flowy-document2/tests/parser/document_data_parser_test.rs new file mode 100644 index 000000000000..67a189603165 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/parser/document_data_parser_test.rs @@ -0,0 +1,105 @@ +use collab_document::blocks::DocumentData; +use flowy_document2::parser::document_data_parser::DocumentDataParser; +use flowy_document2::parser::json::parser::JsonToDocumentParser; +use flowy_document2::parser::parser_entities::{NestedBlock, Range, Selection}; +use std::sync::Arc; + +#[tokio::test] +async fn document_data_parse_json_test() { + let initial_json_str = include_str!("../assets/json/initial_document.json"); + let document_data = JsonToDocumentParser::json_str_to_document(initial_json_str) + .unwrap() + .into(); + let parser = DocumentDataParser::new(Arc::new(document_data), None); + let read_me_json = serde_json::from_str::(initial_json_str).unwrap(); + let json = parser.to_json().unwrap(); + assert_eq!(read_me_json, json); +} + +// range_1 is a range from the 2nd block to the 8th block +#[tokio::test] +async fn document_data_to_json_with_range_1_test() { + let initial_json_str = include_str!("../assets/json/initial_document.json"); + let document_data: DocumentData = JsonToDocumentParser::json_str_to_document(initial_json_str) + .unwrap() + .into(); + + let children_map = &document_data.meta.children_map; + let page_block_id = &document_data.page_id; + let blocks = &document_data.blocks; + let page_block = blocks.get(page_block_id).unwrap(); + let children = children_map.get(page_block.children.as_str()).unwrap(); + + let range = Range { + start: Selection { + block_id: children.get(1).unwrap().to_string(), + index: 4, + length: 15, + }, + end: Selection { + block_id: children.get(7).unwrap().to_string(), + index: 0, + length: 11, + }, + }; + let parser = DocumentDataParser::new(Arc::new(document_data), Some(range)); + let json = parser.to_json().unwrap(); + let part_1 = include_str!("../assets/json/range_1.json"); + let part_1_json = serde_json::from_str::(part_1).unwrap(); + assert_eq!(part_1_json, json); +} + +// range_2 is a range from the 4th block's first child to the 18th block's first child +#[tokio::test] +async fn document_data_to_json_with_range_2_test() { + let initial_json_str = include_str!("../assets/json/initial_document.json"); + let document_data: DocumentData = JsonToDocumentParser::json_str_to_document(initial_json_str) + .unwrap() + .into(); + + let children_map = &document_data.meta.children_map; + let page_block_id = &document_data.page_id; + let blocks = &document_data.blocks; + let page_block = blocks.get(page_block_id).unwrap(); + + let start_block_parent_id = children_map + .get(page_block.children.as_str()) + .unwrap() + .get(3) + .unwrap(); + let start_block_parent = blocks.get(start_block_parent_id).unwrap(); + let start_block_id = children_map + .get(start_block_parent.children.as_str()) + .unwrap() + .get(0) + .unwrap(); + + let start = Selection { + block_id: start_block_id.to_string(), + index: 6, + length: 27, + }; + + let end_block_parent_id = children_map + .get(page_block.children.as_str()) + .unwrap() + .get(17) + .unwrap(); + let end_block_parent = blocks.get(end_block_parent_id).unwrap(); + let end_block_children = children_map + .get(end_block_parent.children.as_str()) + .unwrap(); + let end_block_id = end_block_children.get(0).unwrap(); + let end = Selection { + block_id: end_block_id.to_string(), + index: 0, + length: 11, + }; + + let range = Range { start, end }; + let parser = DocumentDataParser::new(Arc::new(document_data), Some(range)); + let json = parser.to_json().unwrap(); + let part_2 = include_str!("../assets/json/range_2.json"); + let part_2_json = serde_json::from_str::(part_2).unwrap(); + assert_eq!(part_2_json, json); +} diff --git a/frontend/rust-lib/flowy-document2/tests/parser/html_text/mod.rs b/frontend/rust-lib/flowy-document2/tests/parser/html_text/mod.rs new file mode 100644 index 000000000000..60914e9678d0 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/parser/html_text/mod.rs @@ -0,0 +1,2 @@ +mod test; +mod utils; diff --git a/frontend/rust-lib/flowy-document2/tests/parser/html_text/test.rs b/frontend/rust-lib/flowy-document2/tests/parser/html_text/test.rs new file mode 100644 index 000000000000..9935443a14c0 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/parser/html_text/test.rs @@ -0,0 +1,37 @@ +use crate::parser::html_text::utils::{assert_document_html_eq, assert_document_text_eq}; + +macro_rules! generate_test_cases { + ($($block_ty:ident),*) => { + [ + $( + ( + include_str!(concat!("../../assets/json/", stringify!($block_ty), ".json")), + include_str!(concat!("../../assets/html/", stringify!($block_ty), ".html")), + include_str!(concat!("../../assets/text/", stringify!($block_ty), ".txt")), + ) + ),* + ] + }; +} + +#[tokio::test] +async fn block_tests() { + let test_cases = generate_test_cases!( + heading, + callout, + paragraph, + divider, + image, + math_equation, + code, + bulleted_list, + numbered_list, + todo_list, + toggle_list, + quote + ); + for (json_data, expect_html, expect_text) in test_cases.iter() { + assert_document_html_eq(json_data, expect_html); + assert_document_text_eq(json_data, expect_text); + } +} diff --git a/frontend/rust-lib/flowy-document2/tests/parser/html_text/utils.rs b/frontend/rust-lib/flowy-document2/tests/parser/html_text/utils.rs new file mode 100644 index 000000000000..5484da9ede86 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/parser/html_text/utils.rs @@ -0,0 +1,21 @@ +use flowy_document2::parser::document_data_parser::DocumentDataParser; +use flowy_document2::parser::json::parser::JsonToDocumentParser; +use std::sync::Arc; + +pub fn assert_document_html_eq(source: &str, expect: &str) { + let document_data = JsonToDocumentParser::json_str_to_document(source) + .unwrap() + .into(); + let parser = DocumentDataParser::new(Arc::new(document_data), None); + let html = parser.to_html(); + assert_eq!(expect, html); +} + +pub fn assert_document_text_eq(source: &str, expect: &str) { + let document_data = JsonToDocumentParser::json_str_to_document(source) + .unwrap() + .into(); + let parser = DocumentDataParser::new(Arc::new(document_data), None); + let text = parser.to_text(); + assert_eq!(expect, text); +} diff --git a/frontend/rust-lib/flowy-document2/tests/parser/mod.rs b/frontend/rust-lib/flowy-document2/tests/parser/mod.rs index cff0e9089e51..18ec9c997681 100644 --- a/frontend/rust-lib/flowy-document2/tests/parser/mod.rs +++ b/frontend/rust-lib/flowy-document2/tests/parser/mod.rs @@ -1 +1,3 @@ +mod document_data_parser_test; +mod html_text; mod json; From d358e18f332ff77006dfefcbb7b1e0b745d0d346 Mon Sep 17 00:00:00 2001 From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Date: Mon, 30 Oct 2023 17:34:37 +0100 Subject: [PATCH 10/56] fix: add card at the beginning (#3835) --- .../board/board_add_row_test.dart | 101 ++++++++++++++++++ .../board/board_test_runner.dart | 2 + .../application/database_controller.dart | 2 + .../application/database_view_service.dart | 6 +- .../board/application/board_bloc.dart | 6 +- 5 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 frontend/appflowy_flutter/integration_test/board/board_add_row_test.dart diff --git a/frontend/appflowy_flutter/integration_test/board/board_add_row_test.dart b/frontend/appflowy_flutter/integration_test/board/board_add_row_test.dart new file mode 100644 index 000000000000..0ef813bec618 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/board/board_add_row_test.dart @@ -0,0 +1,101 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:appflowy_board/appflowy_board.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../util/util.dart'; + +const defaultFirstCardName = 'Card 1'; +const defaultLastCardName = 'Card 3'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('board add row test', () { + testWidgets('Add card from header', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.createNewPageWithName(layout: ViewLayoutPB.Board); + + final findFirstCard = find.descendant( + of: find.byType(AppFlowyGroupCard), + matching: find.byType(FlowyText), + ); + + FlowyText firstCardText = tester.firstWidget(findFirstCard); + expect(firstCardText.text, defaultFirstCardName); + + await tester.tap( + find + .descendant( + of: find.byType(AppFlowyGroupHeader), + matching: find.byType(FlowySvg), + ) + .first, + ); + await tester.pumpAndSettle(); + + const newCardName = 'Card 4'; + await tester.enterText( + find.descendant( + of: find.byType(IntrinsicHeight), + matching: find.byType(TextField), + ), + newCardName, + ); + await tester.pumpAndSettle(); + + await tester.tap(find.byType(AppFlowyBoard)); + await tester.pumpAndSettle(); + + firstCardText = tester.firstWidget(findFirstCard); + expect(firstCardText.text, newCardName); + }); + + testWidgets('Add card from footer', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.createNewPageWithName(layout: ViewLayoutPB.Board); + + final findLastCard = find.descendant( + of: find.byType(AppFlowyGroupCard), + matching: find.byType(FlowyText), + ); + + FlowyText? lastCardText = + tester.widgetList(findLastCard).last as FlowyText; + expect(lastCardText.text, defaultLastCardName); + + await tester.tap( + find + .descendant( + of: find.byType(AppFlowyGroupFooter), + matching: find.byType(FlowySvg), + ) + .first, + ); + await tester.pumpAndSettle(); + + const newCardName = 'Card 4'; + await tester.enterText( + find.descendant( + of: find.byType(IntrinsicHeight), + matching: find.byType(TextField), + ), + newCardName, + ); + await tester.pumpAndSettle(); + + await tester.tap(find.byType(AppFlowyBoard)); + await tester.pumpAndSettle(); + + lastCardText = tester.widgetList(findLastCard).last as FlowyText; + expect(lastCardText.text, newCardName); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/board/board_test_runner.dart b/frontend/appflowy_flutter/integration_test/board/board_test_runner.dart index ebed667fb2b8..d95c1cbdd7b4 100644 --- a/frontend/appflowy_flutter/integration_test/board/board_test_runner.dart +++ b/frontend/appflowy_flutter/integration_test/board/board_test_runner.dart @@ -1,10 +1,12 @@ import 'package:integration_test/integration_test.dart'; import 'board_row_test.dart' as board_row_test; +import 'board_add_row_test.dart' as board_add_row_test; void startTesting() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); // Board integration tests board_row_test.main(); + board_add_row_test.main(); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart index 0640c1fc6fca..8cf1d7d4c1cd 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart @@ -177,6 +177,7 @@ class DatabaseController { Future> createRow({ RowId? startRowId, String? groupId, + bool fromBeginning = false, void Function(RowDataBuilder builder)? withCells, }) { Map? cellDataByFieldId; @@ -191,6 +192,7 @@ class DatabaseController { startRowId: startRowId, groupId: groupId, cellDataByFieldId: cellDataByFieldId, + fromBeginning: fromBeginning, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart index 4cfeff80754b..bdada95d69d1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart @@ -35,9 +35,13 @@ class DatabaseViewBackendService { RowId? startRowId, String? groupId, Map? cellDataByFieldId, + bool fromBeginning = false, }) { final payload = CreateRowPayloadPB.create()..viewId = viewId; - payload.startRowId = startRowId ?? ""; + + if (!fromBeginning || startRowId != null) { + payload.startRowId = startRowId ?? ""; + } if (groupId != null) { payload.groupId = groupId; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart index 517700b8a228..8c5d99c1264d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart @@ -90,7 +90,11 @@ class BoardBloc extends Bloc { ); }, createHeaderRow: (String groupId) async { - final result = await databaseController.createRow(groupId: groupId); + final result = await databaseController.createRow( + groupId: groupId, + fromBeginning: true, + ); + result.fold( (_) {}, (err) => Log.error(err), From 59dff54edbe80ee706d287f2c0850541dbab87e5 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 31 Oct 2023 09:49:14 +0800 Subject: [PATCH 11/56] chore: bump version 0.3.7 (#3832) --- CHANGELOG.md | 22 ++++++++++++++++++++-- frontend/appflowy_flutter/pubspec.lock | 9 +++++---- frontend/appflowy_flutter/pubspec.yaml | 5 ++++- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b904f260ac8..baa764c29f57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Release Notes +## Version 0.3.7 - 10/30/2023 + +### New Features +- Support showing checklist items inline in row page. +- Support inserting date from slash menu. +- Support renaming a stack directly by clicking on the stack name. +- Show the detailed reminder content in the notification center. +- Save card order in Board view. +- Allow to hide the ungrouped stack. +- Segmented the checklist progress bar. + +### Bug fixes +- Optimize side panel animation. +- Fix calendar with hidden date or title doesn't show options correctly. +- Fix the horizontal scroll bar disappears in Grid view. +- Improve setting tab UI in Grid view. +- Improve theme of the code block. +- Fix some UI issues. + ## Version 0.3.6 - 10/16/2023 ### New Features @@ -7,8 +26,7 @@ - Added Ukrainian language. - Support auto-hiding sidebar feature, ensuring a streamlined view even when resizing to a smaller window. - Support toggling the notifitcation on/off. -- Added Lemonade theme. - +- Added Lemonade theme. ### Bug fixes - Improve Vietnamese translations. diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 36b53f40a104..397d874f6ee9 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -53,10 +53,11 @@ packages: appflowy_editor: dependency: "direct main" description: - name: appflowy_editor - sha256: cb6a0e7fa545923495cf85f1173b9f5572c67dc2f003b8d2b6dec9305eb5dafa - url: "https://pub.dev" - source: hosted + path: "." + ref: "7336274" + resolved-ref: "7336274ff90402c8dd790b029e00cac60c580f28" + url: "https://github.com/AppFlowy-IO/appflowy-editor.git" + source: git version: "1.5.0" appflowy_popover: dependency: "direct main" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 06765d5b580a..abe808cd1c42 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -44,7 +44,10 @@ dependencies: git: url: https://github.com/AppFlowy-IO/appflowy-board.git ref: 6aba8dd - appflowy_editor: ^1.5.0 + appflowy_editor: + git: + url: https://github.com/AppFlowy-IO/appflowy-editor.git + ref: "7336274" appflowy_popover: path: packages/appflowy_popover From 54dbcb7c5e5177c77f7d79dd590862b59fcd1c0f Mon Sep 17 00:00:00 2001 From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Date: Tue, 31 Oct 2023 20:11:32 +0100 Subject: [PATCH 12/56] fix: link to page dialog offset (#3840) --- .../editor_plugins/base/link_to_page_widget.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart index d670f698010e..ff5ae9984a94 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart @@ -24,8 +24,8 @@ void showLinkToPageMenu( final alignment = menuService.alignment; final offset = menuService.offset; - final top = alignment == Alignment.bottomLeft ? offset.dy : null; - final bottom = alignment == Alignment.topLeft ? offset.dy : null; + final top = alignment == Alignment.topLeft ? offset.dy : null; + final bottom = alignment == Alignment.bottomLeft ? offset.dy : null; keepEditorFocusNotifier.increase(); late OverlayEntry linkToPageMenuEntry; @@ -246,7 +246,6 @@ extension on ViewLayoutPB { case ViewLayoutPB.Calendar: return LocaleKeys.document_slashMenu_calendar_selectACalendarToLinkTo .tr(); - default: throw Exception('Unknown layout type'); } From 3e088d48ac6b3f4d8cfd2b6dba5a3171529932c9 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Wed, 1 Nov 2023 11:45:35 +0800 Subject: [PATCH 13/56] refactor: fav and workspace (#3837) * refactor: workspace * chore: update collab rev * test: add data migration test * fix: test * fix: tauri build * test: fix bloc test * test: fix bloc test * test: fix bloc test * chore: restore magic codde --- .../sidebar/sidebar_favorites_test.dart | 6 +- .../favorite/mobile_favorite_folder.dart | 2 +- .../favorite/mobile_favorite_page.dart | 2 +- .../presentation/home/mobile_folders.dart | 2 +- .../presentation/home/mobile_home_page.dart | 2 +- .../lib/user/application/user_service.dart | 21 +- .../lib/user/presentation/router.dart | 4 +- .../presentation/screens/splash_screen.dart | 2 +- .../workspace/application/home/home_bloc.dart | 4 +- .../workspace/application/menu/menu_bloc.dart | 15 +- .../application/view/view_service.dart | 8 +- .../workspace/workspace_service.dart | 22 +-- .../home/desktop_home_screen.dart | 2 +- .../home/menu/sidebar/sidebar.dart | 2 +- .../settings_file_exporter_widget.dart | 10 +- .../bloc_test/home_test/app_bloc_test.dart | 6 +- .../bloc_test/home_test/home_bloc_test.dart | 7 +- .../bloc_test/home_test/menu_bloc_test.dart | 4 +- frontend/appflowy_flutter/test/util.dart | 8 +- frontend/appflowy_tauri/src-tauri/Cargo.lock | 20 +- frontend/appflowy_tauri/src-tauri/Cargo.toml | 16 +- .../components/auth/auth.hooks.ts | 4 +- .../components/tests/DatabaseTestHelper.ts | 8 +- .../components/tests/DocumentTestHelper.ts | 8 +- .../components/tests/TestFolder.tsx | 5 +- .../stores/effects/user/user_bd_svc.ts | 22 ++- .../effects/workspace/workspace_bd_svc.ts | 10 +- .../workspace/workspace_manager_controller.ts | 13 +- frontend/rust-lib/Cargo.lock | 20 +- frontend/rust-lib/Cargo.toml | 16 +- .../src/document/document_event.rs | 2 +- .../event-integration/src/folder_event.rs | 11 +- .../tests/database/local_test/test.rs | 80 ++++---- .../tests/database/supabase_test/helper.rs | 6 +- .../tests/document/af_cloud_test/util.rs | 6 +- .../tests/document/supabase_test/file_test.rs | 6 +- .../tests/document/supabase_test/helper.rs | 6 +- .../tests/folder/local_test/folder_test.rs | 45 +---- .../tests/folder/local_test/script.rs | 85 +++------ .../folder/local_test/subscription_test.rs | 8 +- .../tests/folder/local_test/test.rs | 87 ++++----- .../tests/folder/supabase_test/helper.rs | 4 +- .../tests/folder/supabase_test/test.rs | 19 +- .../036_fav_v1_workspace_array.zip | Bin 0 -> 47700 bytes .../tests/user/migration_test/version_test.rs | 22 +++ .../tests/user/supabase_test/auth_test.rs | 18 +- .../user/supabase_test/workspace_test.rs | 4 +- .../rust-lib/event-integration/tests/util.rs | 5 +- .../flowy-core/src/integrate/trait_impls.rs | 9 +- frontend/rust-lib/flowy-error/src/code.rs | 3 + frontend/rust-lib/flowy-error/src/errors.rs | 1 + .../rust-lib/flowy-folder-deps/src/cloud.rs | 8 +- .../flowy-folder2/src/entities/icon.rs | 2 +- .../flowy-folder2/src/entities/trash.rs | 2 +- .../flowy-folder2/src/entities/view.rs | 2 +- .../flowy-folder2/src/entities/workspace.rs | 47 ++--- .../flowy-folder2/src/event_handler.rs | 94 ++++----- .../rust-lib/flowy-folder2/src/event_map.rs | 13 +- frontend/rust-lib/flowy-folder2/src/lib.rs | 2 +- .../rust-lib/flowy-folder2/src/manager.rs | 179 ++++++++++-------- .../flowy-folder2/src/notification.rs | 14 +- .../flowy-folder2/src/share/import.rs | 2 +- .../flowy-folder2/src/user_default.rs | 6 +- .../flowy-folder2/src/view_operation.rs | 4 +- .../flowy-server/src/af_cloud/impls/folder.rs | 13 +- .../src/local_server/impls/folder.rs | 6 +- .../flowy-server/src/supabase/api/folder.rs | 9 +- .../flowy-server/src/supabase/api/user.rs | 15 +- .../flowy-server/tests/supabase_test/util.rs | 19 +- .../anon_user_data.rs} | 46 ++--- .../flowy-user/src/anon_user_upgrade/mod.rs | 5 + .../sync_new_user.rs | 5 +- frontend/rust-lib/flowy-user/src/event_map.rs | 2 +- frontend/rust-lib/flowy-user/src/lib.rs | 1 + frontend/rust-lib/flowy-user/src/manager.rs | 25 ++- ..._document.rs => document_empty_content.rs} | 11 +- .../flowy-user/src/migrations/migration.rs | 7 + .../rust-lib/flowy-user/src/migrations/mod.rs | 5 +- .../migrations/workspace_and_favorite_v1.rs | 55 ++++++ 79 files changed, 641 insertions(+), 656 deletions(-) create mode 100644 frontend/rust-lib/event-integration/tests/user/migration_test/history_user_db/036_fav_v1_workspace_array.zip rename frontend/rust-lib/flowy-user/src/{migrations/migrate_to_new_user.rs => anon_user_upgrade/anon_user_data.rs} (92%) create mode 100644 frontend/rust-lib/flowy-user/src/anon_user_upgrade/mod.rs rename frontend/rust-lib/flowy-user/src/{migrations => anon_user_upgrade}/sync_new_user.rs (99%) rename frontend/rust-lib/flowy-user/src/migrations/{historical_document.rs => document_empty_content.rs} (91%) create mode 100644 frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs diff --git a/frontend/appflowy_flutter/integration_test/sidebar/sidebar_favorites_test.dart b/frontend/appflowy_flutter/integration_test/sidebar/sidebar_favorites_test.dart index c0842b487b66..b0af036eeadd 100644 --- a/frontend/appflowy_flutter/integration_test/sidebar/sidebar_favorites_test.dart +++ b/frontend/appflowy_flutter/integration_test/sidebar/sidebar_favorites_test.dart @@ -53,7 +53,7 @@ void main() { await tester.favoriteViewByName(names[1]); expect( tester.findFavoritePageName(names[1]), - findsNWidgets(2), + findsNWidgets(1), ); await tester.unfavoriteViewByName(gettingStarted); @@ -131,7 +131,7 @@ void main() { widget.view.isFavorite && widget.categoryType == FolderCategoryType.favorite, ), - findsNWidgets(6), + findsNWidgets(3), ); await tester.hoverOnPageName( @@ -150,7 +150,7 @@ void main() { widget.view.isFavorite && widget.categoryType == FolderCategoryType.favorite, ), - findsNWidgets(3), + findsNWidgets(2), ); await tester.hoverOnPageName( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart index dd11796c82ac..0f7499b94be3 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart @@ -29,7 +29,7 @@ class MobileFavoritePageFolder extends StatelessWidget { BlocProvider( create: (_) => MenuBloc( user: userProfile, - workspace: workspaceSetting.workspace, + workspaceId: workspaceSetting.workspaceId, )..add(const MenuEvent.initial()), ), BlocProvider( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart index 8f21404100b8..f0b45373bc03 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart @@ -19,7 +19,7 @@ class MobileFavoriteScreen extends StatelessWidget { Widget build(BuildContext context) { return FutureBuilder( future: Future.wait([ - FolderEventGetCurrentWorkspace().send(), + FolderEventGetCurrentWorkspaceSetting().send(), getIt().getUser(), ]), builder: (context, snapshots) { diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart index 606206568278..d6d1c14366f1 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart @@ -29,7 +29,7 @@ class MobileFolders extends StatelessWidget { BlocProvider( create: (_) => MenuBloc( user: user, - workspace: workspaceSetting.workspace, + workspaceId: workspaceSetting.workspaceId, )..add(const MenuEvent.initial()), ), BlocProvider( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart index 0c5db1dfd8f3..8ed1a81f49fb 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart @@ -24,7 +24,7 @@ class MobileHomeScreen extends StatelessWidget { Widget build(BuildContext context) { return FutureBuilder( future: Future.wait([ - FolderEventGetCurrentWorkspace().send(), + FolderEventGetCurrentWorkspaceSetting().send(), getIt().getUser(), ]), builder: (context, snapshots) { diff --git a/frontend/appflowy_flutter/lib/user/application/user_service.dart b/frontend/appflowy_flutter/lib/user/application/user_service.dart index ad9ee477a668..9815af70dce6 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_service.dart @@ -90,19 +90,28 @@ class UserBackendService { } Future, FlowyError>> getWorkspaces() { - final request = WorkspaceIdPB.create(); + // final request = WorkspaceIdPB.create(); + // return FolderEventReadAllWorkspaces(request).send().then((result) { + // return result.fold( + // (workspaces) => left(workspaces.items), + // (error) => right(error), + // ); + // }); + return Future.value(left([])); + } - return FolderEventReadAllWorkspaces(request).send().then((result) { + Future> openWorkspace(String workspaceId) { + final request = WorkspaceIdPB.create()..value = workspaceId; + return FolderEventOpenWorkspace(request).send().then((result) { return result.fold( - (workspaces) => left(workspaces.items), + (workspace) => left(workspace), (error) => right(error), ); }); } - Future> openWorkspace(String workspaceId) { - final request = WorkspaceIdPB.create()..value = workspaceId; - return FolderEventOpenWorkspace(request).send().then((result) { + Future> getCurrentWorkspace() { + return FolderEventReadCurrentWorkspace().send().then((result) { return result.fold( (workspace) => left(workspace), (error) => right(error), diff --git a/frontend/appflowy_flutter/lib/user/presentation/router.dart b/frontend/appflowy_flutter/lib/user/presentation/router.dart index 92c56ef97cec..e3e27aa9529c 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/router.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/router.dart @@ -42,7 +42,7 @@ class AuthRouter { BuildContext context, UserProfilePB userProfile, ) async { - final result = await FolderEventGetCurrentWorkspace().send(); + final result = await FolderEventGetCurrentWorkspaceSetting().send(); result.fold( (workspaceSetting) { // Replace SignInScreen or SkipLogInScreen as root page. @@ -104,7 +104,7 @@ class SplashRouter { }, ); - FolderEventGetCurrentWorkspace().send().then((result) { + FolderEventGetCurrentWorkspaceSetting().send().then((result) { result.fold( (workspaceSettingPB) => pushHomeScreen(context), (r) => null, diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart index 3ea6b371da81..3cdba2c1fe3b 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart @@ -75,7 +75,7 @@ class SplashScreen extends StatelessWidget { if (check.requireSecret) { getIt().pushEncryptionScreen(context, userProfile); } else { - final result = await FolderEventGetCurrentWorkspace().send(); + final result = await FolderEventGetCurrentWorkspaceSetting().send(); result.fold( (workspaceSetting) { // After login, replace Splash screen by corresponding home screen diff --git a/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart index 2d095e211db0..09f8c7e71e65 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart @@ -41,8 +41,8 @@ class HomeBloc extends Bloc { emit(state.copyWith(isLoading: e.isLoading)); }, didReceiveWorkspaceSetting: (_DidReceiveWorkspaceSetting value) { - final latestView = workspaceSetting.hasLatestView() - ? workspaceSetting.latestView + final latestView = value.setting.hasLatestView() + ? value.setting.latestView : state.latestView; emit( diff --git a/frontend/appflowy_flutter/lib/workspace/application/menu/menu_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/menu/menu_bloc.dart index 3ffb9a218b43..c877ddab18f5 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/menu/menu_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/menu/menu_bloc.dart @@ -5,7 +5,6 @@ import 'package:appflowy/workspace/application/workspace/workspace_service.dart' import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:dartz/dartz.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -17,17 +16,17 @@ class MenuBloc extends Bloc { final WorkspaceService _workspaceService; final WorkspaceListener _listener; final UserProfilePB user; - final WorkspacePB workspace; + final String workspaceId; MenuBloc({ required this.user, - required this.workspace, - }) : _workspaceService = WorkspaceService(workspaceId: workspace.id), + required this.workspaceId, + }) : _workspaceService = WorkspaceService(workspaceId: workspaceId), _listener = WorkspaceListener( user: user, - workspaceId: workspace.id, + workspaceId: workspaceId, ), - super(MenuState.initial(workspace)) { + super(MenuState.initial()) { on((event, emit) async { await event.map( initial: (e) async { @@ -122,8 +121,8 @@ class MenuState with _$MenuState { ViewPB? lastCreatedView, }) = _MenuState; - factory MenuState.initial(WorkspacePB workspace) => MenuState( - views: workspace.views, + factory MenuState.initial() => MenuState( + views: [], successOrFailure: left(unit), lastCreatedView: null, ); diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart index a88e19fa0476..3c27ea599a34 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart @@ -200,10 +200,10 @@ class ViewBackendService { Future> fetchViews() async { final result = []; - return FolderEventGetCurrentWorkspace().send().then((value) async { - final workspaces = value.getLeftOrNull(); - if (workspaces != null) { - final views = workspaces.workspace.views; + return FolderEventReadCurrentWorkspace().send().then((value) async { + final workspace = value.getLeftOrNull(); + if (workspace != null) { + final views = workspace.views; for (final view in views) { result.add(view); final childViews = await getAllViews(view); diff --git a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_service.dart b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_service.dart index 4883657aff8a..3843469c8777 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_service.dart @@ -1,15 +1,12 @@ import 'dart:async'; import 'package:dartz/dartz.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart' show CreateViewPayloadPB, MoveViewPayloadPB, ViewLayoutPB, ViewPB; import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; - class WorkspaceService { final String workspaceId; WorkspaceService({ @@ -38,24 +35,7 @@ class WorkspaceService { } Future> getWorkspace() { - final payload = WorkspaceIdPB.create()..value = workspaceId; - return FolderEventReadAllWorkspaces(payload).send().then((result) { - return result.fold( - (workspaces) { - assert(workspaces.items.length == 1); - - if (workspaces.items.isEmpty) { - return right( - FlowyError.create() - ..msg = LocaleKeys.workspace_notFoundError.tr(), - ); - } else { - return left(workspaces.items[0]); - } - }, - (error) => right(error), - ); - }); + return FolderEventReadCurrentWorkspace().send(); } Future, FlowyError>> getViews() { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart index 95d6341e1590..c21758cb66e9 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart @@ -39,7 +39,7 @@ class DesktopHomeScreen extends StatelessWidget { Widget build(BuildContext context) { return FutureBuilder( future: Future.wait([ - FolderEventGetCurrentWorkspace().send(), + FolderEventGetCurrentWorkspaceSetting().send(), getIt().getUser(), ]), builder: (context, snapshots) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart index e2497aebf06d..55c491a120c3 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart @@ -46,7 +46,7 @@ class HomeSideBar extends StatelessWidget { BlocProvider( create: (_) => MenuBloc( user: user, - workspace: workspaceSetting.workspace, + workspaceId: workspaceSetting.workspaceId, )..add(const MenuEvent.initial()), ), BlocProvider( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_exporter_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_exporter_widget.dart index 11112c9b8a64..ee4a88d1c6e9 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_exporter_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_exporter_widget.dart @@ -33,14 +33,14 @@ class _FileExporterWidgetState extends State { @override Widget build(BuildContext context) { - return FutureBuilder>( - future: FolderEventGetCurrentWorkspace().send(), + return FutureBuilder>( + future: FolderEventReadCurrentWorkspace().send(), builder: (context, snapshot) { if (snapshot.hasData && snapshot.connectionState == ConnectionState.done) { - final workspaces = snapshot.data?.getLeftOrNull(); - if (workspaces != null) { - final views = workspaces.workspace.views; + final workspace = snapshot.data?.getLeftOrNull(); + if (workspace != null) { + final views = workspace.views; cubit ??= SettingsFileExporterCubit(views: views); return BlocProvider.value( value: cubit!, diff --git a/frontend/appflowy_flutter/test/bloc_test/home_test/app_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/home_test/app_bloc_test.dart index 8913709d4c4e..89d58cbd54ca 100644 --- a/frontend/appflowy_flutter/test/bloc_test/home_test/app_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/home_test/app_bloc_test.dart @@ -126,7 +126,7 @@ void main() { ..add(const DocumentEvent.initial()); await blocResponseFuture(); - final workspaceSetting = await FolderEventGetCurrentWorkspace() + final workspaceSetting = await FolderEventGetCurrentWorkspaceSetting() .send() .then((result) => result.fold((l) => l, (r) => throw Exception())); workspaceSetting.latestView.id == document1.id; @@ -147,7 +147,7 @@ void main() { final grid = bloc.state.latestCreatedView; assert(grid!.name == "grid 2"); - var workspaceSetting = await FolderEventGetCurrentWorkspace() + var workspaceSetting = await FolderEventGetCurrentWorkspaceSetting() .send() .then((result) => result.fold((l) => l, (r) => throw Exception())); workspaceSetting.latestView.id == grid!.id; @@ -158,7 +158,7 @@ void main() { ..add(const DocumentEvent.initial()); await blocResponseFuture(); - workspaceSetting = await FolderEventGetCurrentWorkspace() + workspaceSetting = await FolderEventGetCurrentWorkspaceSetting() .send() .then((result) => result.fold((l) => l, (r) => throw Exception())); workspaceSetting.latestView.id == document.id; diff --git a/frontend/appflowy_flutter/test/bloc_test/home_test/home_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/home_test/home_bloc_test.dart index 7d9f158b6981..2b17e496441e 100644 --- a/frontend/appflowy_flutter/test/bloc_test/home_test/home_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/home_test/home_bloc_test.dart @@ -14,7 +14,7 @@ void main() { }); test('initi home screen', () async { - final workspaceSetting = await FolderEventGetCurrentWorkspace() + final workspaceSetting = await FolderEventGetCurrentWorkspaceSetting() .send() .then((result) => result.fold((l) => l, (r) => throw Exception())); await blocResponseFuture(); @@ -27,7 +27,7 @@ void main() { }); test('open the document', () async { - final workspaceSetting = await FolderEventGetCurrentWorkspace() + final workspaceSetting = await FolderEventGetCurrentWorkspaceSetting() .send() .then((result) => result.fold((l) => l, (r) => throw Exception())); await blocResponseFuture(); @@ -52,6 +52,7 @@ void main() { await FolderEventSetLatestView(ViewIdPB(value: latestView.id)).send(); await blocResponseFuture(); - assert(homeBloc.state.workspaceSetting.latestView.id == latestView.id); + final actual = homeBloc.state.workspaceSetting.latestView.id; + assert(actual == latestView.id); }); } diff --git a/frontend/appflowy_flutter/test/bloc_test/home_test/menu_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/home_test/menu_bloc_test.dart index abbdeaa50506..cae6493ed41a 100644 --- a/frontend/appflowy_flutter/test/bloc_test/home_test/menu_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/home_test/menu_bloc_test.dart @@ -12,7 +12,7 @@ void main() { test('assert initial apps is the build-in app', () async { final menuBloc = MenuBloc( user: testContext.userProfile, - workspace: testContext.currentWorkspace, + workspaceId: testContext.currentWorkspace.id, )..add(const MenuEvent.initial()); await blocResponseFuture(); @@ -22,7 +22,7 @@ void main() { test('reorder apps', () async { final menuBloc = MenuBloc( user: testContext.userProfile, - workspace: testContext.currentWorkspace, + workspaceId: testContext.currentWorkspace.id, )..add(const MenuEvent.initial()); await blocResponseFuture(); menuBloc.add(const MenuEvent.createApp("App 1")); diff --git a/frontend/appflowy_flutter/test/util.dart b/frontend/appflowy_flutter/test/util.dart index 0b86a9c3310b..55e5a7db6249 100644 --- a/frontend/appflowy_flutter/test/util.dart +++ b/frontend/appflowy_flutter/test/util.dart @@ -27,7 +27,7 @@ class AppFlowyUnitTest { late UserProfilePB userProfile; late UserBackendService userService; late WorkspaceService workspaceService; - late List workspaces; + late WorkspacePB workspace; static Future ensureInitialized() async { TestWidgetsFlutterBinding.ensureInitialized(); @@ -68,12 +68,12 @@ class AppFlowyUnitTest { ); } - WorkspacePB get currentWorkspace => workspaces[0]; + WorkspacePB get currentWorkspace => workspace; Future _loadWorkspace() async { - final result = await userService.getWorkspaces(); + final result = await userService.getCurrentWorkspace(); result.fold( - (value) => workspaces = value, + (value) => workspace = value, (error) { throw Exception(error); }, diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 38f1f5f53782..982d4f22e4fc 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -445,7 +445,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" dependencies = [ "borsh-derive", - "hashbrown 0.13.2", + "hashbrown 0.12.3", ] [[package]] @@ -845,7 +845,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" dependencies = [ "anyhow", "async-trait", @@ -864,7 +864,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" dependencies = [ "anyhow", "async-trait", @@ -894,7 +894,7 @@ dependencies = [ [[package]] name = "collab-derive" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" dependencies = [ "proc-macro2", "quote", @@ -906,7 +906,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" dependencies = [ "anyhow", "collab", @@ -926,7 +926,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" dependencies = [ "anyhow", "bytes", @@ -940,7 +940,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" dependencies = [ "anyhow", "chrono", @@ -982,7 +982,7 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" dependencies = [ "async-trait", "bincode", @@ -1003,7 +1003,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" dependencies = [ "anyhow", "async-trait", @@ -1030,7 +1030,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" dependencies = [ "anyhow", "collab", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index 980d4cb0936a..131846e2bad6 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -48,14 +48,14 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c87 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "8861d7a" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "8861d7a" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "8861d7a" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "8861d7a" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "8861d7a" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "8861d7a" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "8861d7a" } -collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "8861d7a" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b05aa7e" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b05aa7e" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b05aa7e" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b05aa7e" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b05aa7e" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b05aa7e" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b05aa7e" } +collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b05aa7e" } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/auth/auth.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/auth/auth.hooks.ts index e87f5c73219e..c5cb0691f405 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/auth/auth.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/auth/auth.hooks.ts @@ -2,9 +2,9 @@ import { currentUserActions } from '../../stores/reducers/current-user/slice'; import { useAppDispatch, useAppSelector } from '../../stores/store'; import { UserProfilePB } from '../../../services/backend/events/flowy-user'; import { AuthBackendService, UserBackendService } from '../../stores/effects/user/user_bd_svc'; -import { FolderEventGetCurrentWorkspace } from '../../../services/backend/events/flowy-folder2'; import { WorkspaceSettingPB } from '../../../services/backend/models/flowy-folder2/workspace'; import { Log } from '../../utils/log'; +import { FolderEventGetCurrentWorkspaceSetting } from '@/services/backend/events/flowy-folder2'; export const useAuth = () => { const dispatch = useAppDispatch(); @@ -99,7 +99,7 @@ export const useAuth = () => { } async function _openWorkspace() { - return FolderEventGetCurrentWorkspace(); + return FolderEventGetCurrentWorkspaceSetting(); } return { currentUser, checkUser, register, login, logout }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/tests/DatabaseTestHelper.ts b/frontend/appflowy_tauri/src/appflowy_app/components/tests/DatabaseTestHelper.ts index 9b4e90b314f6..98a91855a49c 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/tests/DatabaseTestHelper.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/tests/DatabaseTestHelper.ts @@ -6,7 +6,6 @@ import { ViewPB, WorkspaceSettingPB, } from '../../../services/backend'; -import { FolderEventGetCurrentWorkspace } from '../../../services/backend/events/flowy-folder2'; import { DatabaseController } from '../../stores/effects/database/database_controller'; import { RowInfo } from '../../stores/effects/database/row/row_cache'; import { RowController } from '../../stores/effects/database/row/row_controller'; @@ -28,12 +27,15 @@ import { makeSingleSelectTypeOptionContext } from '../../stores/effects/database import { SelectOptionBackendService } from '../../stores/effects/database/cell/select_option_bd_svc'; import { Log } from '$app/utils/log'; import { WorkspaceController } from '../../stores/effects/workspace/workspace_controller'; +import { FolderEventGetCurrentWorkspaceSetting } from '@/services/backend/events/flowy-folder2'; // Create a database page for specific layout type // Do not use it production code. Just for testing export async function createTestDatabaseView(layout: ViewLayoutPB): Promise { - const workspaceSetting: WorkspaceSettingPB = await FolderEventGetCurrentWorkspace().then((result) => result.unwrap()); - const wsSvc = new WorkspaceController(workspaceSetting.workspace.id); + const workspaceSetting: WorkspaceSettingPB = await FolderEventGetCurrentWorkspaceSetting().then((result) => + result.unwrap() + ); + const wsSvc = new WorkspaceController(workspaceSetting.workspace_id); const viewRes = await wsSvc.createView({ name: 'New Grid', layout }); return viewRes; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/tests/DocumentTestHelper.ts b/frontend/appflowy_tauri/src/appflowy_app/components/tests/DocumentTestHelper.ts index a0ff41e929f3..c9d1f0af4da4 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/tests/DocumentTestHelper.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/tests/DocumentTestHelper.ts @@ -1,10 +1,12 @@ import { ViewLayoutPB, WorkspaceSettingPB } from '@/services/backend'; -import { FolderEventGetCurrentWorkspace } from '@/services/backend/events/flowy-folder2'; import { WorkspaceController } from '../../stores/effects/workspace/workspace_controller'; +import { FolderEventGetCurrentWorkspaceSetting } from '@/services/backend/events/flowy-folder2'; export async function createTestDocument() { - const workspaceSetting: WorkspaceSettingPB = await FolderEventGetCurrentWorkspace().then((result) => result.unwrap()); - const appService = new WorkspaceController(workspaceSetting.workspace.id); + const workspaceSetting: WorkspaceSettingPB = await FolderEventGetCurrentWorkspaceSetting().then((result) => + result.unwrap() + ); + const appService = new WorkspaceController(workspaceSetting.workspace_id); const result = await appService.createView({ name: 'New Document', layout: ViewLayoutPB.Document }); return result; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestFolder.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestFolder.tsx index 1989bcdd4c2f..44585d039a20 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestFolder.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestFolder.tsx @@ -18,9 +18,8 @@ const testCreateFolder = async (userId?: number) => { console.log('workspaces: ', workspaces.val.toObject()); } - const currentWorkspace = await userBackendService.getCurrentWorkspace(); - - const workspaceService = new WorkspaceController(currentWorkspace.workspace.id); + const currentWorkspaceSetting = await userBackendService.getCurrentWorkspaceSetting(); + const workspaceService = new WorkspaceController(currentWorkspaceSetting.workspace_id); const rootViews: ViewPB[] = []; for (let i = 1; i <= 3; i++) { diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/user/user_bd_svc.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/user/user_bd_svc.ts index 0cfc29ac0b21..38f5850f3f6d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/user/user_bd_svc.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/user/user_bd_svc.ts @@ -25,8 +25,8 @@ import { import { FolderEventCreateWorkspace, FolderEventOpenWorkspace, - FolderEventGetCurrentWorkspace, - FolderEventReadAllWorkspaces, + FolderEventGetCurrentWorkspaceSetting, + FolderEventReadCurrentWorkspace, } from '@/services/backend/events/flowy-folder2'; export class UserBackendService { @@ -56,8 +56,8 @@ export class UserBackendService { return UserEventUpdateUserProfile(payload); }; - getCurrentWorkspace = async (): Promise => { - const result = await FolderEventGetCurrentWorkspace(); + getCurrentWorkspaceSetting = async (): Promise => { + const result = await FolderEventGetCurrentWorkspaceSetting(); if (result.ok) { return result.val; @@ -67,14 +67,11 @@ export class UserBackendService { }; getWorkspaces = () => { - const payload = WorkspaceIdPB.fromObject({}); - - return FolderEventReadAllWorkspaces(payload); + return FolderEventReadCurrentWorkspace(); }; openWorkspace = (workspaceId: string) => { const payload = WorkspaceIdPB.fromObject({ value: workspaceId }); - return FolderEventOpenWorkspace(payload); }; @@ -115,9 +112,14 @@ export class AuthBackendService { return UserEventSignIn(payload); }; - signUp = (params: { name: string; email: string; password: string; }) => { + signUp = (params: { name: string; email: string; password: string }) => { const deviceId = nanoid(8); - const payload = SignUpPayloadPB.fromObject({ name: params.name, email: params.email, password: params.password, device_id: deviceId }); + const payload = SignUpPayloadPB.fromObject({ + name: params.name, + email: params.email, + password: params.password, + device_id: deviceId, + }); return UserEventSignUp(payload); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/workspace_bd_svc.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/workspace_bd_svc.ts index aab16ba0dcd6..5b57e534beff 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/workspace_bd_svc.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/workspace_bd_svc.ts @@ -1,12 +1,11 @@ import { FolderEventCreateWorkspace, - FolderEventGetCurrentWorkspace, CreateWorkspacePayloadPB, - FolderEventReadAllWorkspaces, FolderEventOpenWorkspace, FolderEventDeleteWorkspace, WorkspaceIdPB, FolderEventReadWorkspaceViews, + FolderEventReadCurrentWorkspace, } from '@/services/backend/events/flowy-folder2'; export class WorkspaceBackendService { @@ -41,14 +40,11 @@ export class WorkspaceBackendService { }; getWorkspaces = async () => { - // if workspaceId is not provided, it will return all workspaces - const workspaceId = new WorkspaceIdPB(); - - return FolderEventReadAllWorkspaces(workspaceId); + return FolderEventReadCurrentWorkspace(); }; getCurrentWorkspace = async () => { - return FolderEventGetCurrentWorkspace(); + return FolderEventReadCurrentWorkspace(); }; getChildPages = async (workspaceId: string) => { diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/workspace_manager_controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/workspace_manager_controller.ts index 4e5becd343f2..d717a0749562 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/workspace_manager_controller.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/workspace_manager_controller.ts @@ -33,14 +33,14 @@ export class WorkspaceManagerController { const result = await this.backendService.getWorkspaces(); if (result.ok) { - const items = result.val.items; + const item = result.val; - return items.map((item) => { - return { + return [ + { id: item.id, name: item.name, - }; - }); + }, + ]; } return []; @@ -50,8 +50,7 @@ export class WorkspaceManagerController { const result = await this.backendService.getCurrentWorkspace(); if (result.ok) { - const workspace = result.val.workspace; - + const workspace = result.val; return { id: workspace.id, name: workspace.name, diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 036d08feffa9..2a88127c1142 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -452,7 +452,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" dependencies = [ "borsh-derive", - "hashbrown 0.13.2", + "hashbrown 0.12.3", ] [[package]] @@ -712,7 +712,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" dependencies = [ "anyhow", "async-trait", @@ -731,7 +731,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" dependencies = [ "anyhow", "async-trait", @@ -761,7 +761,7 @@ dependencies = [ [[package]] name = "collab-derive" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" dependencies = [ "proc-macro2", "quote", @@ -773,7 +773,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" dependencies = [ "anyhow", "collab", @@ -793,7 +793,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" dependencies = [ "anyhow", "bytes", @@ -807,7 +807,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" dependencies = [ "anyhow", "chrono", @@ -849,7 +849,7 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" dependencies = [ "async-trait", "bincode", @@ -870,7 +870,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" dependencies = [ "anyhow", "async-trait", @@ -897,7 +897,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=8861d7a#8861d7a45a2bda493f307483561d92e31fffff4c" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" dependencies = [ "anyhow", "collab", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 1c81d09ff4fa..93ae49c1b41d 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -92,11 +92,11 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c87 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "8861d7a" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "8861d7a" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "8861d7a" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "8861d7a" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "8861d7a" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "8861d7a" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "8861d7a" } -collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "8861d7a" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b05aa7e" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b05aa7e" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b05aa7e" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b05aa7e" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b05aa7e" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b05aa7e" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b05aa7e" } +collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b05aa7e" } diff --git a/frontend/rust-lib/event-integration/src/document/document_event.rs b/frontend/rust-lib/event-integration/src/document/document_event.rs index 2deb791064f4..866ce61154e8 100644 --- a/frontend/rust-lib/event-integration/src/document/document_event.rs +++ b/frontend/rust-lib/event-integration/src/document/document_event.rs @@ -37,7 +37,7 @@ impl DocumentEventTest { pub async fn create_document(&self) -> ViewPB { let core = &self.inner; - let current_workspace = core.get_current_workspace().await.workspace; + let current_workspace = core.get_current_workspace().await; let parent_id = current_workspace.id.clone(); let payload = CreateViewPayloadPB { diff --git a/frontend/rust-lib/event-integration/src/folder_event.rs b/frontend/rust-lib/event-integration/src/folder_event.rs index 5061cf3ce5e4..0dcc8d296719 100644 --- a/frontend/rust-lib/event-integration/src/folder_event.rs +++ b/frontend/rust-lib/event-integration/src/folder_event.rs @@ -47,12 +47,12 @@ impl EventIntegrationTest { .items } - pub async fn get_current_workspace(&self) -> WorkspaceSettingPB { + pub async fn get_current_workspace(&self) -> WorkspacePB { EventBuilder::new(self.clone()) - .event(FolderEvent::GetCurrentWorkspace) + .event(FolderEvent::ReadCurrentWorkspace) .async_send() .await - .parse::() + .parse::() } pub async fn get_all_workspace_views(&self) -> Vec { @@ -148,9 +148,9 @@ pub struct ViewTest { impl ViewTest { #[allow(dead_code)] pub async fn new(sdk: &EventIntegrationTest, layout: ViewLayoutPB, data: Vec) -> Self { - let workspace = create_workspace(sdk, "Workspace", "").await; + let workspace = sdk.folder_manager.get_current_workspace().await.unwrap(); let payload = WorkspaceIdPB { - value: Some(workspace.id.clone()), + value: workspace.id.clone(), }; let _ = EventBuilder::new(sdk.clone()) .event(OpenWorkspace) @@ -196,6 +196,7 @@ impl ViewTest { } } +#[allow(dead_code)] async fn create_workspace(sdk: &EventIntegrationTest, name: &str, desc: &str) -> WorkspacePB { let request = CreateWorkspacePayloadPB { name: name.to_owned(), diff --git a/frontend/rust-lib/event-integration/tests/database/local_test/test.rs b/frontend/rust-lib/event-integration/tests/database/local_test/test.rs index 5ed18809fc8d..0d4b944ff520 100644 --- a/frontend/rust-lib/event-integration/tests/database/local_test/test.rs +++ b/frontend/rust-lib/event-integration/tests/database/local_test/test.rs @@ -14,7 +14,7 @@ use lib_infra::util::timestamp; #[tokio::test] async fn get_database_id_event_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) .await; @@ -36,7 +36,7 @@ async fn get_database_id_event_test() { #[tokio::test] async fn get_database_event_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) .await; @@ -50,7 +50,7 @@ async fn get_database_event_test() { #[tokio::test] async fn get_field_event_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) .await; @@ -65,7 +65,7 @@ async fn get_field_event_test() { #[tokio::test] async fn create_field_event_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) .await; @@ -79,7 +79,7 @@ async fn create_field_event_test() { #[tokio::test] async fn delete_field_event_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) .await; @@ -100,7 +100,7 @@ async fn delete_field_event_test() { #[tokio::test] async fn delete_primary_field_event_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) .await; @@ -115,7 +115,7 @@ async fn delete_primary_field_event_test() { #[tokio::test] async fn update_field_type_event_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) .await; @@ -133,7 +133,7 @@ async fn update_field_type_event_test() { #[tokio::test] async fn update_primary_field_type_event_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) .await; @@ -152,7 +152,7 @@ async fn update_primary_field_type_event_test() { #[tokio::test] async fn duplicate_field_event_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) .await; @@ -170,7 +170,7 @@ async fn duplicate_field_event_test() { #[tokio::test] async fn duplicate_primary_field_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) .await; @@ -184,7 +184,7 @@ async fn duplicate_primary_field_test() { #[tokio::test] async fn get_primary_field_event_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) .await; @@ -197,7 +197,7 @@ async fn get_primary_field_event_test() { #[tokio::test] async fn create_row_event_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) .await; @@ -210,7 +210,7 @@ async fn create_row_event_test() { #[tokio::test] async fn delete_row_event_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) .await; @@ -233,7 +233,7 @@ async fn delete_row_event_test() { #[tokio::test] async fn get_row_event_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) .await; @@ -249,7 +249,7 @@ async fn get_row_event_test() { #[tokio::test] async fn update_row_meta_event_with_url_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) .await; @@ -277,7 +277,7 @@ async fn update_row_meta_event_with_url_test() { #[tokio::test] async fn update_row_meta_event_with_cover_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) .await; @@ -305,7 +305,7 @@ async fn update_row_meta_event_with_cover_test() { #[tokio::test] async fn delete_row_event_with_invalid_row_id_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) .await; @@ -318,7 +318,7 @@ async fn delete_row_event_with_invalid_row_id_test() { #[tokio::test] async fn duplicate_row_event_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) .await; @@ -335,7 +335,7 @@ async fn duplicate_row_event_test() { #[tokio::test] async fn duplicate_row_event_with_invalid_row_id_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) .await; @@ -352,7 +352,7 @@ async fn duplicate_row_event_with_invalid_row_id_test() { #[tokio::test] async fn move_row_event_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) .await; @@ -372,7 +372,7 @@ async fn move_row_event_test() { #[tokio::test] async fn move_row_event_test2() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) .await; @@ -392,7 +392,7 @@ async fn move_row_event_test2() { #[tokio::test] async fn move_row_event_with_invalid_row_id_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) .await; @@ -419,7 +419,7 @@ async fn move_row_event_with_invalid_row_id_test() { #[tokio::test] async fn update_text_cell_event_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) .await; @@ -449,7 +449,7 @@ async fn update_text_cell_event_test() { #[tokio::test] async fn update_checkbox_cell_event_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) .await; @@ -480,7 +480,7 @@ async fn update_checkbox_cell_event_test() { #[tokio::test] async fn update_single_select_cell_event_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) .await; @@ -507,7 +507,7 @@ async fn update_single_select_cell_event_test() { #[tokio::test] async fn update_date_cell_event_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) .await; @@ -544,7 +544,7 @@ async fn update_date_cell_event_test() { #[tokio::test] async fn update_date_cell_event_with_empty_time_str_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) .await; @@ -580,7 +580,7 @@ async fn update_date_cell_event_with_empty_time_str_test() { #[tokio::test] async fn create_checklist_field_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) .await; @@ -601,7 +601,7 @@ async fn create_checklist_field_test() { #[tokio::test] async fn update_checklist_cell_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) .await; @@ -658,7 +658,7 @@ async fn update_checklist_cell_test() { #[tokio::test] async fn get_groups_event_with_grid_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my board view".to_owned(), vec![]) .await; @@ -670,7 +670,7 @@ async fn get_groups_event_with_grid_test() { #[tokio::test] async fn get_groups_event_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let board_view = test .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) .await; @@ -682,7 +682,7 @@ async fn get_groups_event_test() { #[tokio::test] async fn move_group_event_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let board_view = test .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) .await; @@ -716,7 +716,7 @@ async fn move_group_event_test() { #[tokio::test] async fn move_group_event_with_invalid_id_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let board_view = test .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) .await; @@ -738,7 +738,7 @@ async fn move_group_event_with_invalid_id_test() { #[tokio::test] async fn rename_group_event_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let board_view = test .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) .await; @@ -763,7 +763,7 @@ async fn rename_group_event_test() { #[tokio::test] async fn hide_group_event_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let board_view = test .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) .await; @@ -791,7 +791,7 @@ async fn hide_group_event_test() { #[tokio::test] async fn update_database_layout_event_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) .await; @@ -813,7 +813,7 @@ async fn update_database_layout_event_test() { #[tokio::test] async fn update_database_layout_event_test2() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) .await; @@ -845,7 +845,7 @@ async fn update_database_layout_event_test2() { #[tokio::test] async fn set_group_by_checkbox_field_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let board_view = test .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) .await; @@ -862,7 +862,7 @@ async fn set_group_by_checkbox_field_test() { #[tokio::test] async fn get_all_calendar_event_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let calendar_view = test .create_calendar(¤t_workspace.id, "my calendar view".to_owned(), vec![]) .await; @@ -875,7 +875,7 @@ async fn get_all_calendar_event_test() { #[tokio::test] async fn create_calendar_event_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let calendar_view = test .create_calendar(¤t_workspace.id, "my calendar view".to_owned(), vec![]) .await; diff --git a/frontend/rust-lib/event-integration/tests/database/supabase_test/helper.rs b/frontend/rust-lib/event-integration/tests/database/supabase_test/helper.rs index 5599f78cdec2..19d5d615b7df 100644 --- a/frontend/rust-lib/event-integration/tests/database/supabase_test/helper.rs +++ b/frontend/rust-lib/event-integration/tests/database/supabase_test/helper.rs @@ -38,11 +38,7 @@ impl FlowySupabaseDatabaseTest { let current_workspace = self.inner.get_current_workspace().await; let view = self .inner - .create_grid( - ¤t_workspace.workspace.id, - "my database".to_string(), - vec![], - ) + .create_grid(¤t_workspace.id, "my database".to_string(), vec![]) .await; let database = self.inner.get_database(&view.id).await; (view, database) diff --git a/frontend/rust-lib/event-integration/tests/document/af_cloud_test/util.rs b/frontend/rust-lib/event-integration/tests/document/af_cloud_test/util.rs index eef1023f48b0..c39b36d481ea 100644 --- a/frontend/rust-lib/event-integration/tests/document/af_cloud_test/util.rs +++ b/frontend/rust-lib/event-integration/tests/document/af_cloud_test/util.rs @@ -18,11 +18,7 @@ impl AFCloudDocumentTest { let current_workspace = self.inner.get_current_workspace().await; let view = self .inner - .create_document( - ¤t_workspace.workspace.id, - "my document".to_string(), - vec![], - ) + .create_document(¤t_workspace.id, "my document".to_string(), vec![]) .await; view.id } diff --git a/frontend/rust-lib/event-integration/tests/document/supabase_test/file_test.rs b/frontend/rust-lib/event-integration/tests/document/supabase_test/file_test.rs index 02c0e870c78c..5c4b9c860955 100644 --- a/frontend/rust-lib/event-integration/tests/document/supabase_test/file_test.rs +++ b/frontend/rust-lib/event-integration/tests/document/supabase_test/file_test.rs @@ -12,7 +12,7 @@ use crate::document::supabase_test::helper::FlowySupabaseDocumentTest; #[tokio::test] async fn supabase_document_upload_text_file_test() { if let Some(test) = FlowySupabaseDocumentTest::new().await { - let workspace_id = test.get_current_workspace().await.workspace.id; + let workspace_id = test.get_current_workspace().await.id; let storage_service = test .document_manager .get_file_storage_service() @@ -43,7 +43,7 @@ async fn supabase_document_upload_text_file_test() { #[tokio::test] async fn supabase_document_upload_zip_file_test() { if let Some(test) = FlowySupabaseDocumentTest::new().await { - let workspace_id = test.get_current_workspace().await.workspace.id; + let workspace_id = test.get_current_workspace().await.id; let storage_service = test .document_manager .get_file_storage_service() @@ -85,7 +85,7 @@ async fn supabase_document_upload_zip_file_test() { #[tokio::test] async fn supabase_document_upload_image_test() { if let Some(test) = FlowySupabaseDocumentTest::new().await { - let workspace_id = test.get_current_workspace().await.workspace.id; + let workspace_id = test.get_current_workspace().await.id; let storage_service = test .document_manager .get_file_storage_service() diff --git a/frontend/rust-lib/event-integration/tests/document/supabase_test/helper.rs b/frontend/rust-lib/event-integration/tests/document/supabase_test/helper.rs index 5ddd359a3c4a..cc3344bd4428 100644 --- a/frontend/rust-lib/event-integration/tests/document/supabase_test/helper.rs +++ b/frontend/rust-lib/event-integration/tests/document/supabase_test/helper.rs @@ -23,11 +23,7 @@ impl FlowySupabaseDocumentTest { let current_workspace = self.inner.get_current_workspace().await; self .inner - .create_document( - ¤t_workspace.workspace.id, - "my document".to_string(), - vec![], - ) + .create_document(¤t_workspace.id, "my document".to_string(), vec![]) .await } diff --git a/frontend/rust-lib/event-integration/tests/folder/local_test/folder_test.rs b/frontend/rust-lib/event-integration/tests/folder/local_test/folder_test.rs index 2cb4c93fe958..9323e36a3d78 100644 --- a/frontend/rust-lib/event-integration/tests/folder/local_test/folder_test.rs +++ b/frontend/rust-lib/event-integration/tests/folder/local_test/folder_test.rs @@ -1,53 +1,10 @@ -use collab_folder::core::ViewLayout; +use collab_folder::ViewLayout; use flowy_folder2::entities::icon::{ViewIconPB, ViewIconTypePB}; use crate::folder::local_test::script::FolderScript::*; use crate::folder::local_test::script::FolderTest; -#[tokio::test] -async fn read_all_workspace_test() { - let mut test = FolderTest::new().await; - test.run_scripts(vec![ReadAllWorkspaces]).await; - assert!(!test.all_workspace.is_empty()); -} - -#[tokio::test] -async fn create_workspace_test() { - let mut test = FolderTest::new().await; - let name = "My new workspace".to_owned(); - let desc = "Daily routines".to_owned(); - test - .run_scripts(vec![CreateWorkspace { - name: name.clone(), - desc: desc.clone(), - }]) - .await; - - let workspace = test.workspace.clone(); - assert_eq!(workspace.name, name); - - test - .run_scripts(vec![ - ReadWorkspace(Some(workspace.id.clone())), - AssertWorkspace(workspace), - ]) - .await; -} - -#[tokio::test] -async fn get_workspace_test() { - let mut test = FolderTest::new().await; - let workspace = test.workspace.clone(); - - test - .run_scripts(vec![ - ReadWorkspace(Some(workspace.id.clone())), - AssertWorkspace(workspace), - ]) - .await; -} - #[tokio::test] async fn create_parent_view_test() { let mut test = FolderTest::new().await; diff --git a/frontend/rust-lib/event-integration/tests/folder/local_test/script.rs b/frontend/rust-lib/event-integration/tests/folder/local_test/script.rs index 82d0db03d0d4..bf1c00e8833f 100644 --- a/frontend/rust-lib/event-integration/tests/folder/local_test/script.rs +++ b/frontend/rust-lib/event-integration/tests/folder/local_test/script.rs @@ -1,4 +1,4 @@ -use collab_folder::core::ViewLayout; +use collab_folder::ViewLayout; use event_integration::event_builder::EventBuilder; use event_integration::EventIntegrationTest; @@ -7,14 +7,15 @@ use flowy_folder2::entities::*; use flowy_folder2::event_map::FolderEvent::*; pub enum FolderScript { - // Workspace - ReadAllWorkspaces, + #[allow(dead_code)] CreateWorkspace { name: String, desc: String, }, + #[allow(dead_code)] AssertWorkspace(WorkspacePB), - ReadWorkspace(Option), + #[allow(dead_code)] + ReadWorkspace(String), // App CreateParentView { @@ -65,7 +66,6 @@ pub enum FolderScript { pub struct FolderTest { pub sdk: EventIntegrationTest, - pub all_workspace: Vec, pub workspace: WorkspacePB, pub parent_view: ViewPB, pub child_view: ViewPB, @@ -77,8 +77,15 @@ impl FolderTest { pub async fn new() -> Self { let sdk = EventIntegrationTest::new().await; let _ = sdk.init_anon_user().await; - let workspace = create_workspace(&sdk, "FolderWorkspace", "Folder test workspace").await; - let parent_view = create_app(&sdk, &workspace.id, "Folder App", "Folder test app").await; + let workspace = sdk.folder_manager.get_current_workspace().await.unwrap(); + let parent_view = create_view( + &sdk, + &workspace.id, + "Folder App", + "Folder test app", + ViewLayout::Document, + ) + .await; let view = create_view( &sdk, &parent_view.id, @@ -89,7 +96,6 @@ impl FolderTest { .await; Self { sdk, - all_workspace: vec![], workspace, parent_view, child_view: view, @@ -107,10 +113,6 @@ impl FolderTest { pub async fn run_script(&mut self, script: FolderScript) { let sdk = &self.sdk; match script { - FolderScript::ReadAllWorkspaces => { - let all_workspace = read_workspace(sdk, None).await; - self.all_workspace = all_workspace; - }, FolderScript::CreateWorkspace { name, desc } => { let workspace = create_workspace(sdk, &name, &desc).await; self.workspace = workspace; @@ -119,11 +121,11 @@ impl FolderTest { assert_eq!(self.workspace, workspace, "Workspace not equal"); }, FolderScript::ReadWorkspace(workspace_id) => { - let workspace = read_workspace(sdk, workspace_id).await.pop().unwrap(); + let workspace = read_workspace(sdk, workspace_id).await; self.workspace = workspace; }, FolderScript::CreateParentView { name, desc } => { - let app = create_app(sdk, &self.workspace.id, &name, &desc).await; + let app = create_view(sdk, &self.workspace.id, &name, &desc, ViewLayout::Document).await; self.parent_view = app; }, FolderScript::AssertParentView(app) => { @@ -215,70 +217,27 @@ pub async fn create_workspace(sdk: &EventIntegrationTest, name: &str, desc: &str .parse::() } -pub async fn read_workspace( - sdk: &EventIntegrationTest, - workspace_id: Option, -) -> Vec { +pub async fn read_workspace(sdk: &EventIntegrationTest, workspace_id: String) -> WorkspacePB { let request = WorkspaceIdPB { value: workspace_id, }; - let repeated_workspace = EventBuilder::new(sdk.clone()) - .event(ReadAllWorkspaces) - .payload(request.clone()) - .async_send() - .await - .parse::(); - - let workspaces; - if let Some(workspace_id) = &request.value { - workspaces = repeated_workspace - .items - .into_iter() - .filter(|workspace| &workspace.id == workspace_id) - .collect::>(); - debug_assert_eq!(workspaces.len(), 1); - } else { - workspaces = repeated_workspace.items; - } - - workspaces -} - -pub async fn create_app( - sdk: &EventIntegrationTest, - workspace_id: &str, - name: &str, - desc: &str, -) -> ViewPB { - let create_view_request = CreateViewPayloadPB { - parent_view_id: workspace_id.to_owned(), - name: name.to_string(), - desc: desc.to_string(), - thumbnail: None, - layout: ViewLayout::Document.into(), - initial_data: vec![], - meta: Default::default(), - set_as_current: true, - index: None, - }; - EventBuilder::new(sdk.clone()) - .event(CreateView) - .payload(create_view_request) + .event(ReadCurrentWorkspace) + .payload(request.clone()) .async_send() .await - .parse::() + .parse::() } pub async fn create_view( sdk: &EventIntegrationTest, - app_id: &str, + parent_view_id: &str, name: &str, desc: &str, layout: ViewLayout, ) -> ViewPB { let request = CreateViewPayloadPB { - parent_view_id: app_id.to_string(), + parent_view_id: parent_view_id.to_string(), name: name.to_string(), desc: desc.to_string(), thumbnail: None, diff --git a/frontend/rust-lib/event-integration/tests/folder/local_test/subscription_test.rs b/frontend/rust-lib/event-integration/tests/folder/local_test/subscription_test.rs index 0ad2deaa0f1f..6162095014fa 100644 --- a/frontend/rust-lib/event-integration/tests/folder/local_test/subscription_test.rs +++ b/frontend/rust-lib/event-integration/tests/folder/local_test/subscription_test.rs @@ -17,7 +17,7 @@ use crate::util::receive_with_timeout; /// 6. Ensure that the received views contain the newly created "test_view". async fn create_child_view_in_workspace_subscription_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let workspace = test.get_current_workspace().await.workspace; + let workspace = test.get_current_workspace().await; let rx = test .notification_sender .subscribe::(&workspace.id, FolderNotification::DidUpdateWorkspaceViews); @@ -41,7 +41,7 @@ async fn create_child_view_in_workspace_subscription_test() { #[tokio::test] async fn create_child_view_in_view_subscription_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let mut workspace = test.get_current_workspace().await.workspace; + let mut workspace = test.get_current_workspace().await; let workspace_child_view = workspace.views.pop().unwrap(); let rx = test.notification_sender.subscribe::( &workspace_child_view.id, @@ -73,7 +73,7 @@ async fn create_child_view_in_view_subscription_test() { #[tokio::test] async fn delete_view_subscription_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let workspace = test.get_current_workspace().await.workspace; + let workspace = test.get_current_workspace().await; let rx = test .notification_sender .subscribe::(&workspace.id, FolderNotification::DidUpdateChildViews); @@ -104,7 +104,7 @@ async fn delete_view_subscription_test() { #[tokio::test] async fn update_view_subscription_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let mut workspace = test.get_current_workspace().await.workspace; + let mut workspace = test.get_current_workspace().await; let rx = test .notification_sender .subscribe::(&workspace.id, FolderNotification::DidUpdateChildViews); diff --git a/frontend/rust-lib/event-integration/tests/folder/local_test/test.rs b/frontend/rust-lib/event-integration/tests/folder/local_test/test.rs index 82215040d620..24b64110c3be 100644 --- a/frontend/rust-lib/event-integration/tests/folder/local_test/test.rs +++ b/frontend/rust-lib/event-integration/tests/folder/local_test/test.rs @@ -16,44 +16,45 @@ async fn create_workspace_event_test() { .payload(request) .async_send() .await - .parse::(); - assert_eq!(resp.name, "my second workspace"); + .error() + .unwrap(); + assert_eq!(resp.code, ErrorCode::NotSupportYet); } -#[tokio::test] -async fn open_workspace_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; - let payload = CreateWorkspacePayloadPB { - name: "my second workspace".to_owned(), - desc: "".to_owned(), - }; - // create a workspace - let resp_1 = EventBuilder::new(test.clone()) - .event(flowy_folder2::event_map::FolderEvent::CreateWorkspace) - .payload(payload) - .async_send() - .await - .parse::(); - - // open the workspace - let payload = WorkspaceIdPB { - value: Some(resp_1.id.clone()), - }; - let resp_2 = EventBuilder::new(test) - .event(flowy_folder2::event_map::FolderEvent::OpenWorkspace) - .payload(payload) - .async_send() - .await - .parse::(); - - assert_eq!(resp_1.id, resp_2.id); - assert_eq!(resp_1.name, resp_2.name); -} +// #[tokio::test] +// async fn open_workspace_event_test() { +// let test = EventIntegrationTest::new_with_guest_user().await; +// let payload = CreateWorkspacePayloadPB { +// name: "my second workspace".to_owned(), +// desc: "".to_owned(), +// }; +// // create a workspace +// let resp_1 = EventBuilder::new(test.clone()) +// .event(flowy_folder2::event_map::FolderEvent::CreateWorkspace) +// .payload(payload) +// .async_send() +// .await +// .parse::(); +// +// // open the workspace +// let payload = WorkspaceIdPB { +// value: Some(resp_1.id.clone()), +// }; +// let resp_2 = EventBuilder::new(test) +// .event(flowy_folder2::event_map::FolderEvent::OpenWorkspace) +// .payload(payload) +// .async_send() +// .await +// .parse::(); +// +// assert_eq!(resp_1.id, resp_2.id); +// assert_eq!(resp_1.name, resp_2.name); +// } #[tokio::test] async fn create_view_event_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let view = test .create_view(¤t_workspace.id, "My first view".to_string()) .await; @@ -65,7 +66,7 @@ async fn create_view_event_test() { #[tokio::test] async fn update_view_event_with_name_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let view = test .create_view(¤t_workspace.id, "My first view".to_string()) .await; @@ -86,7 +87,7 @@ async fn update_view_event_with_name_test() { #[tokio::test] async fn update_view_icon_event_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let view = test .create_view(¤t_workspace.id, "My first view".to_string()) .await; @@ -110,7 +111,7 @@ async fn update_view_icon_event_test() { #[tokio::test] async fn delete_view_event_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let view = test .create_view(¤t_workspace.id, "My first view".to_string()) .await; @@ -133,7 +134,7 @@ async fn delete_view_event_test() { #[tokio::test] async fn put_back_trash_event_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let view = test .create_view(¤t_workspace.id, "My first view".to_string()) .await; @@ -176,7 +177,7 @@ async fn put_back_trash_event_test() { #[tokio::test] async fn delete_view_permanently_event_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let view = test .create_view(¤t_workspace.id, "My first view".to_string()) .await; @@ -225,7 +226,7 @@ async fn delete_view_permanently_event_test() { #[tokio::test] async fn delete_all_trash_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; for i in 0..3 { let view = test @@ -269,7 +270,7 @@ async fn delete_all_trash_test() { #[tokio::test] async fn multiple_hierarchy_view_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; for i in 1..4 { let parent = test .create_view(¤t_workspace.id, format!("My {} view", i)) @@ -345,7 +346,7 @@ async fn multiple_hierarchy_view_test() { #[tokio::test] async fn move_view_event_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; for i in 1..4 { let parent = test .create_view(¤t_workspace.id, format!("My {} view", i)) @@ -383,7 +384,7 @@ async fn move_view_event_test() { #[tokio::test] async fn move_view_event_after_delete_view_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; for i in 1..6 { let _ = test .create_view(¤t_workspace.id, format!("My {} view", i)) @@ -425,7 +426,7 @@ async fn move_view_event_after_delete_view_test() { #[tokio::test] async fn move_view_event_after_delete_view_test2() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let parent = test .create_view(¤t_workspace.id, "My view".to_string()) .await; @@ -495,7 +496,7 @@ fn invalid_workspace_name_test_case() -> Vec<(String, ErrorCode)> { #[tokio::test] async fn move_view_across_parent_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let parent_1 = test .create_view(¤t_workspace.id, "My view 1".to_string()) .await; diff --git a/frontend/rust-lib/event-integration/tests/folder/supabase_test/helper.rs b/frontend/rust-lib/event-integration/tests/folder/supabase_test/helper.rs index eac5cb418a3f..171e65fee3d2 100644 --- a/frontend/rust-lib/event-integration/tests/folder/supabase_test/helper.rs +++ b/frontend/rust-lib/event-integration/tests/folder/supabase_test/helper.rs @@ -5,7 +5,7 @@ use collab::core::collab::MutexCollab; use collab::core::origin::CollabOrigin; use collab::preclude::updates::decoder::Decode; use collab::preclude::{merge_updates_v1, JsonValue, Update}; -use collab_folder::core::FolderData; +use collab_folder::FolderData; use event_integration::event_builder::EventBuilder; use flowy_folder2::entities::{FolderSnapshotPB, RepeatedFolderSnapshotPB, WorkspaceIdPB}; @@ -39,7 +39,7 @@ impl FlowySupabaseFolderTest { EventBuilder::new(self.inner.deref().clone()) .event(GetFolderSnapshots) .payload(WorkspaceIdPB { - value: Some(workspace_id.to_string()), + value: workspace_id.to_string(), }) .async_send() .await diff --git a/frontend/rust-lib/event-integration/tests/folder/supabase_test/test.rs b/frontend/rust-lib/event-integration/tests/folder/supabase_test/test.rs index d04937d70dad..4ac2f5446956 100644 --- a/frontend/rust-lib/event-integration/tests/folder/supabase_test/test.rs +++ b/frontend/rust-lib/event-integration/tests/folder/supabase_test/test.rs @@ -12,11 +12,12 @@ use crate::util::{get_folder_data_from_server, receive_with_timeout}; #[tokio::test] async fn supabase_encrypt_folder_test() { if let Some(test) = FlowySupabaseFolderTest::new().await { + let uid = test.user_manager.user_id().unwrap(); let secret = test.enable_encryption().await; let local_folder_data = test.get_local_folder_data().await; - let workspace_id = test.get_current_workspace().await.workspace.id; - let remote_folder_data = get_folder_data_from_server(&workspace_id, Some(secret)) + let workspace_id = test.get_current_workspace().await.id; + let remote_folder_data = get_folder_data_from_server(&uid, &workspace_id, Some(secret)) .await .unwrap() .unwrap(); @@ -28,8 +29,9 @@ async fn supabase_encrypt_folder_test() { #[tokio::test] async fn supabase_decrypt_folder_data_test() { if let Some(test) = FlowySupabaseFolderTest::new().await { + let uid = test.user_manager.user_id().unwrap(); let secret = Some(test.enable_encryption().await); - let workspace_id = test.get_current_workspace().await.workspace.id; + let workspace_id = test.get_current_workspace().await.id; test .create_view(&workspace_id, "encrypt view".to_string()) .await; @@ -41,7 +43,7 @@ async fn supabase_decrypt_folder_data_test() { receive_with_timeout(rx, Duration::from_secs(10)) .await .unwrap(); - let folder_data = get_folder_data_from_server(&workspace_id, secret) + let folder_data = get_folder_data_from_server(&uid, &workspace_id, secret) .await .unwrap() .unwrap(); @@ -54,8 +56,9 @@ async fn supabase_decrypt_folder_data_test() { #[should_panic] async fn supabase_decrypt_with_invalid_secret_folder_data_test() { if let Some(test) = FlowySupabaseFolderTest::new().await { + let uid = test.user_manager.user_id().unwrap(); let _ = Some(test.enable_encryption().await); - let workspace_id = test.get_current_workspace().await.workspace.id; + let workspace_id = test.get_current_workspace().await.id; test .create_view(&workspace_id, "encrypt view".to_string()) .await; @@ -66,7 +69,7 @@ async fn supabase_decrypt_with_invalid_secret_folder_data_test() { .await .unwrap(); - let _ = get_folder_data_from_server(&workspace_id, Some("invalid secret".to_string())) + let _ = get_folder_data_from_server(&uid, &workspace_id, Some("invalid secret".to_string())) .await .unwrap(); } @@ -74,7 +77,7 @@ async fn supabase_decrypt_with_invalid_secret_folder_data_test() { #[tokio::test] async fn supabase_folder_snapshot_test() { if let Some(test) = FlowySupabaseFolderTest::new().await { - let workspace_id = test.get_current_workspace().await.workspace.id; + let workspace_id = test.get_current_workspace().await.id; let rx = test .notification_sender .subscribe::(&workspace_id, DidUpdateFolderSnapshotState); @@ -92,7 +95,7 @@ async fn supabase_folder_snapshot_test() { #[tokio::test] async fn supabase_initial_folder_snapshot_test2() { if let Some(test) = FlowySupabaseFolderTest::new().await { - let workspace_id = test.get_current_workspace().await.workspace.id; + let workspace_id = test.get_current_workspace().await.id; test .create_view(&workspace_id, "supabase test view1".to_string()) diff --git a/frontend/rust-lib/event-integration/tests/user/migration_test/history_user_db/036_fav_v1_workspace_array.zip b/frontend/rust-lib/event-integration/tests/user/migration_test/history_user_db/036_fav_v1_workspace_array.zip new file mode 100644 index 0000000000000000000000000000000000000000..2fa72a1e649e2beaf59e936e4409ee99a8572580 GIT binary patch literal 47700 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 J7|fS~cmR7%@lgN( literal 0 HcmV?d00001 diff --git a/frontend/rust-lib/event-integration/tests/user/migration_test/version_test.rs b/frontend/rust-lib/event-integration/tests/user/migration_test/version_test.rs index dcaed72a0dbd..5b38c29d0dc2 100644 --- a/frontend/rust-lib/event-integration/tests/user/migration_test/version_test.rs +++ b/frontend/rust-lib/event-integration/tests/user/migration_test/version_test.rs @@ -38,3 +38,25 @@ async fn migrate_020_historical_empty_document_test() { assert_eq!(database.rows.len(), 3); drop(cleaner); } + +#[tokio::test] +async fn migrate_036_fav_v1_workspace_array_test() { + // Used to test migration: FavoriteV1AndWorkspaceArrayMigration + let (cleaner, user_db_path) = unzip_history_user_db( + "./tests/user/migration_test/history_user_db", + "036_fav_v1_workspace_array", + ) + .unwrap(); + let test = + EventIntegrationTest::new_with_user_data_path(user_db_path, DEFAULT_NAME.to_string()).await; + + let views = test.get_all_workspace_views().await; + assert_eq!(views.len(), 2); + assert_eq!(views[0].name, "root page"); + assert_eq!(views[1].name, "⭐\u{fe0f} Getting started"); + + let views = test.get_views(&views[1].id).await; + assert_eq!(views.child_views.len(), 3); + assert!(views.child_views[2].is_favorite); + drop(cleaner); +} diff --git a/frontend/rust-lib/event-integration/tests/user/supabase_test/auth_test.rs b/frontend/rust-lib/event-integration/tests/user/supabase_test/auth_test.rs index 5375d5f33952..53a0d7117973 100644 --- a/frontend/rust-lib/event-integration/tests/user/supabase_test/auth_test.rs +++ b/frontend/rust-lib/event-integration/tests/user/supabase_test/auth_test.rs @@ -4,7 +4,7 @@ use assert_json_diff::assert_json_eq; use collab_database::rows::database_row_document_id_from_row_id; use collab_document::blocks::DocumentData; use collab_entity::CollabType; -use collab_folder::core::FolderData; +use collab_folder::FolderData; use nanoid::nanoid; use serde_json::json; @@ -303,21 +303,12 @@ async fn migrate_anon_data_on_cloud_signup() { let folder_data: FolderData = test .folder_manager .get_cloud_service() - .get_folder_data(&user_profile.workspace_id) + .get_folder_data(&user_profile.workspace_id, &user_profile.id) .await .unwrap() .unwrap(); let expected_folder_data = expected_workspace_sync_folder_data(); - - if folder_data.workspaces.len() != expected_folder_data.workspaces.len() { - dbg!(&folder_data.workspaces); - } - - assert_eq!( - folder_data.workspaces.len(), - expected_folder_data.workspaces.len() - ); assert_eq!(folder_data.views.len(), expected_folder_data.views.len()); // After migration, the ids of the folder_data should be different from the expected_folder_data @@ -329,10 +320,7 @@ async fn migrate_anon_data_on_cloud_signup() { assert_eq!(left_view.name, right_view.name); } - assert_ne!( - folder_data.current_workspace_id, - expected_folder_data.current_workspace_id - ); + assert_ne!(folder_data.workspace.id, expected_folder_data.workspace.id); assert_ne!(folder_data.current_view, expected_folder_data.current_view); let database_views = folder_data diff --git a/frontend/rust-lib/event-integration/tests/user/supabase_test/workspace_test.rs b/frontend/rust-lib/event-integration/tests/user/supabase_test/workspace_test.rs index 2a971ef63635..543e76f26201 100644 --- a/frontend/rust-lib/event-integration/tests/user/supabase_test/workspace_test.rs +++ b/frontend/rust-lib/event-integration/tests/user/supabase_test/workspace_test.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use event_integration::{event_builder::EventBuilder, EventIntegrationTest}; use flowy_folder2::entities::WorkspaceSettingPB; -use flowy_folder2::event_map::FolderEvent::GetCurrentWorkspace; +use flowy_folder2::event_map::FolderEvent::GetCurrentWorkspaceSetting; use flowy_server::supabase::define::{USER_EMAIL, USER_UUID}; use flowy_user::entities::{AuthTypePB, OauthSignInPB, UserProfilePB}; use flowy_user::event_map::UserEvent::*; @@ -32,7 +32,7 @@ async fn initial_workspace_test() { .parse::(); let workspace_settings = EventBuilder::new(test.clone()) - .event(GetCurrentWorkspace) + .event(GetCurrentWorkspaceSetting) .async_send() .await .parse::(); diff --git a/frontend/rust-lib/event-integration/tests/util.rs b/frontend/rust-lib/event-integration/tests/util.rs index 7653b39c4c1e..97c52a303af0 100644 --- a/frontend/rust-lib/event-integration/tests/util.rs +++ b/frontend/rust-lib/event-integration/tests/util.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use std::time::Duration; use anyhow::Error; -use collab_folder::core::FolderData; +use collab_folder::FolderData; use collab_plugins::cloud_storage::RemoteCollabStorage; use nanoid::nanoid; use tokio::sync::mpsc::Receiver; @@ -135,11 +135,12 @@ pub fn encryption_collab_service( } pub async fn get_folder_data_from_server( + uid: &i64, folder_id: &str, encryption_secret: Option, ) -> Result, Error> { let (cloud_service, _encryption) = encryption_folder_service(encryption_secret); - cloud_service.get_folder_data(folder_id).await + cloud_service.get_folder_data(folder_id, uid).await } pub async fn get_folder_snapshots( diff --git a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs index bd4df22ff4b5..de9eb2c22bc5 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs @@ -140,13 +140,18 @@ impl FolderCloudService for ServerProvider { FutureResult::new(async move { server?.folder_service().create_workspace(uid, &name).await }) } - fn get_folder_data(&self, workspace_id: &str) -> FutureResult, Error> { + fn get_folder_data( + &self, + workspace_id: &str, + uid: &i64, + ) -> FutureResult, Error> { + let uid = *uid; let server = self.get_server(&self.get_server_type()); let workspace_id = workspace_id.to_string(); FutureResult::new(async move { server? .folder_service() - .get_folder_data(&workspace_id) + .get_folder_data(&workspace_id, &uid) .await }) } diff --git a/frontend/rust-lib/flowy-error/src/code.rs b/frontend/rust-lib/flowy-error/src/code.rs index 1675d6c5a792..25c5e9242b6f 100644 --- a/frontend/rust-lib/flowy-error/src/code.rs +++ b/frontend/rust-lib/flowy-error/src/code.rs @@ -256,6 +256,9 @@ pub enum ErrorCode { #[error("Internal server error")] InternalServerError = 84, + + #[error("Not support yet")] + NotSupportYet = 85, } impl ErrorCode { diff --git a/frontend/rust-lib/flowy-error/src/errors.rs b/frontend/rust-lib/flowy-error/src/errors.rs index 358ed13eb8cc..c96094b35b71 100644 --- a/frontend/rust-lib/flowy-error/src/errors.rs +++ b/frontend/rust-lib/flowy-error/src/errors.rs @@ -101,6 +101,7 @@ impl FlowyError { ); static_flowy_error!(collab_not_sync, ErrorCode::CollabDataNotSync); static_flowy_error!(server_error, ErrorCode::InternalServerError); + static_flowy_error!(not_support, ErrorCode::NotSupportYet); } impl std::convert::From for FlowyError { diff --git a/frontend/rust-lib/flowy-folder-deps/src/cloud.rs b/frontend/rust-lib/flowy-folder-deps/src/cloud.rs index 48985c3cd924..98ec0bf8c977 100644 --- a/frontend/rust-lib/flowy-folder-deps/src/cloud.rs +++ b/frontend/rust-lib/flowy-folder-deps/src/cloud.rs @@ -1,5 +1,5 @@ pub use anyhow::Error; -pub use collab_folder::core::{Folder, FolderData, Workspace}; +pub use collab_folder::{Folder, FolderData, Workspace}; use uuid::Uuid; use lib_infra::future::FutureResult; @@ -8,7 +8,11 @@ use lib_infra::future::FutureResult; pub trait FolderCloudService: Send + Sync + 'static { fn create_workspace(&self, uid: i64, name: &str) -> FutureResult; - fn get_folder_data(&self, workspace_id: &str) -> FutureResult, Error>; + fn get_folder_data( + &self, + workspace_id: &str, + uid: &i64, + ) -> FutureResult, Error>; fn get_folder_snapshots( &self, diff --git a/frontend/rust-lib/flowy-folder2/src/entities/icon.rs b/frontend/rust-lib/flowy-folder2/src/entities/icon.rs index 08b8980209d7..2342b0224652 100644 --- a/frontend/rust-lib/flowy-folder2/src/entities/icon.rs +++ b/frontend/rust-lib/flowy-folder2/src/entities/icon.rs @@ -1,5 +1,5 @@ use crate::entities::parser::view::ViewIdentify; -use collab_folder::core::{IconType, ViewIcon}; +use collab_folder::{IconType, ViewIcon}; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; diff --git a/frontend/rust-lib/flowy-folder2/src/entities/trash.rs b/frontend/rust-lib/flowy-folder2/src/entities/trash.rs index b1158dbe079e..183324c8dbb6 100644 --- a/frontend/rust-lib/flowy-folder2/src/entities/trash.rs +++ b/frontend/rust-lib/flowy-folder2/src/entities/trash.rs @@ -1,4 +1,4 @@ -use collab_folder::core::TrashInfo; +use collab_folder::TrashInfo; use flowy_derive::ProtoBuf; #[derive(Eq, PartialEq, ProtoBuf, Default, Debug, Clone)] diff --git a/frontend/rust-lib/flowy-folder2/src/entities/view.rs b/frontend/rust-lib/flowy-folder2/src/entities/view.rs index f75f06980dec..f12048791e75 100644 --- a/frontend/rust-lib/flowy-folder2/src/entities/view.rs +++ b/frontend/rust-lib/flowy-folder2/src/entities/view.rs @@ -3,7 +3,7 @@ use std::convert::TryInto; use std::ops::{Deref, DerefMut}; use std::sync::Arc; -use collab_folder::core::{View, ViewLayout}; +use collab_folder::{View, ViewLayout}; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; diff --git a/frontend/rust-lib/flowy-folder2/src/entities/workspace.rs b/frontend/rust-lib/flowy-folder2/src/entities/workspace.rs index 0889d878bfba..6ce3328da695 100644 --- a/frontend/rust-lib/flowy-folder2/src/entities/workspace.rs +++ b/frontend/rust-lib/flowy-folder2/src/entities/workspace.rs @@ -1,7 +1,7 @@ use std::convert::TryInto; use collab::core::collab_state::SyncState; -use collab_folder::core::Workspace; +use collab_folder::Workspace; use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; @@ -38,16 +38,16 @@ impl std::convert::From<(Workspace, Vec)> for WorkspacePB { } } -impl std::convert::From for WorkspacePB { - fn from(workspace: Workspace) -> Self { - WorkspacePB { - id: workspace.id, - name: workspace.name, - views: Default::default(), - create_time: workspace.created_at, - } - } -} +// impl std::convert::From for WorkspacePB { +// fn from(workspace: Workspace) -> Self { +// WorkspacePB { +// id: workspace.id, +// name: workspace.name, +// views: Default::default(), +// create_time: workspace.created_at, +// } +// } +// } #[derive(PartialEq, Eq, Debug, Default, ProtoBuf)] pub struct RepeatedWorkspacePB { @@ -55,14 +55,9 @@ pub struct RepeatedWorkspacePB { pub items: Vec, } -impl From> for RepeatedWorkspacePB { - fn from(workspaces: Vec) -> Self { - Self { - items: workspaces - .into_iter() - .map(|workspace| workspace.into()) - .collect::>(), - } +impl From> for RepeatedWorkspacePB { + fn from(workspaces: Vec) -> Self { + Self { items: workspaces } } } @@ -98,22 +93,14 @@ impl TryInto for CreateWorkspacePayloadPB { // Read all workspaces if the workspace_id is None #[derive(Clone, ProtoBuf, Default, Debug)] pub struct WorkspaceIdPB { - #[pb(index = 1, one_of)] - pub value: Option, -} - -impl WorkspaceIdPB { - pub fn new(workspace_id: Option) -> Self { - Self { - value: workspace_id, - } - } + #[pb(index = 1)] + pub value: String, } #[derive(Default, ProtoBuf, Debug, Clone)] pub struct WorkspaceSettingPB { #[pb(index = 1)] - pub workspace: WorkspacePB, + pub workspace_id: String, #[pb(index = 2, one_of)] pub latest_view: Option, diff --git a/frontend/rust-lib/flowy-folder2/src/event_handler.rs b/frontend/rust-lib/flowy-folder2/src/event_handler.rs index fc586523a83a..97a119911e35 100644 --- a/frontend/rust-lib/flowy-folder2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-folder2/src/event_handler.rs @@ -24,7 +24,18 @@ pub(crate) async fn create_workspace_handler( let folder = upgrade_folder(folder)?; let params: CreateWorkspaceParams = data.into_inner().try_into()?; let workspace = folder.create_workspace(params).await?; - data_result_ok(workspace.into()) + let views = folder + .get_views_belong_to(&workspace.id) + .await? + .into_iter() + .map(view_pb_without_child_views) + .collect::>(); + data_result_ok(WorkspacePB { + id: workspace.id, + name: workspace.name, + views, + create_time: workspace.created_at, + }) } #[tracing::instrument(level = "debug", skip(folder), err)] @@ -43,52 +54,39 @@ pub(crate) async fn open_workspace_handler( folder: AFPluginState>, ) -> DataResult { let folder = upgrade_folder(folder)?; - let params: WorkspaceIdPB = data.into_inner(); - match params.value { - None => Err(FlowyError::workspace_id().with_context("workspace id should not be empty")), - Some(workspace_id) => { - if workspace_id.is_empty() { - Err(FlowyError::workspace_id().with_context("workspace id should not be empty")) - } else { - let workspace = folder.open_workspace(&workspace_id).await?; - let views = folder.get_workspace_views(&workspace_id).await?; - let workspace_pb: WorkspacePB = (workspace, views).into(); - data_result_ok(workspace_pb) - } - }, + let workspace_id = data.into_inner().value; + if workspace_id.is_empty() { + Err(FlowyError::workspace_id().with_context("workspace id should not be empty")) + } else { + let workspace = folder.open_workspace(&workspace_id).await?; + let views = folder.get_workspace_views(&workspace_id).await?; + let workspace_pb: WorkspacePB = (workspace, views).into(); + data_result_ok(workspace_pb) } } -#[tracing::instrument(level = "debug", skip(data, folder), err)] -pub(crate) async fn read_workspaces_handler( - data: AFPluginData, +#[tracing::instrument(level = "debug", skip(folder), err)] +pub(crate) async fn read_current_workspace_setting_handler( folder: AFPluginState>, -) -> DataResult { +) -> DataResult { let folder = upgrade_folder(folder)?; - let params: WorkspaceIdPB = data.into_inner(); - let workspaces = match params.value { - None => folder.get_all_workspaces().await, - Some(workspace_id) => folder - .get_workspace(&workspace_id) - .await - .map(|workspace| vec![workspace]) - .unwrap_or_default(), - }; - - data_result_ok(workspaces.into()) + let setting = folder + .get_workspace_setting_pb() + .await + .ok_or(FlowyError::record_not_found())?; + data_result_ok(setting) } #[tracing::instrument(level = "debug", skip(folder), err)] -pub async fn get_current_workspace_setting_handler( +pub(crate) async fn read_current_workspace_handler( folder: AFPluginState>, -) -> DataResult { +) -> DataResult { let folder = upgrade_folder(folder)?; - let workspace = folder.get_current_workspace().await?; - let latest_view: Option = folder.get_current_view().await; - data_result_ok(WorkspaceSettingPB { - workspace, - latest_view, - }) + let workspace = folder + .get_workspace_pb() + .await + .ok_or(FlowyError::record_not_found())?; + data_result_ok(workspace) } pub(crate) async fn create_view_handler( @@ -125,7 +123,7 @@ pub(crate) async fn read_view_handler( ) -> DataResult { let folder = upgrade_folder(folder)?; let view_id: ViewIdPB = data.into_inner(); - let view_pb = folder.get_view(&view_id.value).await?; + let view_pb = folder.get_view_pb(&view_id.value).await?; data_result_ok(view_pb) } @@ -239,17 +237,10 @@ pub(crate) async fn read_favorites_handler( let favorites = folder.get_all_favorites().await; let mut views = vec![]; for info in favorites { - let view = folder.get_view(&info.id).await; - match view { - Ok(view) => { - views.push(view); - }, - Err(err) => { - return Err(err); - }, + if let Ok(view) = folder.get_view_pb(&info.id).await { + views.push(view); } } - data_result_ok(RepeatedViewPB { items: views }) } #[tracing::instrument(level = "debug", skip(folder), err)] @@ -319,10 +310,7 @@ pub(crate) async fn get_folder_snapshots_handler( folder: AFPluginState>, ) -> DataResult { let folder = upgrade_folder(folder)?; - if let Some(workspace_id) = &data.value { - let snapshots = folder.get_folder_snapshots(workspace_id, 10).await?; - data_result_ok(RepeatedFolderSnapshotPB { items: snapshots }) - } else { - data_result_ok(RepeatedFolderSnapshotPB { items: vec![] }) - } + let data = data.into_inner(); + let snapshots = folder.get_folder_snapshots(&data.value, 10).await?; + data_result_ok(RepeatedFolderSnapshotPB { items: snapshots }) } diff --git a/frontend/rust-lib/flowy-folder2/src/event_map.rs b/frontend/rust-lib/flowy-folder2/src/event_map.rs index 0b2aefce153b..d26b2b6ef5a2 100644 --- a/frontend/rust-lib/flowy-folder2/src/event_map.rs +++ b/frontend/rust-lib/flowy-folder2/src/event_map.rs @@ -12,11 +12,8 @@ pub fn init(folder: Weak) -> AFPlugin { AFPlugin::new().name("Flowy-Folder").state(folder) // Workspace .event(FolderEvent::CreateWorkspace, create_workspace_handler) - .event( - FolderEvent::GetCurrentWorkspace, - get_current_workspace_setting_handler, - ) - .event(FolderEvent::ReadAllWorkspaces, read_workspaces_handler) + .event(FolderEvent::GetCurrentWorkspaceSetting, read_current_workspace_setting_handler) + .event(FolderEvent::ReadCurrentWorkspace, read_current_workspace_handler) .event(FolderEvent::OpenWorkspace, open_workspace_handler) .event(FolderEvent::ReadWorkspaceViews, get_workspace_views_handler) // View @@ -52,11 +49,11 @@ pub enum FolderEvent { /// Read the current opening workspace. Currently, we only support one workspace #[event(output = "WorkspaceSettingPB")] - GetCurrentWorkspace = 1, + GetCurrentWorkspaceSetting = 1, /// Return a list of workspaces that the current user can access. - #[event(input = "WorkspaceIdPB", output = "RepeatedWorkspacePB")] - ReadAllWorkspaces = 2, + #[event(output = "WorkspacePB")] + ReadCurrentWorkspace = 2, /// Delete the workspace #[event(input = "WorkspaceIdPB")] diff --git a/frontend/rust-lib/flowy-folder2/src/lib.rs b/frontend/rust-lib/flowy-folder2/src/lib.rs index 2e5df210ca88..b78899d21917 100644 --- a/frontend/rust-lib/flowy-folder2/src/lib.rs +++ b/frontend/rust-lib/flowy-folder2/src/lib.rs @@ -1,4 +1,4 @@ -pub use collab_folder::core::ViewLayout; +pub use collab_folder::ViewLayout; pub mod entities; pub mod event_handler; diff --git a/frontend/rust-lib/flowy-folder2/src/manager.rs b/frontend/rust-lib/flowy-folder2/src/manager.rs index 7a2b37c8c224..461c3824e498 100644 --- a/frontend/rust-lib/flowy-folder2/src/manager.rs +++ b/frontend/rust-lib/flowy-folder2/src/manager.rs @@ -5,9 +5,9 @@ use std::sync::{Arc, Weak}; use collab::core::collab::{CollabRawData, MutexCollab}; use collab::core::collab_state::SyncState; use collab_entity::CollabType; -use collab_folder::core::{ - FavoritesInfo, Folder, FolderData, FolderNotify, TrashChange, TrashChangeReceiver, TrashInfo, - View, ViewChange, ViewChangeReceiver, ViewLayout, ViewUpdate, Workspace, +use collab_folder::{ + FavoriteId, Folder, FolderData, FolderNotify, TrashChange, TrashChangeReceiver, TrashInfo, + UserId, View, ViewChange, ViewChangeReceiver, ViewLayout, ViewUpdate, Workspace, }; use parking_lot::{Mutex, RwLock}; use tokio_stream::wrappers::WatchStream; @@ -24,12 +24,11 @@ use crate::entities::icon::UpdateViewIconParams; use crate::entities::{ view_pb_with_child_views, view_pb_without_child_views, ChildViewUpdatePB, CreateViewParams, CreateWorkspaceParams, DeletedViewPB, FolderSnapshotPB, FolderSnapshotStatePB, FolderSyncStatePB, - RepeatedTrashPB, RepeatedViewPB, RepeatedWorkspacePB, UpdateViewParams, UserFolderPB, ViewPB, - WorkspacePB, + RepeatedTrashPB, RepeatedViewPB, UpdateViewParams, UserFolderPB, ViewPB, WorkspacePB, + WorkspaceSettingPB, }; use crate::notification::{ - send_notification, send_workspace_notification, send_workspace_setting_notification, - FolderNotification, + send_notification, send_workspace_setting_notification, FolderNotification, }; use crate::share::ImportParams; use crate::user_default::DefaultFolderBuilder; @@ -74,6 +73,7 @@ impl FolderManager { Ok(manager) } + #[instrument(level = "debug", skip(self), err)] pub async fn get_current_workspace(&self) -> FlowyResult { self.with_folder( || { @@ -92,19 +92,7 @@ impl FolderManager { }; match folder.get_current_workspace() { - None => { - // The current workspace should always exist. If not, try to find the first workspace. - // from the folder. Otherwise, return an error. - let mut workspaces = folder.workspaces.get_all_workspaces(); - if workspaces.is_empty() { - Err(FlowyError::record_not_found().with_context("Can not find the workspace")) - } else { - tracing::error!("Can't find the current workspace, use the first workspace"); - let workspace = workspaces.remove(0); - folder.set_current_workspace(&workspace.id); - workspace_pb_from_workspace(workspace, folder) - } - }, + None => Err(FlowyError::record_not_found().with_context("Can not find the workspace")), Some(workspace) => workspace_pb_from_workspace(workspace, folder), } }, @@ -118,9 +106,9 @@ impl FolderManager { .mutex_folder .lock() .as_ref() - .map(|folder| folder.get_current_workspace_id()); + .map(|folder| folder.get_workspace_id()); - if let Some(Some(workspace_id)) = workspace_id { + if let Some(workspace_id) = workspace_id { self.get_workspace_views(&workspace_id).await } else { tracing::warn!("Can't get current workspace views"); @@ -129,7 +117,7 @@ impl FolderManager { } pub async fn get_workspace_views(&self, workspace_id: &str) -> FlowyResult> { - let views = self.with_folder(std::vec::Vec::new, |folder| { + let views = self.with_folder(Vec::new, |folder| { get_workspace_view_pbs(workspace_id, folder) }); @@ -160,18 +148,25 @@ impl FolderManager { } => { let is_exist = is_exist_in_local_disk(&self.user, &workspace_id).unwrap_or(false); if is_exist { + event!(Level::INFO, "Restore folder from local disk"); let collab = self .collab_for_folder(uid, &workspace_id, collab_db, vec![]) .await?; - Folder::open(collab, Some(folder_notifier)) + Folder::open(UserId::from(uid), collab, Some(folder_notifier))? } else if create_if_not_exist { + event!(Level::INFO, "Create folder with default folder builder"); let folder_data = DefaultFolderBuilder::build(uid, workspace_id.to_string(), &self.operation_handlers) .await; let collab = self .collab_for_folder(uid, &workspace_id, collab_db, vec![]) .await?; - Folder::create(collab, Some(folder_notifier), Some(folder_data)) + Folder::create( + UserId::from(uid), + collab, + Some(folder_notifier), + folder_data, + ) } else { return Err(FlowyError::new( ErrorCode::RecordNotFound, @@ -180,19 +175,26 @@ impl FolderManager { } }, FolderInitializeDataSource::Cloud(raw_data) => { + event!(Level::INFO, "Restore folder from cloud service"); if raw_data.is_empty() { return Err(workspace_data_not_sync_error(uid, &workspace_id)); } let collab = self .collab_for_folder(uid, &workspace_id, collab_db, raw_data) .await?; - Folder::open(collab, Some(folder_notifier)) + Folder::open(UserId::from(uid), collab, Some(folder_notifier))? }, FolderInitializeDataSource::FolderData(folder_data) => { + event!(Level::INFO, "Restore folder with passed-in folder data"); let collab = self .collab_for_folder(uid, &workspace_id, collab_db, vec![]) .await?; - Folder::create(collab, Some(folder_notifier), Some(folder_data)) + Folder::create( + UserId::from(uid), + collab, + Some(folder_notifier), + folder_data, + ) }, }; @@ -325,49 +327,58 @@ impl FolderManager { pub async fn clear(&self, _user_id: i64) {} #[tracing::instrument(level = "info", skip_all, err)] - pub async fn create_workspace(&self, params: CreateWorkspaceParams) -> FlowyResult { - let workspace = self - .cloud_service - .create_workspace(self.user.user_id()?, ¶ms.name) - .await?; - - self.with_folder( - || (), - |folder| { - folder.workspaces.create_workspace(workspace.clone()); - folder.set_current_workspace(&workspace.id); - }, - ); - - let repeated_workspace = RepeatedWorkspacePB { - items: vec![workspace.clone().into()], - }; - send_workspace_notification(FolderNotification::DidCreateWorkspace, repeated_workspace); - Ok(workspace) + pub async fn create_workspace(&self, _params: CreateWorkspaceParams) -> FlowyResult { + Err(FlowyError::not_support()) } #[tracing::instrument(level = "info", skip_all, err)] - pub async fn open_workspace(&self, workspace_id: &str) -> FlowyResult { + pub async fn open_workspace(&self, _workspace_id: &str) -> FlowyResult { self.with_folder( || Err(FlowyError::internal()), |folder| { - let workspace = folder - .workspaces - .get_workspace(workspace_id) - .ok_or_else(|| { - FlowyError::record_not_found().with_context("Can't open not existing workspace") - })?; - folder.set_current_workspace(&workspace.id); + let workspace = folder.get_current_workspace().ok_or_else(|| { + FlowyError::record_not_found().with_context("Can't open not existing workspace") + })?; Ok::(workspace) }, ) } - pub async fn get_workspace(&self, workspace_id: &str) -> Option { - self.with_folder( - || None, - |folder| folder.workspaces.get_workspace(workspace_id), - ) + pub async fn get_workspace(&self, _workspace_id: &str) -> Option { + self.with_folder(|| None, |folder| folder.get_current_workspace()) + } + + pub async fn get_workspace_setting_pb(&self) -> Option { + let workspace_id = self.get_current_workspace_id().await.ok()?; + let latest_view = self.get_current_view().await; + Some(WorkspaceSettingPB { + workspace_id, + latest_view, + }) + } + + pub async fn get_workspace_pb(&self) -> Option { + let workspace_pb = { + let guard = self.mutex_folder.lock(); + let folder = guard.as_ref()?; + let workspace = folder.get_current_workspace()?; + + let views = folder + .views + .get_views_belong_to(&workspace.id) + .into_iter() + .map(view_pb_without_child_views) + .collect::>(); + + WorkspacePB { + id: workspace.id, + name: workspace.name, + views, + create_time: workspace.created_at, + } + }; + + Some(workspace_pb) } async fn get_current_workspace_id(&self) -> FlowyResult { @@ -375,7 +386,7 @@ impl FolderManager { .mutex_folder .lock() .as_ref() - .and_then(|folder| folder.get_current_workspace_id()) + .map(|folder| folder.get_workspace_id()) .ok_or(FlowyError::internal().with_context("Unexpected empty workspace id")) } @@ -399,8 +410,12 @@ impl FolderManager { } pub async fn get_all_workspaces(&self) -> Vec { - self.with_folder(std::vec::Vec::new, |folder| { - folder.workspaces.get_all_workspaces() + self.with_folder(Vec::new, |folder| { + let mut workspaces = vec![]; + if let Some(workspace) = folder.get_current_workspace() { + workspaces.push(workspace); + } + workspaces }) } @@ -478,7 +493,7 @@ impl FolderManager { /// The child views of the view will only access the first. So if you want to get the child view's /// child view, you need to call this method again. #[tracing::instrument(level = "debug", skip(self, view_id), err)] - pub async fn get_view(&self, view_id: &str) -> FlowyResult { + pub async fn get_view_pb(&self, view_id: &str) -> FlowyResult { let view_id = view_id.to_string(); let folder = self.mutex_folder.lock(); let folder = folder.as_ref().ok_or_else(folder_not_init_error)?; @@ -586,7 +601,7 @@ impl FolderManager { new_parent_id: String, prev_view_id: Option, ) -> FlowyResult<()> { - let view = self.get_view(&view_id).await?; + let view = self.get_view_pb(&view_id).await?; let old_parent_id = view.parent_view_id; self.with_folder( || (), @@ -620,7 +635,7 @@ impl FolderManager { .collect::>() } else { self - .get_view(&parent_view_id) + .get_view_pb(&parent_view_id) .await? .child_views .into_iter() @@ -653,7 +668,7 @@ impl FolderManager { /// Return a list of views that belong to the given parent view id. #[tracing::instrument(level = "debug", skip(self, parent_view_id), err)] pub async fn get_views_belong_to(&self, parent_view_id: &str) -> FlowyResult>> { - let views = self.with_folder(std::vec::Vec::new, |folder| { + let views = self.with_folder(Vec::new, |folder| { folder.views.get_views_belong_to(parent_view_id) }); Ok(views) @@ -722,22 +737,22 @@ impl FolderManager { #[tracing::instrument(level = "trace", skip(self), err)] pub(crate) async fn set_current_view(&self, view_id: &str) -> Result<(), FlowyError> { - let folder = self.mutex_folder.lock(); - let folder = folder.as_ref().ok_or_else(folder_not_init_error)?; - folder.set_current_view(view_id); + let workspace_id = self.with_folder( + || Err(FlowyError::record_not_found()), + |folder| { + folder.set_current_view(view_id); + Ok(folder.get_workspace_id()) + }, + )?; - let workspace = folder.get_current_workspace(); - let view = folder - .get_current_view() - .and_then(|view_id| folder.views.get_view(&view_id)); - send_workspace_setting_notification(workspace, view); + send_workspace_setting_notification(workspace_id, self.get_current_view().await); Ok(()) } #[tracing::instrument(level = "trace", skip(self))] pub(crate) async fn get_current_view(&self) -> Option { let view_id = self.with_folder(|| None, |folder| folder.get_current_view())?; - self.get_view(&view_id).await.ok() + self.get_view_pb(&view_id).await.ok() } /// Toggles the favorite status of a view identified by `view_id`If the view is not a favorite, it will be added to the favorites list; otherwise, it will be removed from the list. @@ -761,7 +776,7 @@ impl FolderManager { // Used by toggle_favorites to send notification to frontend, after the favorite status of view has been changed.It sends two distinct notifications: one to correctly update the concerned view's is_favorite status, and another to update the list of favorites that is to be displayed. async fn send_toggle_favorite_notification(&self, view_id: &str) { - if let Ok(view) = self.get_view(view_id).await { + if let Ok(view) = self.get_view_pb(view_id).await { let notification_type = if view.is_favorite { FolderNotification::DidFavoriteView } else { @@ -780,8 +795,8 @@ impl FolderManager { } #[tracing::instrument(level = "trace", skip(self))] - pub(crate) async fn get_all_favorites(&self) -> Vec { - self.with_folder(std::vec::Vec::new, |folder| { + pub(crate) async fn get_all_favorites(&self) -> Vec { + self.with_folder(Vec::new, |folder| { let trash_ids = folder .get_all_trash() .into_iter() @@ -796,7 +811,7 @@ impl FolderManager { #[tracing::instrument(level = "trace", skip(self))] pub(crate) async fn get_all_trash(&self) -> Vec { - self.with_folder(std::vec::Vec::new, |folder| folder.get_all_trash()) + self.with_folder(Vec::new, |folder| folder.get_all_trash()) } #[tracing::instrument(level = "trace", skip(self))] @@ -825,7 +840,7 @@ impl FolderManager { /// Delete all the trash permanently. #[tracing::instrument(level = "trace", skip(self))] pub(crate) async fn delete_all_trash(&self) { - let deleted_trash = self.with_folder(std::vec::Vec::new, |folder| folder.get_all_trash()); + let deleted_trash = self.with_folder(Vec::new, |folder| folder.get_all_trash()); for trash in deleted_trash { let _ = self.delete_trash(&trash.id).await; } @@ -928,7 +943,7 @@ impl FolderManager { } } - if let Ok(view_pb) = self.get_view(view_id).await { + if let Ok(view_pb) = self.get_view_pb(view_id).await { send_notification(&view_pb.id, FolderNotification::DidUpdateView) .payload(view_pb) .send(); @@ -1179,7 +1194,7 @@ fn notify_parent_view_did_change>( ) -> Option<()> { let folder = folder.lock(); let folder = folder.as_ref()?; - let workspace_id = folder.get_current_workspace_id()?; + let workspace_id = folder.get_workspace_id(); let trash_ids = folder .get_all_trash() .into_iter() diff --git a/frontend/rust-lib/flowy-folder2/src/notification.rs b/frontend/rust-lib/flowy-folder2/src/notification.rs index 2d41567b5769..cff47d917f94 100644 --- a/frontend/rust-lib/flowy-folder2/src/notification.rs +++ b/frontend/rust-lib/flowy-folder2/src/notification.rs @@ -1,12 +1,8 @@ -use std::sync::Arc; - -use collab_folder::core::{View, Workspace}; - use flowy_derive::ProtoBuf_Enum; use flowy_notification::NotificationBuilder; use lib_dispatch::prelude::ToBytes; -use crate::entities::{view_pb_without_child_views, WorkspacePB, WorkspaceSettingPB}; +use crate::entities::{ViewPB, WorkspaceSettingPB}; const FOLDER_OBSERVABLE_SOURCE: &str = "Workspace"; @@ -82,13 +78,11 @@ pub(crate) fn send_workspace_notification(ty: FolderNotification, pa } pub(crate) fn send_workspace_setting_notification( - current_workspace: Option, - current_view: Option>, + workspace_id: String, + latest_view: Option, ) -> Option<()> { - let workspace: WorkspacePB = current_workspace?.into(); - let latest_view = current_view.map(view_pb_without_child_views); let setting = WorkspaceSettingPB { - workspace, + workspace_id, latest_view, }; send_workspace_notification(FolderNotification::DidUpdateWorkspaceSetting, setting); diff --git a/frontend/rust-lib/flowy-folder2/src/share/import.rs b/frontend/rust-lib/flowy-folder2/src/share/import.rs index af11f32006a7..531461a232c1 100644 --- a/frontend/rust-lib/flowy-folder2/src/share/import.rs +++ b/frontend/rust-lib/flowy-folder2/src/share/import.rs @@ -1,4 +1,4 @@ -use collab_folder::core::ViewLayout; +use collab_folder::ViewLayout; #[derive(Clone, Debug)] pub enum ImportType { diff --git a/frontend/rust-lib/flowy-folder2/src/user_default.rs b/frontend/rust-lib/flowy-folder2/src/user_default.rs index e5f02cd2bcb3..d12c1ae596d8 100644 --- a/frontend/rust-lib/flowy-folder2/src/user_default.rs +++ b/frontend/rust-lib/flowy-folder2/src/user_default.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use collab_folder::core::{FolderData, RepeatedViewIdentifier, ViewIdentifier, Workspace}; +use collab_folder::{FolderData, RepeatedViewIdentifier, ViewIdentifier, Workspace}; use tokio::sync::RwLock; use lib_infra::util::timestamp; @@ -44,10 +44,10 @@ impl DefaultFolderBuilder { }; FolderData { - current_workspace_id: workspace.id.clone(), + workspace, current_view: first_view.id, - workspaces: vec![workspace], views: FlattedViews::flatten_views(views), + favorites: Default::default(), } } } diff --git a/frontend/rust-lib/flowy-folder2/src/view_operation.rs b/frontend/rust-lib/flowy-folder2/src/view_operation.rs index be1d935cf367..d9ccfab1fb6b 100644 --- a/frontend/rust-lib/flowy-folder2/src/view_operation.rs +++ b/frontend/rust-lib/flowy-folder2/src/view_operation.rs @@ -3,8 +3,8 @@ use std::future::Future; use std::sync::Arc; use bytes::Bytes; -pub use collab_folder::core::View; -use collab_folder::core::{RepeatedViewIdentifier, ViewIcon, ViewIdentifier, ViewLayout}; +pub use collab_folder::View; +use collab_folder::{RepeatedViewIdentifier, ViewIcon, ViewIdentifier, ViewLayout}; use tokio::sync::RwLock; use flowy_error::FlowyError; diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs index 0f4d6283a641..f2086f960878 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs @@ -1,4 +1,4 @@ -use anyhow::Error; +use anyhow::{anyhow, Error}; use client_api::entity::QueryCollabParams; use collab::core::origin::CollabOrigin; use collab_entity::CollabType; @@ -16,10 +16,15 @@ where T: AFServer, { fn create_workspace(&self, _uid: i64, _name: &str) -> FutureResult { - FutureResult::new(async move { todo!() }) + FutureResult::new(async move { Err(anyhow!("Not support yet")) }) } - fn get_folder_data(&self, workspace_id: &str) -> FutureResult, Error> { + fn get_folder_data( + &self, + workspace_id: &str, + uid: &i64, + ) -> FutureResult, Error> { + let uid = *uid; let workspace_id = workspace_id.to_string(); let try_get_client = self.0.try_get_client(); FutureResult::new(async move { @@ -33,7 +38,7 @@ where .await .map_err(FlowyError::from)?]; let folder = - Folder::from_collab_raw_data(CollabOrigin::Empty, updates, &workspace_id, vec![])?; + Folder::from_collab_raw_data(uid, CollabOrigin::Empty, updates, &workspace_id, vec![])?; Ok(folder.get_folder_data()) }) } diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs index b550aeaa0e92..45c4efbc323a 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs @@ -27,7 +27,11 @@ impl FolderCloudService for LocalServerFolderCloudServiceImpl { }) } - fn get_folder_data(&self, _workspace_id: &str) -> FutureResult, Error> { + fn get_folder_data( + &self, + _workspace_id: &str, + _uid: &i64, + ) -> FutureResult, Error> { FutureResult::new(async move { Ok(None) }) } diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs b/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs index c3e15678c164..5a16416359a5 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs @@ -69,7 +69,12 @@ where }) } - fn get_folder_data(&self, workspace_id: &str) -> FutureResult, Error> { + fn get_folder_data( + &self, + workspace_id: &str, + uid: &i64, + ) -> FutureResult, Error> { + let uid = *uid; let try_get_postgrest = self.server.try_get_postgrest(); let workspace_id = workspace_id.to_string(); FutureResult::new(async move { @@ -85,7 +90,7 @@ where } let folder = - Folder::from_collab_raw_data(CollabOrigin::Empty, updates, &workspace_id, vec![])?; + Folder::from_collab_raw_data(uid, CollabOrigin::Empty, updates, &workspace_id, vec![])?; Ok(folder.get_folder_data()) }) } diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs index 3f446bfcf8b6..f915c2cbfb3a 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs @@ -17,14 +17,13 @@ use tokio_retry::{Action, RetryIf}; use uuid::Uuid; use flowy_error::FlowyError; -use flowy_folder_deps::cloud::{Folder, Workspace}; +use flowy_folder_deps::cloud::{Folder, FolderData, Workspace}; use flowy_user_deps::cloud::*; use flowy_user_deps::entities::*; use flowy_user_deps::DEFAULT_USER_NAME; use lib_dispatch::prelude::af_spawn; use lib_infra::box_any::BoxAny; use lib_infra::future::FutureResult; -use lib_infra::util::timestamp; use crate::response::ExtendedResponse; use crate::supabase::api::request::{ @@ -609,15 +608,9 @@ fn empty_workspace_update(collab_object: &CollabObject) -> Vec { &collab_object.object_id, vec![], )); - let folder = Folder::create(collab.clone(), None, None); - folder.workspaces.create_workspace(Workspace { - id: workspace_id.clone(), - name: "My workspace".to_string(), - child_views: Default::default(), - created_at: timestamp(), - }); - folder.set_current_workspace(&workspace_id); - collab.encode_as_update_v1().0 + let workspace = Workspace::new(workspace_id, "My workspace".to_string()); + let folder = Folder::create(collab_object.uid, collab, None, FolderData::new(workspace)); + folder.encode_as_update_v1().0 } fn oauth_params_from_box_any(any: BoxAny) -> Result { diff --git a/frontend/rust-lib/flowy-server/tests/supabase_test/util.rs b/frontend/rust-lib/flowy-server/tests/supabase_test/util.rs index b22fe1b92530..d79537a07c9c 100644 --- a/frontend/rust-lib/flowy-server/tests/supabase_test/util.rs +++ b/frontend/rust-lib/flowy-server/tests/supabase_test/util.rs @@ -96,15 +96,23 @@ pub fn encryption_collab_service( } #[allow(dead_code)] -pub async fn print_encryption_folder(folder_id: &str, encryption_secret: Option) { +pub async fn print_encryption_folder( + uid: &i64, + folder_id: &str, + encryption_secret: Option, +) { let (cloud_service, _encryption) = encryption_folder_service(encryption_secret); - let folder_data = cloud_service.get_folder_data(folder_id).await.unwrap(); + let folder_data = cloud_service.get_folder_data(folder_id, uid).await.unwrap(); let json = serde_json::to_value(folder_data).unwrap(); println!("{}", serde_json::to_string_pretty(&json).unwrap()); } #[allow(dead_code)] -pub async fn print_encryption_folder_snapshot(folder_id: &str, encryption_secret: Option) { +pub async fn print_encryption_folder_snapshot( + uid: &i64, + folder_id: &str, + encryption_secret: Option, +) { let (cloud_service, _encryption) = encryption_collab_service(encryption_secret); let snapshot = cloud_service .get_snapshots(folder_id, 1) @@ -115,7 +123,10 @@ pub async fn print_encryption_folder_snapshot(folder_id: &str, encryption_secret MutexCollab::new_with_raw_data(CollabOrigin::Empty, folder_id, vec![snapshot.blob], vec![]) .unwrap(), ); - let folder_data = Folder::open(collab, None).get_folder_data().unwrap(); + let folder_data = Folder::open(uid, collab, None) + .unwrap() + .get_folder_data() + .unwrap(); let json = serde_json::to_value(folder_data).unwrap(); println!("{}", serde_json::to_string_pretty(&json).unwrap()); } diff --git a/frontend/rust-lib/flowy-user/src/migrations/migrate_to_new_user.rs b/frontend/rust-lib/flowy-user/src/anon_user_upgrade/anon_user_data.rs similarity index 92% rename from frontend/rust-lib/flowy-user/src/migrations/migrate_to_new_user.rs rename to frontend/rust-lib/flowy-user/src/anon_user_upgrade/anon_user_data.rs index e9cb39ef2d4a..152c305f8b76 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/migrate_to_new_user.rs +++ b/frontend/rust-lib/flowy-user/src/anon_user_upgrade/anon_user_data.rs @@ -11,7 +11,7 @@ use collab_database::database::{ }; use collab_database::rows::{database_row_document_id_from_row_id, mut_row_with_collab, RowId}; use collab_database::user::DatabaseWithViewsArray; -use collab_folder::core::Folder; +use collab_folder::{Folder, UserId}; use parking_lot::{Mutex, RwLock}; use collab_integrate::{PersistenceError, RocksCollabDB, YrsDocAction}; @@ -22,7 +22,7 @@ use crate::migrations::MigrationUser; /// Migration the collab objects of the old user to new user. Currently, it only happens when /// the user is a local user and try to use AppFlowy cloud service. -pub fn migration_local_user_on_sign_up( +pub fn migration_anon_user_on_sign_up( old_user: &MigrationUser, old_collab_db: &Arc, new_user: &MigrationUser, @@ -207,7 +207,13 @@ where old_folder_collab.with_origin_transact_mut(|txn| { old_collab_r_txn.load_doc_with_txn(old_uid, old_workspace_id, txn) })?; - let old_folder = Folder::open(Arc::new(MutexCollab::from_collab(old_folder_collab)), None); + let oid_user_id = UserId::from(old_uid); + let old_folder = Folder::open( + oid_user_id, + Arc::new(MutexCollab::from_collab(old_folder_collab)), + None, + ) + .map_err(|err| PersistenceError::InvalidData(err.to_string()))?; let mut folder_data = old_folder .get_folder_data() .ok_or(PersistenceError::Internal( @@ -219,25 +225,13 @@ where .insert(old_workspace_id.to_string(), new_workspace_id.to_string()); // 1. Replace the workspace views id to new id - debug_assert!(folder_data.workspaces.len() == 1); - + folder_data.workspace.id = new_workspace_id.clone(); folder_data - .workspaces + .workspace + .child_views .iter_mut() - .enumerate() - .for_each(|(index, workspace)| { - if index == 0 { - workspace.id = new_workspace_id.to_string(); - } else { - tracing::warn!("🔴migrate folder: more than one workspace"); - workspace.id = old_to_new_id_map.get_new_id(&workspace.id); - } - workspace - .child_views - .iter_mut() - .for_each(|view_identifier| { - view_identifier.id = old_to_new_id_map.get_new_id(&view_identifier.id); - }); + .for_each(|view_identifier| { + view_identifier.id = old_to_new_id_map.get_new_id(&view_identifier.id); }); folder_data.views.iter_mut().for_each(|view| { @@ -253,15 +247,6 @@ where }); }); - match old_to_new_id_map.get(&folder_data.current_workspace_id) { - Some(new_workspace_id) => { - folder_data.current_workspace_id = new_workspace_id.clone(); - }, - None => { - tracing::error!("🔴migrate folder: current workspace id not found"); - }, - } - match old_to_new_id_map.get(&folder_data.current_view) { Some(new_view_id) => { folder_data.current_view = new_view_id.clone(); @@ -276,7 +261,8 @@ where let new_folder_collab = Collab::new_with_raw_data(origin, new_workspace_id, vec![], vec![]) .map_err(|err| PersistenceError::Internal(Box::new(err)))?; let mutex_collab = Arc::new(MutexCollab::from_collab(new_folder_collab)); - let _ = Folder::create(mutex_collab.clone(), None, Some(folder_data)); + let new_user_id = UserId::from(new_uid); + let _ = Folder::create(new_user_id, mutex_collab.clone(), None, folder_data); { let mutex_collab = mutex_collab.lock(); diff --git a/frontend/rust-lib/flowy-user/src/anon_user_upgrade/mod.rs b/frontend/rust-lib/flowy-user/src/anon_user_upgrade/mod.rs new file mode 100644 index 000000000000..f48ee7a701d9 --- /dev/null +++ b/frontend/rust-lib/flowy-user/src/anon_user_upgrade/mod.rs @@ -0,0 +1,5 @@ +pub use anon_user_data::*; +pub use sync_new_user::*; + +mod anon_user_data; +mod sync_new_user; diff --git a/frontend/rust-lib/flowy-user/src/migrations/sync_new_user.rs b/frontend/rust-lib/flowy-user/src/anon_user_upgrade/sync_new_user.rs similarity index 99% rename from frontend/rust-lib/flowy-user/src/migrations/sync_new_user.rs rename to frontend/rust-lib/flowy-user/src/anon_user_upgrade/sync_new_user.rs index b30ece016042..3afdab4c51c6 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/sync_new_user.rs +++ b/frontend/rust-lib/flowy-user/src/anon_user_upgrade/sync_new_user.rs @@ -10,7 +10,7 @@ use collab_database::database::get_database_row_ids; use collab_database::rows::database_row_document_id_from_row_id; use collab_database::user::{get_database_with_views, DatabaseWithViews}; use collab_entity::{CollabObject, CollabType}; -use collab_folder::core::{Folder, View, ViewLayout}; +use collab_folder::{Folder, View, ViewLayout}; use parking_lot::Mutex; use collab_integrate::{PersistenceError, RocksCollabDB, YrsDocAction}; @@ -255,9 +255,10 @@ async fn sync_folder( let update = collab.encode_as_update_v1().0; ( MutexFolder::new(Folder::open( + uid, Arc::new(MutexCollab::from_collab(collab)), None, - )), + )?), update, ) }; diff --git a/frontend/rust-lib/flowy-user/src/event_map.rs b/frontend/rust-lib/flowy-user/src/event_map.rs index 93bca6403075..07953d232dab 100644 --- a/frontend/rust-lib/flowy-user/src/event_map.rs +++ b/frontend/rust-lib/flowy-user/src/event_map.rs @@ -1,7 +1,7 @@ use std::sync::{Arc, Weak}; use collab_database::database::WatchStream; -use collab_folder::core::FolderData; +use collab_folder::FolderData; use strum_macros::Display; use flowy_derive::{Flowy_Event, ProtoBuf_Enum}; diff --git a/frontend/rust-lib/flowy-user/src/lib.rs b/frontend/rust-lib/flowy-user/src/lib.rs index 2a27658200f3..982b7af837a9 100644 --- a/frontend/rust-lib/flowy-user/src/lib.rs +++ b/frontend/rust-lib/flowy-user/src/lib.rs @@ -1,6 +1,7 @@ #[macro_use] extern crate flowy_sqlite; +mod anon_user_upgrade; pub mod entities; mod event_handler; pub mod event_map; diff --git a/frontend/rust-lib/flowy-user/src/manager.rs b/frontend/rust-lib/flowy-user/src/manager.rs index be9ae8a99269..6adf0e23c0d6 100644 --- a/frontend/rust-lib/flowy-user/src/manager.rs +++ b/frontend/rust-lib/flowy-user/src/manager.rs @@ -19,12 +19,12 @@ use flowy_user_deps::entities::*; use lib_dispatch::prelude::af_spawn; use lib_infra::box_any::BoxAny; +use crate::anon_user_upgrade::{migration_anon_user_on_sign_up, sync_user_data_to_cloud}; use crate::entities::{AuthStateChangedPB, AuthStatePB, UserProfilePB, UserSettingPB}; use crate::event_map::{DefaultUserStatusCallback, UserCloudServiceProvider, UserStatusCallback}; -use crate::migrations::historical_document::HistoricalEmptyDocumentMigration; -use crate::migrations::migrate_to_new_user::migration_local_user_on_sign_up; -use crate::migrations::migration::UserLocalDataMigration; -use crate::migrations::sync_new_user::sync_user_data_to_cloud; +use crate::migrations::document_empty_content::HistoricalEmptyDocumentMigration; +use crate::migrations::migration::{UserDataMigration, UserLocalDataMigration}; +use crate::migrations::workspace_and_favorite_v1::FavoriteV1AndWorkspaceArrayMigration; use crate::migrations::MigrationUser; use crate::services::cloud_config::get_cloud_config; use crate::services::collab_interact::{CollabInteract, DefaultCollabInteract}; @@ -165,8 +165,13 @@ impl UserManager { self.database.get_pool(session.user_id), ) { (Ok(collab_db), Ok(sqlite_pool)) => { - match UserLocalDataMigration::new(session.clone(), collab_db, sqlite_pool) - .run(vec![Box::new(HistoricalEmptyDocumentMigration)]) + // ⚠️The order of migrations is crucial. If you're adding a new migration, please ensure + // it's appended to the end of the list. + let migrations: Vec> = vec![ + Box::new(HistoricalEmptyDocumentMigration), + Box::new(FavoriteV1AndWorkspaceArrayMigration), + ]; + match UserLocalDataMigration::new(session.clone(), collab_db, sqlite_pool).run(migrations) { Ok(applied_migrations) => { if !applied_migrations.is_empty() { @@ -358,12 +363,12 @@ impl UserManager { }; event!( tracing::Level::INFO, - "Migrate old user data from {:?} to {:?}", + "Migrate anon user data from {:?} to {:?}", old_user.user_profile.uid, new_user.user_profile.uid ); self - .migrate_local_user_to_cloud(&old_user, &new_user) + .migrate_anon_user_to_cloud(&old_user, &new_user) .await?; let _ = self.database.close(old_user.session.user_id); } @@ -689,14 +694,14 @@ impl UserManager { Ok(()) } - async fn migrate_local_user_to_cloud( + async fn migrate_anon_user_to_cloud( &self, old_user: &MigrationUser, new_user: &MigrationUser, ) -> Result<(), FlowyError> { let old_collab_db = self.database.get_collab_db(old_user.session.user_id)?; let new_collab_db = self.database.get_collab_db(new_user.session.user_id)?; - migration_local_user_on_sign_up(old_user, &old_collab_db, new_user, &new_collab_db)?; + migration_anon_user_on_sign_up(old_user, &old_collab_db, new_user, &new_collab_db)?; if let Err(err) = sync_user_data_to_cloud( self.cloud_services.get_user_service()?, diff --git a/frontend/rust-lib/flowy-user/src/migrations/historical_document.rs b/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs similarity index 91% rename from frontend/rust-lib/flowy-user/src/migrations/historical_document.rs rename to frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs index 0b256141735e..d5945f1c22a5 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/historical_document.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs @@ -4,7 +4,7 @@ use collab::core::collab::MutexCollab; use collab::core::origin::{CollabClient, CollabOrigin}; use collab_document::document::Document; use collab_document::document_data::default_document_data; -use collab_folder::core::Folder; +use collab_folder::Folder; use collab_integrate::{RocksCollabDB, YrsDocAction}; use flowy_error::{internal_error, FlowyResult}; @@ -25,8 +25,13 @@ impl UserDataMigration for HistoricalEmptyDocumentMigration { if let Ok(updates) = write_txn.get_all_updates(session.user_id, &session.user_workspace.id) { let origin = CollabOrigin::Client(CollabClient::new(session.user_id, "phantom")); // Deserialize the folder from the raw data - let folder = - Folder::from_collab_raw_data(origin.clone(), updates, &session.user_workspace.id, vec![])?; + let folder = Folder::from_collab_raw_data( + session.user_id, + origin.clone(), + updates, + &session.user_workspace.id, + vec![], + )?; // Migration the first level documents of the workspace let migration_views = folder.get_workspace_views(&session.user_workspace.id); diff --git a/frontend/rust-lib/flowy-user/src/migrations/migration.rs b/frontend/rust-lib/flowy-user/src/migrations/migration.rs index e874c39946a0..c156c4b99464 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/migration.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/migration.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use chrono::NaiveDateTime; use diesel::{RunQueryDsl, SqliteConnection}; +use tracing::event; use collab_integrate::RocksCollabDB; use flowy_error::FlowyResult; @@ -54,6 +55,12 @@ impl UserLocalDataMigration { { let migration_name = migration.name().to_string(); if !duplicated_names.contains(&migration_name) { + event!( + tracing::Level::INFO, + "Running migration {}", + migration.name() + ); + migration.run(&self.session, &self.collab_db)?; applied_migrations.push(migration.name().to_string()); save_record(&conn, &migration_name); diff --git a/frontend/rust-lib/flowy-user/src/migrations/mod.rs b/frontend/rust-lib/flowy-user/src/migrations/mod.rs index b35e4377dc2d..be26507ec17e 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/mod.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/mod.rs @@ -1,7 +1,6 @@ pub use define::*; mod define; -pub mod historical_document; -pub mod migrate_to_new_user; +pub mod document_empty_content; pub mod migration; -pub mod sync_new_user; +pub mod workspace_and_favorite_v1; diff --git a/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs b/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs new file mode 100644 index 000000000000..768b38f20de0 --- /dev/null +++ b/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs @@ -0,0 +1,55 @@ +use std::sync::Arc; + +use collab::core::origin::{CollabClient, CollabOrigin}; +use collab_folder::Folder; + +use collab_integrate::{RocksCollabDB, YrsDocAction}; +use flowy_error::{internal_error, FlowyResult}; + +use crate::migrations::migration::UserDataMigration; +use crate::services::entities::Session; + +/// 1. Migrate the workspace: { favorite: [view_id] } to { favorite: { uid: [view_id] } } +/// 2. Migrate { workspaces: [workspace object] } to { views: { workspace object } }. Make each folder +/// only have one workspace. +pub struct FavoriteV1AndWorkspaceArrayMigration; + +impl UserDataMigration for FavoriteV1AndWorkspaceArrayMigration { + fn name(&self) -> &str { + "workspace_favorite_v1_and_workspace_array_migration" + } + + fn run(&self, session: &Session, collab_db: &Arc) -> FlowyResult<()> { + let write_txn = collab_db.write_txn(); + if let Ok(updates) = write_txn.get_all_updates(session.user_id, &session.user_workspace.id) { + let origin = CollabOrigin::Client(CollabClient::new(session.user_id, "phantom")); + // Deserialize the folder from the raw data + let folder = Folder::from_collab_raw_data( + session.user_id, + origin, + updates, + &session.user_workspace.id, + vec![], + )?; + + folder.migrate_workspace_to_view(); + + let favorite_view_ids = folder + .get_favorite_v1() + .into_iter() + .map(|fav| fav.id) + .collect::>(); + + if !favorite_view_ids.is_empty() { + folder.add_favorites(favorite_view_ids); + } + + let (doc_state, sv) = folder.encode_as_update_v1(); + write_txn + .flush_doc_with(session.user_id, &session.user_workspace.id, &doc_state, &sv) + .map_err(internal_error)?; + write_txn.commit_transaction().map_err(internal_error)?; + } + Ok(()) + } +} From 21d34d1fe0df624339cd2dc5b2c13ff7b768eb85 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Wed, 1 Nov 2023 14:47:25 +0800 Subject: [PATCH 14/56] chore: update collab rev (#3852) --- frontend/appflowy_tauri/src-tauri/Cargo.lock | 18 +++++++++--------- frontend/appflowy_tauri/src-tauri/Cargo.toml | 16 ++++++++-------- frontend/rust-lib/Cargo.lock | 18 +++++++++--------- frontend/rust-lib/Cargo.toml | 16 ++++++++-------- .../flowy-folder2/src/event_handler.rs | 6 +++--- frontend/rust-lib/flowy-folder2/src/manager.rs | 4 ++-- 6 files changed, 39 insertions(+), 39 deletions(-) diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 982d4f22e4fc..ddfc303bd27f 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -845,7 +845,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" dependencies = [ "anyhow", "async-trait", @@ -864,7 +864,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" dependencies = [ "anyhow", "async-trait", @@ -894,7 +894,7 @@ dependencies = [ [[package]] name = "collab-derive" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" dependencies = [ "proc-macro2", "quote", @@ -906,7 +906,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" dependencies = [ "anyhow", "collab", @@ -926,7 +926,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" dependencies = [ "anyhow", "bytes", @@ -940,7 +940,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" dependencies = [ "anyhow", "chrono", @@ -982,7 +982,7 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" dependencies = [ "async-trait", "bincode", @@ -1003,7 +1003,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" dependencies = [ "anyhow", "async-trait", @@ -1030,7 +1030,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" dependencies = [ "anyhow", "collab", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index 131846e2bad6..b7a4e519580c 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -48,14 +48,14 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c87 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b05aa7e" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b05aa7e" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b05aa7e" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b05aa7e" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b05aa7e" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b05aa7e" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b05aa7e" } -collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b05aa7e" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "01f92f7bb" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "01f92f7bb" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "01f92f7bb" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "01f92f7bb" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "01f92f7bb" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "01f92f7bb" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "01f92f7bb" } +collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "01f92f7bb" } diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 2a88127c1142..06dd20e10572 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -712,7 +712,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" dependencies = [ "anyhow", "async-trait", @@ -731,7 +731,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" dependencies = [ "anyhow", "async-trait", @@ -761,7 +761,7 @@ dependencies = [ [[package]] name = "collab-derive" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" dependencies = [ "proc-macro2", "quote", @@ -773,7 +773,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" dependencies = [ "anyhow", "collab", @@ -793,7 +793,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" dependencies = [ "anyhow", "bytes", @@ -807,7 +807,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" dependencies = [ "anyhow", "chrono", @@ -849,7 +849,7 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" dependencies = [ "async-trait", "bincode", @@ -870,7 +870,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" dependencies = [ "anyhow", "async-trait", @@ -897,7 +897,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b05aa7e#9b05aa7e8500b42fa56117052de6e658235d6325" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" dependencies = [ "anyhow", "collab", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 93ae49c1b41d..8d30996ea8a1 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -92,11 +92,11 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c87 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b05aa7e" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b05aa7e" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b05aa7e" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b05aa7e" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b05aa7e" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b05aa7e" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b05aa7e" } -collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b05aa7e" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "01f92f7bb" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "01f92f7bb" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "01f92f7bb" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "01f92f7bb" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "01f92f7bb" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "01f92f7bb" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "01f92f7bb" } +collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "01f92f7bb" } diff --git a/frontend/rust-lib/flowy-folder2/src/event_handler.rs b/frontend/rust-lib/flowy-folder2/src/event_handler.rs index 97a119911e35..7a3676b547bf 100644 --- a/frontend/rust-lib/flowy-folder2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-folder2/src/event_handler.rs @@ -234,10 +234,10 @@ pub(crate) async fn read_favorites_handler( folder: AFPluginState>, ) -> DataResult { let folder = upgrade_folder(folder)?; - let favorites = folder.get_all_favorites().await; + let favorite_items = folder.get_all_favorites().await; let mut views = vec![]; - for info in favorites { - if let Ok(view) = folder.get_view_pb(&info.id).await { + for item in favorite_items { + if let Ok(view) = folder.get_view_pb(&item.id).await { views.push(view); } } diff --git a/frontend/rust-lib/flowy-folder2/src/manager.rs b/frontend/rust-lib/flowy-folder2/src/manager.rs index 461c3824e498..a710c4b5d1af 100644 --- a/frontend/rust-lib/flowy-folder2/src/manager.rs +++ b/frontend/rust-lib/flowy-folder2/src/manager.rs @@ -6,7 +6,7 @@ use collab::core::collab::{CollabRawData, MutexCollab}; use collab::core::collab_state::SyncState; use collab_entity::CollabType; use collab_folder::{ - FavoriteId, Folder, FolderData, FolderNotify, TrashChange, TrashChangeReceiver, TrashInfo, + Folder, FolderData, FolderNotify, SectionItem, TrashChange, TrashChangeReceiver, TrashInfo, UserId, View, ViewChange, ViewChangeReceiver, ViewLayout, ViewUpdate, Workspace, }; use parking_lot::{Mutex, RwLock}; @@ -795,7 +795,7 @@ impl FolderManager { } #[tracing::instrument(level = "trace", skip(self))] - pub(crate) async fn get_all_favorites(&self) -> Vec { + pub(crate) async fn get_all_favorites(&self) -> Vec { self.with_folder(Vec::new, |folder| { let trash_ids = folder .get_all_trash() From c34a7a92fbc99bc5a09be4daa9edb0cf63293f7c Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 2 Nov 2023 15:24:17 +0800 Subject: [PATCH 15/56] feat: support customizing page icon (#3849) * chore: don't use cache when building release package * feat: refactor icon widget design * feat: sync the emoji between page and view * feat: use cache to store the emoji data to prevent reloading * feat: customize the emoji item builder * feat: add i18n and shuffle emoji button * fix: integration test * feat: replace emoji picker in Grid and slash menu * feat: support adding icon on mobile platform * feat: support adding and removing icon on mobile * test: add integration tests --- .github/workflows/release.yml | 24 -- .../database_row_page_test.dart | 4 - .../document_with_cover_image_test.dart | 49 +++- .../integration_test/util/base.dart | 14 +- .../util/common_operations.dart | 10 +- .../util/database_test_op.dart | 4 +- .../util/editor_test_operations.dart | 23 +- .../integration_test/util/emoji.dart | 7 - .../integration_test/util/expectation.dart | 16 ++ .../presentation/base/mobile_view_page.dart | 16 +- .../page_item/mobile_view_item.dart | 20 +- .../lib/plugins/base/emoji/emoji_picker.dart | 95 ++++++++ .../base/emoji/emoji_picker_header.dart | 48 ++++ .../plugins/base/emoji/emoji_picker_i18n.dart | 41 ++++ .../base/emoji/emoji_picker_screen.dart | 22 ++ .../plugins/base/emoji/emoji_search_bar.dart | 156 +++++++++++++ .../plugins/base/emoji/emoji_skin_tone.dart | 112 +++++++++ .../lib/plugins/base/icon/icon_picker.dart | 121 ++++++++++ .../plugins/base/icon/icon_picker_page.dart | 48 ++++ .../database_view/widgets/row/row_banner.dart | 16 +- .../lib/plugins/document/document_page.dart | 12 +- .../base/emoji_picker_button.dart | 2 +- .../callout/callout_block_component.dart | 2 +- .../header/document_header_node_widget.dart | 212 +++++++++++------- .../editor_plugins/header/emoji_popover.dart | 76 ------- .../document/presentation/editor_style.dart | 4 + .../lib/startup/tasks/generate_router.dart | 19 ++ .../application/view/view_service.dart | 20 +- .../home/menu/view/view_item.dart | 67 +++++- .../widgets/emoji_picker/emoji_menu_item.dart | 14 +- .../lib/style_widget/text_field.dart | 19 +- frontend/appflowy_flutter/pubspec.lock | 56 ++++- frontend/appflowy_flutter/pubspec.yaml | 9 +- frontend/resources/translations/en.json | 14 +- 34 files changed, 1116 insertions(+), 256 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_header.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_i18n.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_screen.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_search_bar.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_skin_tone.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker_page.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_popover.dart diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c8009aeb86d6..eea2f7edfea0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,7 +59,6 @@ jobs: with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} - cache: true - name: Install Rust toolchain uses: actions-rs/toolchain@v1 @@ -70,11 +69,6 @@ jobs: components: rustfmt profile: minimal - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: appflowy-lib-cache - key: ${{ matrix.job.os }}-${{ matrix.job.target }} - - name: Install prerequisites working-directory: frontend run: | @@ -151,7 +145,6 @@ jobs: with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} - cache: true - name: Install Rust toolchain uses: actions-rs/toolchain@v1 @@ -162,11 +155,6 @@ jobs: components: rustfmt profile: minimal - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: appflowy-lib-cache - key: ${{ matrix.job.os }}-${{ matrix.job.target }} - - name: Install prerequisites working-directory: frontend run: | @@ -257,7 +245,6 @@ jobs: with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} - cache: true - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable @@ -266,11 +253,6 @@ jobs: targets: ${{ matrix.job.targets }} components: rustfmt - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: appflowy-lib-cache - key: ${{ matrix.job.os }}-${{ matrix.job.target }} - - name: Install prerequisites working-directory: frontend run: | @@ -366,7 +348,6 @@ jobs: with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} - cache: true - name: Install Rust toolchain uses: actions-rs/toolchain@v1 @@ -377,11 +358,6 @@ jobs: components: rustfmt profile: minimal - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: appflowy-lib-cache - key: ${{ matrix.job.os }}-${{ matrix.job.target }} - - name: Install prerequisites working-directory: frontend run: | diff --git a/frontend/appflowy_flutter/integration_test/database_row_page_test.dart b/frontend/appflowy_flutter/integration_test/database_row_page_test.dart index 62d5c81be31e..9a771eb876f9 100644 --- a/frontend/appflowy_flutter/integration_test/database_row_page_test.dart +++ b/frontend/appflowy_flutter/integration_test/database_row_page_test.dart @@ -42,7 +42,6 @@ void main() { await tester.hoverRowBanner(); await tester.openEmojiPicker(); - await tester.switchToEmojiList(); await tester.tapEmoji('😀'); // After select the emoji, the EmojiButton will show up @@ -60,12 +59,10 @@ void main() { await tester.openFirstRowDetailPage(); await tester.hoverRowBanner(); await tester.openEmojiPicker(); - await tester.switchToEmojiList(); await tester.tapEmoji('😀'); // Update existing selected emoji await tester.tapButton(find.byType(EmojiButton)); - await tester.switchToEmojiList(); await tester.tapEmoji('😅'); // The emoji already displayed in the row banner @@ -89,7 +86,6 @@ void main() { await tester.openFirstRowDetailPage(); await tester.hoverRowBanner(); await tester.openEmojiPicker(); - await tester.switchToEmojiList(); await tester.tapEmoji('😀'); // Remove the emoji diff --git a/frontend/appflowy_flutter/integration_test/document/document_with_cover_image_test.dart b/frontend/appflowy_flutter/integration_test/document/document_with_cover_image_test.dart index 5bcf4b7b43a4..360202e0164b 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_with_cover_image_test.dart +++ b/frontend/appflowy_flutter/integration_test/document/document_with_cover_image_test.dart @@ -1,4 +1,8 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:emoji_mart/emoji_mart.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -61,7 +65,6 @@ void main() { // Insert a document icon await tester.editor.tapAddIconButton(); - await tester.switchToEmojiList(); await tester.tapEmoji('😀'); tester.expectToSeeDocumentIcon('😀'); @@ -73,13 +76,11 @@ void main() { // Add the icon back for further testing await tester.editor.hoverOnCoverToolbar(); await tester.editor.tapAddIconButton(); - await tester.switchToEmojiList(); await tester.tapEmoji('😀'); tester.expectToSeeDocumentIcon('😀'); // Change the document icon await tester.editor.tapOnIconWidget(); - await tester.switchToEmojiList(); await tester.tapEmoji('😅'); tester.expectToSeeDocumentIcon('😅'); @@ -102,7 +103,6 @@ void main() { // Insert a document icon await tester.editor.tapAddIconButton(); - await tester.switchToEmojiList(); await tester.tapEmoji('😀'); // Insert a document cover @@ -116,5 +116,46 @@ void main() { await tester.editor.hoverOnCoverToolbar(); tester.expectToSeeEmptyDocumentHeaderToolbar(); }); + + testWidgets('shuffle icon', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.editor.hoverOnCoverToolbar(); + await tester.editor.tapAddIconButton(); + + // click the shuffle button + await tester.tapButton( + find.byTooltip(LocaleKeys.emoji_random.tr()), + ); + tester.expectDocumentIconNotNull(); + }); + + testWidgets('change skin tone', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.editor.hoverOnCoverToolbar(); + await tester.editor.tapAddIconButton(); + + final searchEmojiTextField = find.byWidgetPredicate( + (widget) => + widget is TextField && + widget.decoration!.hintText == LocaleKeys.emoji_search.tr(), + ); + await tester.enterText( + searchEmojiTextField, + 'hand', + ); + + // change skin tone + await tester.editor.changeEmojiSkinTone(EmojiSkinTone.dark); + + // select an icon with skin tone + const hand = '👋🏿'; + await tester.tapEmoji(hand); + tester.expectToSeeDocumentIcon(hand); + tester.isPageWithIcon(gettingStarted, hand); + }); }); } diff --git a/frontend/appflowy_flutter/integration_test/util/base.dart b/frontend/appflowy_flutter/integration_test/util/base.dart index bfe6a8d833f0..b28188629bf1 100644 --- a/frontend/appflowy_flutter/integration_test/util/base.dart +++ b/frontend/appflowy_flutter/integration_test/util/base.dart @@ -1,9 +1,11 @@ import 'dart:async'; import 'dart:io'; +import 'package:appflowy/env/env.dart'; import 'package:appflowy/startup/entry_point.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/presentation/presentation.dart'; +import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart'; import 'package:flowy_infra/uuid.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -81,9 +83,15 @@ extension AppFlowyTestBase on WidgetTester { } Future waitUntilSignInPageShow() async { - final finder = find.byType(GoButton); - await pumpUntilFound(finder); - expect(finder, findsOneWidget); + if (isCloudEnabled) { + final finder = find.byType(SignInAnonymousButton); + await pumpUntilFound(finder); + expect(finder, findsOneWidget); + } else { + final finder = find.byType(GoButton); + await pumpUntilFound(finder); + expect(finder, findsOneWidget); + } } Future pumpUntilFound( diff --git a/frontend/appflowy_flutter/integration_test/util/common_operations.dart b/frontend/appflowy_flutter/integration_test/util/common_operations.dart index 15a1d74f3ad5..7094f4a333be 100644 --- a/frontend/appflowy_flutter/integration_test/util/common_operations.dart +++ b/frontend/appflowy_flutter/integration_test/util/common_operations.dart @@ -7,6 +7,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/share/share_button.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/presentation/screens/screens.dart'; +import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart'; @@ -27,8 +28,15 @@ import 'util.dart'; extension CommonOperations on WidgetTester { /// Tap the GetStart button on the launch page. Future tapGoButton() async { + // local version final goButton = find.byType(GoButton); - await tapButton(goButton); + if (goButton.evaluate().isNotEmpty) { + await tapButton(goButton); + } else { + // cloud version + final anonymousButton = find.byType(SignInAnonymousButton); + await tapButton(anonymousButton); + } if (Platform.isWindows) { await pumpAndSettle(const Duration(milliseconds: 200)); diff --git a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart index 89b94e6a2cbf..b91511e5d3f5 100644 --- a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart @@ -38,7 +38,6 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_header.dart'; import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart'; import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart'; -import 'package:appflowy/plugins/database_view/widgets/setting/setting_property_list.dart'; import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart'; @@ -53,7 +52,7 @@ import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_property.dart'; import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart'; +import 'package:appflowy/plugins/database_view/widgets/setting/setting_property_list.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; @@ -618,7 +617,6 @@ extension AppFlowyDatabaseTest on WidgetTester { Future openEmojiPicker() async { await tapButton(find.byType(EmojiPickerButton)); - await tapButton(find.byType(EmojiSelectionMenu)); } Future tapDateCellInRowDetailPage() async { diff --git a/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart b/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart index 5b3e8c07775a..51e99ceddf9d 100644 --- a/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart +++ b/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart @@ -1,15 +1,18 @@ import 'dart:ui'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; +import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart'; +import 'package:appflowy/plugins/base/icon/icon_picker.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/custom_cover_picker.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_popover.dart'; import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:easy_localization/easy_localization.dart'; +import 'package:emoji_mart/emoji_mart.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -54,7 +57,18 @@ class EditorOperations { await tester.tapButtonWithName( LocaleKeys.document_plugins_cover_addIcon.tr(), ); - expect(find.byType(EmojiPopover), findsOneWidget); + expect(find.byType(FlowyEmojiPicker), findsOneWidget); + } + + /// Taps on the 'Skin tone' button + /// + /// Must call [tapAddIconButton] first. + Future changeEmojiSkinTone(EmojiSkinTone skinTone) async { + await tester.tapButton( + find.byTooltip(LocaleKeys.emoji_selectSkinTone.tr()), + ); + final skinToneButton = find.text(EmojiSkinToneWrapper(skinTone).name); + await tester.tapButton(skinToneButton); } /// Taps the 'Remove Icon' button in the cover toolbar and the icon popover @@ -62,7 +76,10 @@ class EditorOperations { Finder button = find.text(LocaleKeys.document_plugins_cover_removeIcon.tr()); if (isInPicker) { - button = find.descendant(of: find.byType(EmojiPopover), matching: button); + button = find.descendant( + of: find.byType(FlowyIconPicker), + matching: button, + ); } await tester.tapButton(button); diff --git a/frontend/appflowy_flutter/integration_test/util/emoji.dart b/frontend/appflowy_flutter/integration_test/util/emoji.dart index 616f3da6eccc..a3bfbc02f6e5 100644 --- a/frontend/appflowy_flutter/integration_test/util/emoji.dart +++ b/frontend/appflowy_flutter/integration_test/util/emoji.dart @@ -1,15 +1,8 @@ -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'base.dart'; extension EmojiTestExtension on WidgetTester { - /// Must call [openEmojiPicker] first - Future switchToEmojiList() async { - final icon = find.byIcon(Icons.tag_faces); - await tapButton(icon); - } - Future tapEmoji(String emoji) async { final emojiWidget = find.text(emoji); await tapButton(emojiWidget); diff --git a/frontend/appflowy_flutter/integration_test/util/expectation.dart b/frontend/appflowy_flutter/integration_test/util/expectation.dart index 56bd2c6804d7..ec2f63d30c4c 100644 --- a/frontend/appflowy_flutter/integration_test/util/expectation.dart +++ b/frontend/appflowy_flutter/integration_test/util/expectation.dart @@ -108,6 +108,13 @@ extension Expectation on WidgetTester { expect(iconWidget, findsOneWidget); } + void expectDocumentIconNotNull() { + final iconWidget = find.byWidgetPredicate( + (widget) => widget is EmojiIconWidget && widget.emoji.isNotEmpty, + ); + expect(iconWidget, findsOneWidget); + } + void expectToSeeDocumentCover(CoverType type) { final findCover = find.byWidgetPredicate( (widget) => widget is DocumentCover && widget.coverType == type, @@ -193,4 +200,13 @@ extension Expectation on WidgetTester { matching: findPageName(name, layout: layout), ); } + + void isPageWithIcon(String name, String emoji) { + final pageName = findPageName(name); + final icon = find.descendant( + of: pageName, + matching: find.text(emoji), + ); + expect(icon, findsOneWidget); + } } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart index e63008f25fb6..ec85278ea3b8 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart @@ -106,12 +106,22 @@ class _MobileViewPageState extends State { } Widget _buildApp(ViewPB? view, List actions, Widget child) { + final icon = view?.icon.value; return Scaffold( appBar: AppBar( titleSpacing: 0, - title: FlowyText.semibold( - view?.name ?? widget.title ?? '', - fontSize: 14.0, + title: Row( + children: [ + if (icon != null) + FlowyText( + '$icon ', + fontSize: 22.0, + ), + FlowyText.regular( + view?.name ?? widget.title ?? '', + fontSize: 14.0, + ), + ], ), leading: AppBarBackButton( onTap: () => context.pop(), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart index d24806f24670..18cc9a1bb9eb 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart @@ -303,11 +303,8 @@ class _SingleMobileInnerViewItemState extends State { _buildLeftIcon(), const HSpace(4), // icon - SizedBox.square( - dimension: 22, - child: widget.view.defaultIcon(), - ), - const HSpace(12), + _buildViewIconButton(), + const HSpace(8), // title Expanded( child: FlowyText.regular( @@ -356,6 +353,19 @@ class _SingleMobileInnerViewItemState extends State { return child; } + Widget _buildViewIconButton() { + final icon = widget.view.icon.value.isNotEmpty + ? FlowyText( + widget.view.icon.value, + fontSize: 24.0, + ) + : SizedBox.square( + dimension: 26.0, + child: widget.view.defaultIcon(), + ); + return icon; + } + // > button or · button // show > if the view is expandable. // show · if the view can't contain child views. diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart new file mode 100644 index 000000000000..fb9a55652fbd --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart @@ -0,0 +1,95 @@ +import 'package:appflowy/plugins/base/emoji/emoji_picker_header.dart'; +import 'package:appflowy/plugins/base/emoji/emoji_search_bar.dart'; +import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart'; +import 'package:emoji_mart/emoji_mart.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +// use a global value to store the selected emoji to prevent reloading every time. +EmojiData? _cachedEmojiData; + +class FlowyEmojiPicker extends StatefulWidget { + const FlowyEmojiPicker({ + super.key, + required this.onEmojiSelected, + }); + + final EmojiSelectedCallback onEmojiSelected; + + @override + State createState() => _FlowyEmojiPickerState(); +} + +class _FlowyEmojiPickerState extends State { + EmojiData? emojiData; + + @override + void initState() { + super.initState(); + + // load the emoji data from cache if it's available + if (_cachedEmojiData != null) { + emojiData = _cachedEmojiData; + } else { + EmojiData.builtIn().then( + (value) { + _cachedEmojiData = value; + setState(() { + emojiData = value; + }); + }, + ); + } + } + + @override + Widget build(BuildContext context) { + if (emojiData == null) { + return const Center( + child: SizedBox.square( + dimension: 24.0, + child: CircularProgressIndicator( + strokeWidth: 2.0, + ), + ), + ); + } + + return EmojiPicker( + emojiData: emojiData!, + configuration: EmojiPickerConfiguration( + showSectionHeader: true, + showTabs: false, + defaultSkinTone: lastSelectedEmojiSkinTone ?? EmojiSkinTone.none, + ), + onEmojiSelected: widget.onEmojiSelected, + headerBuilder: (context, category) { + return FlowyEmojiHeader( + category: category, + ); + }, + itemBuilder: (context, emojiId, emoji, callback) { + return FlowyIconButton( + iconPadding: const EdgeInsets.all(2.0), + icon: FlowyText( + emoji, + fontSize: 28.0, + ), + onPressed: () => callback(emojiId, emoji), + ); + }, + searchBarBuilder: (context, keyword, skinTone) { + return FlowyEmojiSearchBar( + emojiData: emojiData!, + onKeywordChanged: (value) { + keyword.value = value; + }, + onSkinToneChanged: (value) { + skinTone.value = value; + }, + onRandomEmojiSelected: widget.onEmojiSelected, + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_header.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_header.dart new file mode 100644 index 000000000000..19b3ad939a82 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_header.dart @@ -0,0 +1,48 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:emoji_mart/emoji_mart.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +class FlowyEmojiHeader extends StatelessWidget { + const FlowyEmojiHeader({ + super.key, + required this.category, + }); + + final Category category; + + @override + Widget build(BuildContext context) { + if (PlatformExtension.isDesktopOrWeb) { + return Container( + height: 22, + padding: const EdgeInsets.symmetric(horizontal: 8.0), + color: Theme.of(context).cardColor, + child: FlowyText.regular(category.id), + ); + } else { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 40, + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 8.0), + color: Theme.of(context).cardColor, + child: Padding( + padding: const EdgeInsets.only( + top: 14.0, + bottom: 4.0, + ), + child: FlowyText.regular(category.id), + ), + ), + const Divider( + height: 1, + thickness: 1, + ), + ], + ); + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_i18n.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_i18n.dart new file mode 100644 index 000000000000..0c4fb066aaf5 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_i18n.dart @@ -0,0 +1,41 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:emoji_mart/emoji_mart.dart'; + +class FlowyEmojiPickerI18n extends EmojiPickerI18n { + @override + String get activity => LocaleKeys.emoji_categories_activities.tr(); + + @override + String get flags => LocaleKeys.emoji_categories_flags.tr(); + + @override + String get foods => LocaleKeys.emoji_categories_food.tr(); + + @override + String get frequent => LocaleKeys.emoji_categories_frequentlyUsed.tr(); + + @override + String get nature => LocaleKeys.emoji_categories_nature.tr(); + + @override + String get objects => LocaleKeys.emoji_categories_objects.tr(); + + @override + String get people => LocaleKeys.emoji_categories_smileys.tr(); + + @override + String get places => LocaleKeys.emoji_categories_places.tr(); + + @override + String get search => LocaleKeys.emoji_search.tr(); + + @override + String get symbols => LocaleKeys.emoji_categories_symbols.tr(); + + @override + String get searchHintText => LocaleKeys.emoji_search.tr(); + + @override + String get searchNoResult => LocaleKeys.emoji_noEmojiFound.tr(); +} diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_screen.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_screen.dart new file mode 100644 index 000000000000..fa578bb132ae --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_screen.dart @@ -0,0 +1,22 @@ +import 'package:appflowy/plugins/base/icon/icon_picker_page.dart'; +import 'package:flutter/material.dart'; + +class MobileEmojiPickerScreen extends StatelessWidget { + static const routeName = '/emoji_picker'; + static const viewId = 'id'; + + const MobileEmojiPickerScreen({ + super.key, + required this.id, + }); + + /// view id + final String id; + + @override + Widget build(BuildContext context) { + return IconPickerPage( + id: id, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_search_bar.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_search_bar.dart new file mode 100644 index 000000000000..c7cf0a094374 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_search_bar.dart @@ -0,0 +1,156 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:emoji_mart/emoji_mart.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; + +typedef EmojiKeywordChangedCallback = void Function(String keyword); +typedef EmojiSkinToneChanged = void Function(EmojiSkinTone skinTone); + +class FlowyEmojiSearchBar extends StatefulWidget { + const FlowyEmojiSearchBar({ + super.key, + required this.emojiData, + required this.onKeywordChanged, + required this.onSkinToneChanged, + required this.onRandomEmojiSelected, + }); + + final EmojiData emojiData; + final EmojiKeywordChangedCallback onKeywordChanged; + final EmojiSkinToneChanged onSkinToneChanged; + final EmojiSelectedCallback onRandomEmojiSelected; + + @override + State createState() => _FlowyEmojiSearchBarState(); +} + +class _FlowyEmojiSearchBarState extends State { + final TextEditingController controller = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.symmetric( + vertical: 8.0, + horizontal: PlatformExtension.isDesktopOrWeb ? 0.0 : 8.0, + ), + child: Row( + children: [ + Expanded( + child: _SearchTextField( + onKeywordChanged: widget.onKeywordChanged, + ), + ), + const HSpace(6.0), + _RandomEmojiButton( + emojiData: widget.emojiData, + onRandomEmojiSelected: widget.onRandomEmojiSelected, + ), + const HSpace(6.0), + FlowyEmojiSkinToneSelector( + onEmojiSkinToneChanged: widget.onSkinToneChanged, + ), + const HSpace(6.0), + ], + ), + ); + } +} + +class _RandomEmojiButton extends StatelessWidget { + const _RandomEmojiButton({ + required this.emojiData, + required this.onRandomEmojiSelected, + }); + + final EmojiData emojiData; + final EmojiSelectedCallback onRandomEmojiSelected; + + @override + Widget build(BuildContext context) { + return FlowyTooltip( + message: LocaleKeys.emoji_random.tr(), + child: FlowyButton( + useIntrinsicWidth: true, + text: const Icon( + Icons.shuffle_rounded, + ), + onTap: () { + final random = emojiData.random; + onRandomEmojiSelected( + random.$1, + random.$2, + ); + }, + ), + ); + } +} + +class _SearchTextField extends StatefulWidget { + const _SearchTextField({ + required this.onKeywordChanged, + }); + + final EmojiKeywordChangedCallback onKeywordChanged; + + @override + State<_SearchTextField> createState() => _SearchTextFieldState(); +} + +class _SearchTextFieldState extends State<_SearchTextField> { + final TextEditingController controller = TextEditingController(); + + @override + void dispose() { + controller.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 32.0, + ), + child: FlowyTextField( + autoFocus: true, + hintText: LocaleKeys.emoji_search.tr(), + controller: controller, + onChanged: widget.onKeywordChanged, + prefixIcon: const Padding( + padding: EdgeInsets.only( + left: 8.0, + right: 4.0, + ), + child: FlowySvg( + FlowySvgs.search_s, + ), + ), + prefixIconConstraints: const BoxConstraints( + maxHeight: 18.0, + ), + suffixIcon: Padding( + padding: const EdgeInsets.all(4.0), + child: FlowyButton( + text: const FlowySvg( + FlowySvgs.close_lg, + ), + margin: EdgeInsets.zero, + useIntrinsicWidth: true, + onTap: () { + controller.clear(); + widget.onKeywordChanged(''); + }, + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_skin_tone.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_skin_tone.dart new file mode 100644 index 000000000000..eebc73ed0536 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_skin_tone.dart @@ -0,0 +1,112 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:emoji_mart/emoji_mart.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; + +// use a temporary global value to store last selected skin tone +EmojiSkinTone? lastSelectedEmojiSkinTone; + +class FlowyEmojiSkinToneSelector extends StatefulWidget { + const FlowyEmojiSkinToneSelector({ + super.key, + required this.onEmojiSkinToneChanged, + }); + + final EmojiSkinToneChanged onEmojiSkinToneChanged; + + @override + State createState() => + _FlowyEmojiSkinToneSelectorState(); +} + +class _FlowyEmojiSkinToneSelectorState + extends State { + EmojiSkinTone skinTone = EmojiSkinTone.none; + + @override + Widget build(BuildContext context) { + return PopoverActionList( + direction: PopoverDirection.bottomWithCenterAligned, + offset: const Offset(0, 8), + actions: EmojiSkinTone.values + .map((action) => EmojiSkinToneWrapper(action)) + .toList(), + buildChild: (controller) { + return FlowyTooltip( + message: LocaleKeys.emoji_selectSkinTone.tr(), + child: FlowyIconButton( + icon: Padding( + // add a left padding to align the emoji center + padding: const EdgeInsets.only( + left: 3.0, + ), + child: FlowyText( + lastSelectedEmojiSkinTone?.icon ?? '✋', + fontSize: 22.0, + ), + ), + onPressed: () => controller.show(), + ), + ); + }, + onSelected: (action, controller) async { + widget.onEmojiSkinToneChanged(action.inner); + setState(() { + lastSelectedEmojiSkinTone = action.inner; + }); + controller.close(); + }, + ); + } +} + +class EmojiSkinToneWrapper extends ActionCell { + EmojiSkinToneWrapper(this.inner); + + final EmojiSkinTone inner; + + Widget? icon(Color iconColor) => null; + + @override + String get name { + final String i18n; + switch (inner) { + case EmojiSkinTone.none: + i18n = LocaleKeys.emoji_skinTone_default.tr(); + case EmojiSkinTone.light: + i18n = LocaleKeys.emoji_skinTone_light.tr(); + case EmojiSkinTone.mediumLight: + i18n = LocaleKeys.emoji_skinTone_mediumLight.tr(); + case EmojiSkinTone.medium: + i18n = LocaleKeys.emoji_skinTone_medium.tr(); + case EmojiSkinTone.mediumDark: + i18n = LocaleKeys.emoji_skinTone_mediumDark.tr(); + case EmojiSkinTone.dark: + i18n = LocaleKeys.emoji_skinTone_dark.tr(); + } + return '${inner.icon} $i18n'; + } +} + +extension on EmojiSkinTone { + String get icon { + switch (this) { + case EmojiSkinTone.none: + return '✋'; + case EmojiSkinTone.light: + return '✋🏻'; + case EmojiSkinTone.mediumLight: + return '✋🏼'; + case EmojiSkinTone.medium: + return '✋🏽'; + case EmojiSkinTone.mediumDark: + return '✋🏾'; + case EmojiSkinTone.dark: + return '✋🏿'; + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart new file mode 100644 index 000000000000..e60555a1eff8 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart @@ -0,0 +1,121 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flutter/material.dart'; + +enum FlowyIconType { + emoji, + icon, + custom; +} + +class FlowyIconPicker extends StatefulWidget { + const FlowyIconPicker({ + super.key, + required this.onSelected, + }); + + final void Function(FlowyIconType type, String value) onSelected; + + @override + State createState() => _FlowyIconPickerState(); +} + +class _FlowyIconPickerState extends State + with SingleTickerProviderStateMixin { + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + // ONLY supports emoji picker for now + return DefaultTabController( + length: 1, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + _buildTabs(context), + const Spacer(), + _RemoveIconButton( + onTap: () { + widget.onSelected(FlowyIconType.icon, ''); + }, + ), + ], + ), + const Divider( + height: 2, + ), + Expanded( + child: TabBarView( + children: [ + FlowyEmojiPicker( + onEmojiSelected: (_, emoji) { + widget.onSelected(FlowyIconType.emoji, emoji); + }, + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildTabs(BuildContext context) { + return Align( + alignment: Alignment.centerLeft, + child: TabBar( + indicatorSize: TabBarIndicatorSize.label, + isScrollable: true, + overlayColor: MaterialStatePropertyAll( + Theme.of(context).colorScheme.secondary, + ), + padding: EdgeInsets.zero, + tabs: [ + FlowyHover( + style: const HoverStyle(borderRadius: BorderRadius.zero), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 8.0, + ), + child: FlowyText( + LocaleKeys.emoji_emojiTab.tr(), + ), + ), + ) + ], + ), + ); + } +} + +class _RemoveIconButton extends StatelessWidget { + const _RemoveIconButton({ + required this.onTap, + }); + + final VoidCallback onTap; + @override + Widget build(BuildContext context) { + return SizedBox( + height: 28, + child: FlowyButton( + onTap: onTap, + useIntrinsicWidth: true, + text: FlowyText( + LocaleKeys.document_plugins_cover_removeIcon.tr(), + ), + leftIcon: const FlowySvg(FlowySvgs.delete_s), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker_page.dart b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker_page.dart new file mode 100644 index 000000000000..43cc4c67de87 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker_page.dart @@ -0,0 +1,48 @@ +import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart'; +import 'package:appflowy/plugins/base/icon/icon_picker.dart'; +import 'package:appflowy/workspace/application/view/view_service.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class IconPickerPage extends StatefulWidget { + const IconPickerPage({ + super.key, + required this.id, + }); + + /// view id + final String id; + + @override + State createState() => _IconPickerPageState(); +} + +class _IconPickerPageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + titleSpacing: 0, + title: const FlowyText.semibold( + 'Page icon', + fontSize: 14.0, + ), + leading: AppBarBackButton( + onTap: () => context.pop(), + ), + ), + body: SafeArea( + child: FlowyIconPicker( + onSelected: (_, emoji) { + ViewBackendService.updateViewIcon( + viewId: widget.id, + viewIcon: emoji, + ); + context.pop(); + }, + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_banner.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_banner.dart index 8a99d638ee67..8de53226930a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_banner.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_banner.dart @@ -186,10 +186,9 @@ class _BannerTitleState extends State<_BannerTitle> { controller: widget.popoverController, triggerActions: PopoverTriggerFlags.none, direction: PopoverDirection.bottomWithLeftAligned, + constraints: const BoxConstraints(maxWidth: 380, maxHeight: 300), popupBuilder: (popoverContext) => _buildEmojiPicker((emoji) { - context - .read() - .add(RowBannerEvent.setIcon(emoji.emoji)); + context.read().add(RowBannerEvent.setIcon(emoji)); widget.popoverController.close(); }), child: Row(children: children), @@ -199,7 +198,7 @@ class _BannerTitleState extends State<_BannerTitle> { } } -typedef OnSubmittedEmoji = void Function(Emoji emoji); +typedef OnSubmittedEmoji = void Function(String emoji); const _kBannerActionHeight = 40.0; class EmojiButton extends StatelessWidget { @@ -286,12 +285,9 @@ class RemoveEmojiButton extends StatelessWidget { } Widget _buildEmojiPicker(OnSubmittedEmoji onSubmitted) { - return SizedBox( - height: 250, - child: EmojiSelectionMenu( - onSubmitted: onSubmitted, - onExit: () {}, - ), + return EmojiSelectionMenu( + onSubmitted: onSubmitted, + onExit: () {}, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart index 1c424a2c83fc..607f139d3283 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart @@ -12,6 +12,7 @@ import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/util/base64_string.dart'; import 'package:appflowy/workspace/application/notifications/notification_action.dart'; import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart'; +import 'package:appflowy/workspace/application/view/prelude.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart' @@ -111,9 +112,7 @@ class _DocumentPageState extends State { styleCustomizer: EditorStyleCustomizer( context: context, // the 44 is the width of the left action list - padding: PlatformExtension.isMobile - ? const EdgeInsets.only(left: 20, right: 20) - : const EdgeInsets.only(left: 40, right: 40 + 44), + padding: EditorStyleCustomizer.documentPadding, ), header: _buildCoverAndIcon(context), ); @@ -140,6 +139,13 @@ class _DocumentPageState extends State { return DocumentHeaderNodeWidget( node: page, editorState: editorState!, + view: widget.view, + onIconChanged: (icon) async { + await ViewBackendService.updateViewIcon( + viewId: widget.view.id, + viewIcon: icon, + ); + }, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart index 8b4bf37ffbcf..2d4e48379be5 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart @@ -15,7 +15,7 @@ class EmojiPickerButton extends StatelessWidget { final String emoji; final double emojiSize; final Size emojiPickerSize; - final void Function(Emoji emoji, PopoverController controller) onSubmitted; + final void Function(String emoji, PopoverController controller) onSubmitted; final PopoverController popoverController = PopoverController(); @override diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart index 3d67ac106bfd..7ede37eb7299 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart @@ -186,7 +186,7 @@ class _CalloutBlockComponentWidgetState ), // force to refresh the popover state emoji: emoji, onSubmitted: (emoji, controller) { - setEmoji(emoji.emoji); + setEmoji(emoji); controller.close(); }, ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart index c958aac8022a..358c85ff2dc5 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart @@ -2,17 +2,23 @@ import 'dart:io'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart'; +import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart'; +import 'package:appflowy/plugins/base/icon/icon_picker.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; +import 'package:appflowy/plugins/document/presentation/editor_style.dart'; +import 'package:appflowy/workspace/application/view/view_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_listener.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/rounded_button.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; import 'cover_editor.dart'; -import 'emoji_icon_widget.dart'; -import 'emoji_popover.dart'; const double kCoverHeight = 250.0; const double kIconHeight = 60.0; @@ -45,13 +51,17 @@ enum CoverType { class DocumentHeaderNodeWidget extends StatefulWidget { const DocumentHeaderNodeWidget({ + super.key, required this.node, required this.editorState, - super.key, + required this.onIconChanged, + required this.view, }); final Node node; final EditorState editorState; + final void Function(String icon) onIconChanged; + final ViewPB view; @override State createState() => @@ -64,19 +74,33 @@ class _DocumentHeaderNodeWidgetState extends State { ); String? get coverDetails => widget.node.attributes[DocumentHeaderBlockKeys.coverDetails]; - String get icon => widget.node.attributes[DocumentHeaderBlockKeys.icon]; - bool get hasIcon => - widget.node.attributes[DocumentHeaderBlockKeys.icon]?.isNotEmpty ?? false; + String? get icon => widget.node.attributes[DocumentHeaderBlockKeys.icon]; + bool get hasIcon => viewIcon.isNotEmpty; bool get hasCover => coverType != CoverType.none; + String viewIcon = ''; + late final ViewListener viewListener; + @override void initState() { super.initState(); + final value = widget.view.icon.value; + viewIcon = value.isNotEmpty ? value : icon ?? ''; widget.node.addListener(_reload); + viewListener = ViewListener( + viewId: widget.view.id, + )..start( + onViewUpdated: (p0) { + setState(() { + viewIcon = p0.icon.value; + }); + }, + ); } @override void dispose() { + viewListener.stop(); widget.node.removeListener(_reload); super.dispose(); } @@ -108,7 +132,7 @@ class _DocumentHeaderNodeWidgetState extends State { ), if (hasIcon) Positioned( - left: 80, + left: PlatformExtension.isDesktopOrWeb ? 80 : 20, // if hasCover, there shouldn't be icons present so the icon can // be closer to the bottom. bottom: @@ -116,8 +140,10 @@ class _DocumentHeaderNodeWidgetState extends State { child: DocumentIcon( editorState: widget.editorState, node: widget.node, - icon: icon, - onIconChanged: (icon) => _saveCover(icon: icon), + icon: viewIcon, + onIconChanged: (icon) async { + _saveCover(icon: icon); + }, ), ), ], @@ -153,6 +179,7 @@ class _DocumentHeaderNodeWidgetState extends State { } if (icon != null) { attributes[DocumentHeaderBlockKeys.icon] = icon; + widget.onIconChanged(icon); } transaction.updateNode(widget.node, attributes); @@ -188,29 +215,42 @@ class _DocumentHeaderToolbarState extends State { final PopoverController _popoverController = PopoverController(); + @override + void initState() { + super.initState(); + + isHidden = PlatformExtension.isDesktopOrWeb; + } + @override Widget build(BuildContext context) { - return MouseRegion( - onEnter: (event) => setHidden(false), - onExit: (event) { - if (!isPopoverOpen) { - setHidden(true); - } - }, - opaque: false, - child: Container( - alignment: Alignment.bottomLeft, - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 40), - child: SizedBox( - height: 28, - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: buildRowChildren(), - ), + Widget child = Container( + alignment: Alignment.bottomLeft, + width: double.infinity, + padding: EditorStyleCustomizer.documentPadding, + child: SizedBox( + height: 28, + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: buildRowChildren(), ), ), ); + + if (PlatformExtension.isDesktopOrWeb) { + child = MouseRegion( + onEnter: (event) => setHidden(false), + onExit: (event) { + if (!isPopoverOpen) { + setHidden(true); + } + }, + opaque: false, + child: child, + ); + } + + return child; } List buildRowChildren() { @@ -251,42 +291,50 @@ class _DocumentHeaderToolbarState extends State { ), ); } else { - children.add( - AppFlowyPopover( + Widget child = FlowyButton( + leftIconSize: const Size.square(18), + useIntrinsicWidth: true, + leftIcon: const Icon( + Icons.emoji_emotions_outlined, + size: 18, + ), + text: FlowyText.regular( + LocaleKeys.document_plugins_cover_addIcon.tr(), + ), + onTap: PlatformExtension.isDesktop + ? null + : () => context.push( + Uri( + path: MobileEmojiPickerScreen.routeName, + queryParameters: { + MobileEmojiPickerScreen.viewId: + context.read().state.view.id, + }, + ).toString(), + ), + ); + + if (PlatformExtension.isDesktop) { + child = AppFlowyPopover( onClose: () => isPopoverOpen = false, controller: _popoverController, offset: const Offset(0, 8), direction: PopoverDirection.bottomWithCenterAligned, - constraints: BoxConstraints.loose(const Size(300, 250)), - child: FlowyButton( - leftIconSize: const Size.square(18), - useIntrinsicWidth: true, - leftIcon: const Icon( - Icons.emoji_emotions_outlined, - size: 18, - ), - text: FlowyText.regular( - LocaleKeys.document_plugins_cover_addIcon.tr(), - ), - ), + constraints: BoxConstraints.loose(const Size(360, 380)), + child: child, popupBuilder: (BuildContext popoverContext) { isPopoverOpen = true; - return EmojiPopover( - showRemoveButton: widget.hasIcon, - removeIcon: () { - widget.onCoverChanged(icon: ""); - _popoverController.close(); - }, - node: widget.node, - editorState: widget.editorState, - onEmojiChanged: (Emoji emoji) { - widget.onCoverChanged(icon: emoji.emoji); + return FlowyIconPicker( + onSelected: (type, value) { + widget.onCoverChanged(icon: value); _popoverController.close(); }, ); }, - ), - ); + ); + } + + children.add(child); } return children; @@ -471,27 +519,41 @@ class _DocumentIconState extends State { @override Widget build(BuildContext context) { - return AppFlowyPopover( - direction: PopoverDirection.bottomWithCenterAligned, - controller: _popoverController, - offset: const Offset(0, 8), - constraints: BoxConstraints.loose(const Size(320, 380)), - child: EmojiIconWidget(emoji: widget.icon), - popupBuilder: (BuildContext popoverContext) { - return EmojiPopover( - node: widget.node, - showRemoveButton: true, - removeIcon: () { - widget.onIconChanged(""); - _popoverController.close(); - }, - editorState: widget.editorState, - onEmojiChanged: (Emoji emoji) { - widget.onIconChanged(emoji.emoji); - _popoverController.close(); - }, - ); - }, + Widget child = EmojiIconWidget( + emoji: widget.icon, ); + + if (PlatformExtension.isDesktopOrWeb) { + child = AppFlowyPopover( + direction: PopoverDirection.bottomWithCenterAligned, + controller: _popoverController, + offset: const Offset(0, 8), + constraints: BoxConstraints.loose(const Size(360, 380)), + child: child, + popupBuilder: (BuildContext popoverContext) { + return FlowyIconPicker( + onSelected: (type, value) { + widget.onIconChanged(value); + _popoverController.close(); + }, + ); + }, + ); + } else { + child = GestureDetector( + child: child, + onTap: () => context.push( + Uri( + path: MobileEmojiPickerScreen.routeName, + queryParameters: { + MobileEmojiPickerScreen.viewId: + context.read().state.view.id, + }, + ).toString(), + ), + ); + } + + return child; } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_popover.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_popover.dart deleted file mode 100644 index 4dfd9ece382b..000000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_popover.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; - -import 'package:flutter/material.dart'; - -/// Add icon menu in Header -class EmojiPopover extends StatefulWidget { - final EditorState editorState; - final Node node; - final void Function(Emoji emoji) onEmojiChanged; - final VoidCallback removeIcon; - final bool showRemoveButton; - - const EmojiPopover({ - super.key, - required this.editorState, - required this.node, - required this.onEmojiChanged, - required this.removeIcon, - required this.showRemoveButton, - }); - - @override - State createState() => _EmojiPopoverState(); -} - -class _EmojiPopoverState extends State { - @override - Widget build(BuildContext context) { - return Column( - children: [ - if (widget.showRemoveButton) - Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: Align( - alignment: Alignment.centerRight, - child: DeleteButton(onTap: widget.removeIcon), - ), - ), - Expanded( - child: EmojiPicker( - onEmojiSelected: (category, emoji) { - widget.onEmojiChanged(emoji); - }, - config: buildFlowyEmojiPickerConfig(context), - ), - ), - ], - ); - } -} - -class DeleteButton extends StatelessWidget { - final VoidCallback onTap; - const DeleteButton({required this.onTap, Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 28, - child: FlowyButton( - onTap: onTap, - useIntrinsicWidth: true, - text: FlowyText( - LocaleKeys.document_plugins_cover_removeIcon.tr(), - ), - leftIcon: const FlowySvg(FlowySvgs.delete_s), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart index 503332dfef8f..d7f925d00f78 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -29,6 +29,10 @@ class EditorStyleCustomizer { throw UnimplementedError(); } + static EdgeInsets get documentPadding => PlatformExtension.isMobile + ? const EdgeInsets.only(left: 20, right: 20) + : const EdgeInsets.only(left: 40, right: 40 + 44); + EditorStyle desktop() { final theme = Theme.of(context); final fontSize = context.read().state.fontSize; diff --git a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart index 134fd36fd82e..a9415f14d6fe 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart @@ -3,6 +3,7 @@ import 'package:appflowy/mobile/presentation/database/mobile_calendar_screen.dar import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart'; import 'package:appflowy/mobile/presentation/favorite/mobile_favorite_page.dart'; import 'package:appflowy/mobile/presentation/presentation.dart'; +import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/tasks/app_widget.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; @@ -47,6 +48,9 @@ GoRouter generateRouter(Widget child) { // trash _mobileHomeTrashPageRoute(), + + // emoji picker + _mobileEmojiPickerPageRoute(), ], // Desktop and Mobile @@ -200,6 +204,21 @@ GoRoute _mobileHomeTrashPageRoute() { ); } +GoRoute _mobileEmojiPickerPageRoute() { + return GoRoute( + parentNavigatorKey: AppGlobals.rootNavKey, + path: MobileEmojiPickerScreen.routeName, + pageBuilder: (context, state) { + final id = state.uri.queryParameters[MobileEmojiPickerScreen.viewId]!; + return MaterialPage( + child: MobileEmojiPickerScreen( + id: id, + ), + ); + }, + ); +} + GoRoute _desktopHomeScreenRoute() { return GoRoute( path: DesktopHomeScreen.routeName, diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart index 3c27ea599a34..8536d50fdc1e 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart @@ -1,10 +1,9 @@ import 'dart:async'; -import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart'; -import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart'; +import 'package:dartz/dartz.dart'; class ViewBackendService { static Future> createView({ @@ -149,9 +148,24 @@ class ViewBackendService { if (isFavorite != null) { payload.isFavorite = isFavorite; } + return FolderEventUpdateView(payload).send(); } + static Future> updateViewIcon({ + required String viewId, + required String viewIcon, + }) { + final icon = ViewIconPB() + ..ty = ViewIconTypePB.Emoji + ..value = viewIcon; + final payload = UpdateViewIconPayloadPB.create() + ..viewId = viewId + ..icon = icon; + + return FolderEventUpdateViewIcon(payload).send(); + } + // deprecated static Future> moveView({ required String viewId, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart index e3ea73cfe450..05ce8902e238 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart @@ -1,10 +1,11 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/base/icon/icon_picker.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; -import 'package:appflowy/workspace/application/view/view_bloc.dart'; +import 'package:appflowy/workspace/application/view/prelude.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart'; @@ -14,6 +15,7 @@ import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.d import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; @@ -36,6 +38,7 @@ class ViewItem extends StatelessWidget { this.isFirstChild = false, this.isDraggable = true, required this.isFeedback, + this.height = 28.0, }); final ViewPB view; @@ -67,6 +70,8 @@ class ViewItem extends StatelessWidget { // identify if the view item is rendered as feedback widget inside DraggableItem final bool isFeedback; + final double height; + @override Widget build(BuildContext context) { return BlocProvider( @@ -96,6 +101,7 @@ class ViewItem extends StatelessWidget { isFirstChild: isFirstChild, isDraggable: isDraggable, isFeedback: isFeedback, + height: height, ); }, ), @@ -121,6 +127,7 @@ class InnerViewItem extends StatelessWidget { this.onTertiarySelected, this.isFirstChild = false, required this.isFeedback, + required this.height, }); final ViewPB view; @@ -140,6 +147,7 @@ class InnerViewItem extends StatelessWidget { final bool showActions; final ViewItemOnSelected onSelected; final ViewItemOnSelected? onTertiarySelected; + final double height; @override Widget build(BuildContext context) { @@ -155,6 +163,7 @@ class InnerViewItem extends StatelessWidget { isDraggable: isDraggable, leftPadding: leftPadding, isFeedback: isFeedback, + height: height, ); // if the view is expanded and has child views, render its child views @@ -233,6 +242,7 @@ class SingleInnerViewItem extends StatefulWidget { required this.onSelected, this.onTertiarySelected, required this.isFeedback, + required this.height, }); final ViewPB view; @@ -249,12 +259,16 @@ class SingleInnerViewItem extends StatefulWidget { final ViewItemOnSelected onSelected; final ViewItemOnSelected? onTertiarySelected; final FolderCategoryType categoryType; + final double height; @override State createState() => _SingleInnerViewItemState(); } class _SingleInnerViewItemState extends State { + final controller = PopoverController(); + bool isIconPickerOpened = false; + @override Widget build(BuildContext context) { if (widget.isFeedback) { @@ -266,7 +280,8 @@ class _SingleInnerViewItemState extends State { hoverColor: Theme.of(context).colorScheme.secondary, ), resetHoverOnRebuild: widget.showActions, - buildWhenOnHover: () => !widget.showActions && !_isDragging, + buildWhenOnHover: () => + !widget.showActions && !_isDragging && !isIconPickerOpened, builder: (_, onHover) => _buildViewItem(onHover), isSelected: () => widget.showActions || @@ -279,10 +294,7 @@ class _SingleInnerViewItemState extends State { // expand icon _buildLeftIcon(), // icon - SizedBox.square( - dimension: 16, - child: widget.view.defaultIcon(), - ), + _buildViewIconButton(), const HSpace(5), // title Expanded( @@ -309,7 +321,7 @@ class _SingleInnerViewItemState extends State { onTap: () => widget.onSelected(widget.view), onTertiaryTapDown: (_) => widget.onTertiarySelected?.call(widget.view), child: SizedBox( - height: 26, + height: widget.height, child: Padding( padding: EdgeInsets.only(left: widget.level * widget.leftPadding), child: Row( @@ -320,6 +332,47 @@ class _SingleInnerViewItemState extends State { ); } + Widget _buildViewIconButton() { + final icon = widget.view.icon.value.isNotEmpty + ? FlowyText( + widget.view.icon.value, + fontSize: 18.0, + ) + : SizedBox.square( + dimension: 20.0, + child: widget.view.defaultIcon(), + ); + return AppFlowyPopover( + offset: const Offset(20, 0), + controller: controller, + direction: PopoverDirection.rightWithCenterAligned, + constraints: BoxConstraints.loose(const Size(360, 380)), + onClose: () => setState(() { + isIconPickerOpened = false; + }), + child: GestureDetector( + // prevent the tap event from being passed to the parent widget + onTap: () {}, + child: FlowyTooltip( + message: LocaleKeys.document_plugins_cover_changeIcon.tr(), + child: icon, + ), + ), + popupBuilder: (context) { + isIconPickerOpened = true; + return FlowyIconPicker( + onSelected: (_, emoji) { + ViewBackendService.updateViewIcon( + viewId: widget.view.id, + viewIcon: emoji, + ); + controller.close(); + }, + ); + }, + ); + } + // > button or · button // show > if the view is expandable. // show · if the view can't contain child views. diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart index 65ad4c642e27..d7a1a266572b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart @@ -1,11 +1,10 @@ +import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra_ui/style_widget/decoration.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'emoji_picker.dart'; - SelectionMenuItem emojiMenuItem = SelectionMenuItem( name: 'Emoji', icon: (editorState, onSelected, style) => SelectableIconWidget( @@ -54,7 +53,7 @@ void showEmojiPickerMenu( ), child: EmojiSelectionMenu( onSubmitted: (emoji) { - editorState.insertTextAtCurrentSelection(emoji.emoji); + editorState.insertTextAtCurrentSelection(emoji); }, onExit: () { // close emoji panel @@ -73,7 +72,7 @@ class EmojiSelectionMenu extends StatefulWidget { required this.onExit, }) : super(key: key); - final void Function(Emoji emoji) onSubmitted; + final void Function(String emoji) onSubmitted; final void Function() onExit; @override @@ -111,9 +110,10 @@ class _EmojiSelectionMenuState extends State { @override Widget build(BuildContext context) { - return EmojiPicker( - onEmojiSelected: (category, emoji) => widget.onSubmitted(emoji), - config: buildFlowyEmojiPickerConfig(context), + return FlowyEmojiPicker( + onEmojiSelected: (_, emoji) { + widget.onSubmitted(emoji); + }, ); } } diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart index 24135d92287f..efad4a6f304a 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart @@ -22,6 +22,10 @@ class FlowyTextField extends StatefulWidget { final String? errorText; final int maxLines; final bool showCounter; + final Widget? prefixIcon; + final Widget? suffixIcon; + final BoxConstraints? prefixIconConstraints; + final BoxConstraints? suffixIconConstraints; const FlowyTextField({ super.key, @@ -42,6 +46,10 @@ class FlowyTextField extends StatefulWidget { this.errorText, this.maxLines = 1, this.showCounter = true, + this.prefixIcon, + this.suffixIcon, + this.prefixIconConstraints, + this.suffixIconConstraints, }); @override @@ -55,6 +63,8 @@ class FlowyTextFieldState extends State { @override void initState() { + super.initState(); + focusNode = widget.focusNode ?? FocusNode(); focusNode.addListener(notifyDidEndEditing); @@ -67,10 +77,10 @@ class FlowyTextFieldState extends State { WidgetsBinding.instance.addPostFrameCallback((_) { focusNode.requestFocus(); controller.selection = TextSelection.fromPosition( - TextPosition(offset: controller.text.length)); + TextPosition(offset: controller.text.length), + ); }); } - super.initState(); } void _debounceOnChangedText(Duration duration, String text) { @@ -113,6 +123,7 @@ class FlowyTextFieldState extends State { maxLength: widget.maxLength, maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds, style: widget.textStyle ?? Theme.of(context).textTheme.bodySmall, + textAlignVertical: TextAlignVertical.center, decoration: InputDecoration( constraints: BoxConstraints( maxHeight: widget.errorText?.isEmpty ?? true ? 32 : 58), @@ -158,6 +169,10 @@ class FlowyTextFieldState extends State { ), borderRadius: Corners.s8Border, ), + prefixIcon: widget.prefixIcon, + suffixIcon: widget.suffixIcon, + prefixIconConstraints: widget.prefixIconConstraints, + suffixIconConstraints: widget.suffixIconConstraints, ), ); } diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 397d874f6ee9..74658d9c6723 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -53,12 +53,11 @@ packages: appflowy_editor: dependency: "direct main" description: - path: "." - ref: "7336274" - resolved-ref: "7336274ff90402c8dd790b029e00cac60c580f28" - url: "https://github.com/AppFlowy-IO/appflowy-editor.git" - source: git - version: "1.5.0" + name: appflowy_editor + sha256: d3112408f28ca3b7b8d3d1ecc90a0c1ba7c1fe807ab285c07b1e9d312b1d3cad + url: "https://pub.dev" + source: hosted + version: "1.5.1" appflowy_popover: dependency: "direct main" description: @@ -346,6 +345,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0+3" + easy_debounce: + dependency: transitive + description: + name: easy_debounce + sha256: f082609cfb8f37defb9e37fc28bc978c6712dedf08d4c5a26f820fa10165a236 + url: "https://pub.dev" + source: hosted + version: "2.0.3" easy_localization: dependency: "direct main" description: @@ -362,6 +369,15 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.2" + emoji_mart: + dependency: "direct main" + description: + path: "." + ref: "067f718" + resolved-ref: "067f7188965c8fcb7be02ce174ce2b6757f288ee" + url: "https://github.com/LucasXu0/emoji_mart.git" + source: git + version: "0.0.1" envied: dependency: "direct main" description: @@ -533,6 +549,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + flutter_sticky_header: + dependency: transitive + description: + name: flutter_sticky_header + sha256: "017f398fbb45a589e01491861ca20eb6570a763fd9f3888165a978e11248c709" + url: "https://pub.dev" + source: hosted + version: "0.6.5" flutter_svg: dependency: "direct main" description: @@ -571,10 +595,10 @@ packages: dependency: "direct main" description: name: freezed_annotation - sha256: aeac15850ef1b38ee368d4c53ba9a847e900bb2c53a4db3f6881cbb3cb684338 + sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.4.1" frontend_server_client: dependency: transitive description: @@ -1390,6 +1414,14 @@ packages: description: flutter source: sdk version: "0.0.99" + sliver_tools: + dependency: transitive + description: + name: sliver_tools + sha256: eae28220badfb9d0559207badcbbc9ad5331aac829a88cb0964d330d2a4636a6 + url: "https://pub.dev" + source: hosted + version: "0.2.12" source_gen: dependency: transitive description: @@ -1728,6 +1760,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.7" + value_layout_builder: + dependency: transitive + description: + name: value_layout_builder + sha256: "98202ec1807e94ac72725b7f0d15027afde513c55c69ff3f41bcfccb950831bc" + url: "https://pub.dev" + source: hosted + version: "0.3.1" vector_graphics: dependency: transitive description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index abe808cd1c42..24f060e392d8 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -44,10 +44,7 @@ dependencies: git: url: https://github.com/AppFlowy-IO/appflowy-board.git ref: 6aba8dd - appflowy_editor: - git: - url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "7336274" + appflowy_editor: ^1.5.1 appflowy_popover: path: packages/appflowy_popover @@ -110,6 +107,10 @@ dependencies: go_router: ^10.1.2 string_validator: ^1.0.0 unsplash_client: ^2.1.1 + emoji_mart: + git: + url: https://github.com/LucasXu0/emoji_mart.git + ref: "067f718" # Notifications # TODO: Consider implementing custom package diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 4adb7acc98aa..5aea6e5b446a 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -619,13 +619,14 @@ "add": "Add", "back": "Back", "saveToGallery": "Save to gallery", - "removeIcon": "Remove Icon", + "removeIcon": "Remove icon", "pasteImageUrl": "Paste image URL", "or": "OR", "pickFromFiles": "Pick from files", "couldNotFetchImage": "Could not fetch image", "imageSavingFailed": "Image Saving Failed", - "addIcon": "Add Icon", + "addIcon": "Add icon", + "changeIcon": "Change icon", "coverRemoveAlert": "It will be removed from cover after it is deleted.", "alertDialogConfirmation": "Are you sure, you want to continue?" }, @@ -818,6 +819,7 @@ "gray": "Gray" }, "emoji": { + "emojiTab": "Emoji", "search": "Search emoji", "noRecent": "No recent emoji", "noEmojiFound": "No emoji found", @@ -837,6 +839,14 @@ "flags": "Flags", "nature": "Nature", "frequentlyUsed": "Frequently Used" + }, + "skinTone": { + "default": "Default", + "light": "Light", + "mediumLight": "Medium-Light", + "medium": "Medium", + "mediumDark": "Medium-Dark", + "dark": "Dark" } }, "inlineActions": { From 1ad85416d8893657db7f052a8c02c52a4ea7ca31 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 2 Nov 2023 15:24:35 +0800 Subject: [PATCH 16/56] fix: unable to build macOS universal package on x86 machine (#3863) * chore: don't use cache when building release package * fix: set ARCHS = ARCHS_STANDARD on macOS --- .../macos/Runner.xcodeproj/project.pbxproj | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/frontend/appflowy_flutter/macos/Runner.xcodeproj/project.pbxproj b/frontend/appflowy_flutter/macos/Runner.xcodeproj/project.pbxproj index 827d1150522c..e34603ede3cf 100644 --- a/frontend/appflowy_flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/frontend/appflowy_flutter/macos/Runner.xcodeproj/project.pbxproj @@ -421,7 +421,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { - ARCHS = arm64; + ARCHS = "$(ARCHS_STANDARD)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; @@ -436,7 +436,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - ONLY_ACTIVE_ARCH = false; + ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.appflowy.appflowy.flutter; PRODUCT_NAME = AppFlowy; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -558,7 +558,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { - ARCHS = arm64; + ARCHS = "$(ARCHS_STANDARD)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; @@ -573,7 +573,6 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - ONLY_ACTIVE_ARCH = false; PRODUCT_BUNDLE_IDENTIFIER = com.appflowy.appflowy.flutter; PRODUCT_NAME = AppFlowy; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -587,7 +586,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { - ARCHS = arm64; + ARCHS = "$(ARCHS_STANDARD)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; @@ -602,7 +601,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - ONLY_ACTIVE_ARCH = false; + ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.appflowy.appflowy.flutter; PRODUCT_NAME = AppFlowy; PROVISIONING_PROFILE_SPECIFIER = ""; From dc0af0f4c15d0ffd70d8712e8437dcbed082e0b0 Mon Sep 17 00:00:00 2001 From: "Kilu.He" <108015703+qinluhe@users.noreply.github.com> Date: Thu, 2 Nov 2023 22:13:29 +0800 Subject: [PATCH 17/56] feat: support convert external data to nested json (#3848) * feat: support convert external data to nested json * fix: add some comment * fix: code review * fix: code review * fix: code view * fix: code view * fix: update tauri cargo lock file * fix: remove reduant function * fix: parse dir attribute in element * fix: add comment about parse dir * fix: code review * fix: code review * fix: code review * fix: code review --- frontend/appflowy_tauri/src-tauri/Cargo.lock | 20 +- frontend/rust-lib/Cargo.lock | 20 +- .../src/document/document_event.rs | 17 +- .../tests/document/local_test/test.rs | 58 +- frontend/rust-lib/flowy-document2/Cargo.toml | 3 +- .../rust-lib/flowy-document2/src/entities.rs | 2 + .../flowy-document2/src/event_handler.rs | 91 ++- .../rust-lib/flowy-document2/src/event_map.rs | 54 +- .../flowy-document2/src/parser/constant.rs | 88 ++- .../src/parser/document_data_parser.rs | 175 +++--- .../src/parser/external/mod.rs | 2 + .../src/parser/external/parser.rs | 40 ++ .../src/parser/external/utils.rs | 559 ++++++++++++++++++ .../flowy-document2/src/parser/mod.rs | 1 + .../src/parser/parser_entities.rs | 254 ++++---- .../flowy-document2/src/parser/utils.rs | 89 +-- .../tests/assets/html/bulleted_list.html | 2 +- .../tests/assets/html/callout.html | 4 +- .../tests/assets/html/google_docs.html | 1 + .../tests/assets/html/notion.html | 34 ++ .../tests/assets/html/numbered_list.html | 2 +- .../tests/assets/html/todo_list.html | 2 +- .../tests/assets/html/toggle_list.html | 2 +- .../tests/assets/json/google_docs.json | 351 +++++++++++ .../tests/assets/json/notion.json | 371 ++++++++++++ .../tests/assets/json/plain_text.json | 510 ++++++++++++++++ .../tests/assets/text/plain_text.txt | 64 ++ .../flowy-document2/tests/parser/html/mod.rs | 1 + .../tests/parser/html/parser_test.rs | 45 ++ .../flowy-document2/tests/parser/mod.rs | 3 +- .../{html_text => parse_to_html_text}/mod.rs | 0 .../{html_text => parse_to_html_text}/test.rs | 2 +- .../utils.rs | 0 33 files changed, 2529 insertions(+), 338 deletions(-) create mode 100644 frontend/rust-lib/flowy-document2/src/parser/external/mod.rs create mode 100644 frontend/rust-lib/flowy-document2/src/parser/external/parser.rs create mode 100644 frontend/rust-lib/flowy-document2/src/parser/external/utils.rs create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/html/google_docs.html create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/html/notion.html create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/json/google_docs.json create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/json/notion.json create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/json/plain_text.json create mode 100644 frontend/rust-lib/flowy-document2/tests/assets/text/plain_text.txt create mode 100644 frontend/rust-lib/flowy-document2/tests/parser/html/mod.rs create mode 100644 frontend/rust-lib/flowy-document2/tests/parser/html/parser_test.rs rename frontend/rust-lib/flowy-document2/tests/parser/{html_text => parse_to_html_text}/mod.rs (100%) rename frontend/rust-lib/flowy-document2/tests/parser/{html_text => parse_to_html_text}/test.rs (90%) rename frontend/rust-lib/flowy-document2/tests/parser/{html_text => parse_to_html_text}/utils.rs (100%) diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index ddfc303bd27f..adc7299a62ea 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -770,7 +770,7 @@ dependencies = [ "parking_lot", "realtime-entity", "reqwest", - "scraper", + "scraper 0.17.1", "serde", "serde_json", "serde_repr", @@ -2064,6 +2064,7 @@ dependencies = [ "nanoid", "parking_lot", "protobuf", + "scraper 0.18.0", "serde", "serde_json", "strum_macros 0.21.1", @@ -2071,6 +2072,7 @@ dependencies = [ "tokio-stream", "tracing", "uuid", + "validator", ] [[package]] @@ -5354,6 +5356,22 @@ dependencies = [ "tendril", ] +[[package]] +name = "scraper" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3693f9a0203d49a7ba8f38aa915316b3d535c1862d03dae7009cb71a3408b36a" +dependencies = [ + "ahash 0.8.3", + "cssparser 0.31.2", + "ego-tree", + "getopts", + "html5ever 0.26.0", + "once_cell", + "selectors 0.25.0", + "tendril", +] + [[package]] name = "sct" version = "0.7.0" diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 06dd20e10572..188eb32f5bf1 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -668,7 +668,7 @@ dependencies = [ "parking_lot", "realtime-entity", "reqwest", - "scraper", + "scraper 0.17.1", "serde", "serde_json", "serde_repr", @@ -1885,6 +1885,7 @@ dependencies = [ "nanoid", "parking_lot", "protobuf", + "scraper 0.18.0", "serde", "serde_json", "strum_macros 0.21.1", @@ -1894,6 +1895,7 @@ dependencies = [ "tracing", "tracing-subscriber", "uuid", + "validator", ] [[package]] @@ -4701,6 +4703,22 @@ dependencies = [ "tendril", ] +[[package]] +name = "scraper" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3693f9a0203d49a7ba8f38aa915316b3d535c1862d03dae7009cb71a3408b36a" +dependencies = [ + "ahash 0.8.3", + "cssparser", + "ego-tree", + "getopts", + "html5ever", + "once_cell", + "selectors", + "tendril", +] + [[package]] name = "sct" version = "0.7.0" diff --git a/frontend/rust-lib/event-integration/src/document/document_event.rs b/frontend/rust-lib/event-integration/src/document/document_event.rs index 866ce61154e8..4a2b782f9416 100644 --- a/frontend/rust-lib/event-integration/src/document/document_event.rs +++ b/frontend/rust-lib/event-integration/src/document/document_event.rs @@ -5,7 +5,8 @@ use serde_json::Value; use flowy_document2::entities::*; use flowy_document2::event_map::DocumentEvent; use flowy_document2::parser::parser_entities::{ - ConvertDocumentPayloadPB, ConvertDocumentResponsePB, + ConvertDataToJsonPayloadPB, ConvertDataToJsonResponsePB, ConvertDocumentPayloadPB, + ConvertDocumentResponsePB, }; use flowy_folder2::entities::{CreateViewPayloadPB, ViewLayoutPB, ViewPB}; use flowy_folder2::event_map::FolderEvent; @@ -124,6 +125,20 @@ impl DocumentEventTest { .parse::() } + // convert data to json for document event test + pub async fn convert_data_to_json( + &self, + payload: ConvertDataToJsonPayloadPB, + ) -> ConvertDataToJsonResponsePB { + let core = &self.inner; + EventBuilder::new(core.clone()) + .event(DocumentEvent::ConvertDataToJSON) + .payload(payload) + .async_send() + .await + .parse::() + } + pub async fn create_text(&self, payload: TextDeltaPayloadPB) { let core = &self.inner; EventBuilder::new(core.clone()) diff --git a/frontend/rust-lib/event-integration/tests/document/local_test/test.rs b/frontend/rust-lib/event-integration/tests/document/local_test/test.rs index b03320f247b8..86cea382590b 100644 --- a/frontend/rust-lib/event-integration/tests/document/local_test/test.rs +++ b/frontend/rust-lib/event-integration/tests/document/local_test/test.rs @@ -2,7 +2,9 @@ use collab_document::blocks::json_str_to_hashmap; use event_integration::document::document_event::DocumentEventTest; use event_integration::document::utils::*; use flowy_document2::entities::*; -use flowy_document2::parser::parser_entities::{ConvertDocumentPayloadPB, ExportTypePB}; +use flowy_document2::parser::parser_entities::{ + ConvertDataToJsonPayloadPB, ConvertDocumentPayloadPB, InputType, NestedBlock, ParseTypePB, +}; use serde_json::{json, Value}; use std::collections::HashMap; @@ -125,7 +127,7 @@ async fn apply_text_delta_test() { macro_rules! generate_convert_document_test_cases { ($($json:ident, $text:ident, $html:ident),*) => { [ - $((ExportTypePB { json: $json, text: $text, html: $html }, ($json, $text, $html))),* + $((ParseTypePB { json: $json, text: $text, html: $html }, ($json, $text, $html))),* ] }; } @@ -145,7 +147,7 @@ async fn convert_document_test() { let copy_payload = ConvertDocumentPayloadPB { document_id: view.id.to_string(), range: None, - export_types: export_types.clone(), + parse_types: export_types.clone(), }; let result = test.convert_document(copy_payload).await; assert_eq!(result.json.is_some(), *json_assert); @@ -153,3 +155,53 @@ async fn convert_document_test() { assert_eq!(result.html.is_some(), *html_assert); } } + +/// test convert data to json +/// - input html:

    Hello

    World!

    +/// - input plain text: Hello World! +/// - output json: { "type": "page", "data": {}, "children": [{ "type": "paragraph", "children": [], "data": { "delta": [{ "insert": "Hello" }] } }, { "type": "paragraph", "children": [], "data": { "delta": [{ "insert": " World!" }] } }] } +#[tokio::test] +async fn convert_data_to_json_test() { + let test = DocumentEventTest::new().await; + let _ = test.create_document().await; + + let html = r#"

    Hello

    World!

    "#; + let payload = ConvertDataToJsonPayloadPB { + data: html.to_string(), + input_type: InputType::Html, + }; + let result = test.convert_data_to_json(payload).await; + let expect_json = json!({ + "type": "page", + "data": {}, + "children": [{ + "type": "paragraph", + "children": [], + "data": { + "delta": [{ "insert": "Hello" }] + } + }, { + "type": "paragraph", + "children": [], + "data": { + "delta": [{ "insert": "World!" }] + } + }] + }); + + let expect_json = serde_json::from_value::(expect_json).unwrap(); + assert!(serde_json::from_str::(&result.json) + .unwrap() + .eq(&expect_json)); + + let plain_text = "Hello\nWorld!"; + let payload = ConvertDataToJsonPayloadPB { + data: plain_text.to_string(), + input_type: InputType::PlainText, + }; + let result = test.convert_data_to_json(payload).await; + + assert!(serde_json::from_str::(&result.json) + .unwrap() + .eq(&expect_json)); +} diff --git a/frontend/rust-lib/flowy-document2/Cargo.toml b/frontend/rust-lib/flowy-document2/Cargo.toml index 1bf274ad6d59..332176aed495 100644 --- a/frontend/rust-lib/flowy-document2/Cargo.toml +++ b/frontend/rust-lib/flowy-document2/Cargo.toml @@ -18,7 +18,7 @@ flowy-notification = { workspace = true } flowy-error = { path = "../flowy-error", features = ["impl_from_serde", "impl_from_sqlite", "impl_from_dispatch_error", "impl_from_collab"] } lib-dispatch = { workspace = true } lib-infra = { path = "../../../shared-lib/lib-infra" } - +validator = "0.16.0" protobuf = {version = "2.28.0"} bytes = { version = "1.5" } nanoid = "0.4.0" @@ -33,6 +33,7 @@ indexmap = {version = "1.9.2", features = ["serde"]} uuid = { version = "1.3.3", features = ["v4"] } futures = "0.3.26" tokio-stream = { version = "0.1.14", features = ["sync"] } +scraper = "0.18.0" [dev-dependencies] tempfile = "3.4.0" diff --git a/frontend/rust-lib/flowy-document2/src/entities.rs b/frontend/rust-lib/flowy-document2/src/entities.rs index 52efbed21b63..8e3d68ef6d55 100644 --- a/frontend/rust-lib/flowy-document2/src/entities.rs +++ b/frontend/rust-lib/flowy-document2/src/entities.rs @@ -319,6 +319,7 @@ pub struct ExportDataPB { #[pb(index = 2)] pub export_type: ExportType, } + #[derive(PartialEq, Eq, Debug, ProtoBuf_Enum, Clone, Default)] pub enum ConvertType { #[default] @@ -337,6 +338,7 @@ impl From for ConvertType { } } +/// for convert data to document /// for the json type /// the data is the json string #[derive(Default, ProtoBuf, Debug)] diff --git a/frontend/rust-lib/flowy-document2/src/event_handler.rs b/frontend/rust-lib/flowy-document2/src/event_handler.rs index a576caf697eb..4f1d3bb700f2 100644 --- a/frontend/rust-lib/flowy-document2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-document2/src/event_handler.rs @@ -12,14 +12,18 @@ use collab_document::blocks::{ }; use flowy_error::{FlowyError, FlowyResult}; -use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; +use lib_dispatch::prelude::{ + data_result_ok, AFPluginData, AFPluginDataValidator, AFPluginState, DataResult, +}; use crate::entities::*; use crate::parser::document_data_parser::DocumentDataParser; use crate::parser::parser_entities::{ + ConvertDataToJsonParams, ConvertDataToJsonPayloadPB, ConvertDataToJsonResponsePB, ConvertDocumentParams, ConvertDocumentPayloadPB, ConvertDocumentResponsePB, }; +use crate::parser::external::parser::ExternalDataToNestedJSONParser; use crate::{manager::DocumentManager, parser::json::parser::JsonToDocumentParser}; fn upgrade_document( @@ -309,16 +313,46 @@ impl From<(&Vec, bool)> for DocEventPB { } } -/** -* Handler for converting a document to a JSON string, HTML string, or plain text string. - -* @param data: AFPluginData<[ConvertDocumentPayloadPB]> - -* @param manager: AFPluginState> - -* @return DataResult<[ConvertDocumentResponsePB], FlowyError> - */ -pub async fn convert_document( +/// Handler for converting a document to a JSON string, HTML string, or plain text string. +/// +/// ConvertDocumentPayloadPB is the input of this event. +/// ConvertDocumentResponsePB is the output of this event. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ```txt +/// // document: [{ "block_id": "1", "type": "paragraph", "data": {"delta": [{ "insert": "Hello World!" }] } }, { "block_id": "2", "type": "paragraph", "data": {"delta": [{ "insert": "Hello World!" }] } +/// let test = DocumentEventTest::new().await; +/// let view = test.create_document().await; +/// let payload = ConvertDocumentPayloadPB { +/// document_id: view.id, +/// range: Some(RangePB { +/// start: SelectionPB { +/// block_id: "1".to_string(), +/// index: 0, +/// length: 5, +/// }, +/// end: SelectionPB { +/// block_id: "2".to_string(), +/// index: 5, +/// length: 7, +/// } +/// }), +/// parse_types: ParseTypePB { +/// json: true, +/// text: true, +/// html: true, +/// }, +/// }; +/// let result = test.convert_document(payload).await; +/// assert_eq!(result.json, Some("[{ \"block_id\": \"1\", \"type\": \"paragraph\", \"data\": {\"delta\": [{ \"insert\": \"Hello\" }] } }, { \"block_id\": \"2\", \"type\": \"paragraph\", \"data\": {\"delta\": [{ \"insert\": \" World!\" }] } }".to_string())); +/// assert_eq!(result.text, Some("Hello\n World!".to_string())); +/// assert_eq!(result.html, Some("

    Hello

    World!

    ".to_string())); +/// ``` +/// # +pub async fn convert_document_handler( data: AFPluginData, manager: AFPluginState>, ) -> DataResult { @@ -329,7 +363,7 @@ pub async fn convert_document( let document_data = document.lock().get_document_data()?; let parser = DocumentDataParser::new(Arc::new(document_data), params.range); - if !params.export_types.any_enabled() { + if !params.parse_types.any_enabled() { return data_result_ok(ConvertDocumentResponsePB::default()); } @@ -337,16 +371,43 @@ pub async fn convert_document( data_result_ok(ConvertDocumentResponsePB { json: params - .export_types + .parse_types .json .then(|| serde_json::to_string(root).unwrap_or_default()), html: params - .export_types + .parse_types .html .then(|| parser.to_html_with_json(root)), text: params - .export_types + .parse_types .text .then(|| parser.to_text_with_json(root)), }) } + +/// Handler for converting a string to a JSON string. +/// # Examples +/// Basic usage: +/// ```txt +/// let test = DocumentEventTest::new().await; +/// let payload = ConvertDataToJsonPayloadPB { +/// data: "

    Hello

    World!

    ".to_string(), +/// input_type: InputTypePB::Html, +/// }; +/// let result: ConvertDataToJsonResponsePB = test.convert_data_to_json(payload).await; +/// let expect_json = json!({ "type": "page", "data": {}, "children": [{ "type": "paragraph", "children": [], "data": { "delta": [{ "insert": "Hello" }] } }, { "type": "paragraph", "children": [], "data": { "delta": [{ "insert": " World!" }] } }] }); +/// assert!(serde_json::from_str::(&result.json).unwrap().eq(&serde_json::from_value::(expect_json).unwrap())); +/// ``` +pub(crate) async fn convert_data_to_json_handler( + data: AFPluginData, +) -> DataResult { + let payload: ConvertDataToJsonParams = data.validate()?.into_inner().try_into()?; + let parser = ExternalDataToNestedJSONParser::new(payload.data, payload.input_type); + + let result = match parser.to_nested_block() { + Some(result) => serde_json::to_string(&result)?, + None => "".to_string(), + }; + + data_result_ok(ConvertDataToJsonResponsePB { json: result }) +} diff --git a/frontend/rust-lib/flowy-document2/src/event_map.rs b/frontend/rust-lib/flowy-document2/src/event_map.rs index e7c4dcd13ffb..a43967aa14f9 100644 --- a/frontend/rust-lib/flowy-document2/src/event_map.rs +++ b/frontend/rust-lib/flowy-document2/src/event_map.rs @@ -5,7 +5,6 @@ use strum_macros::Display; use flowy_derive::{Flowy_Event, ProtoBuf_Enum}; use lib_dispatch::prelude::AFPlugin; -use crate::event_handler::convert_document; use crate::event_handler::get_snapshot_handler; use crate::{event_handler::*, manager::DocumentManager}; @@ -28,7 +27,11 @@ pub fn init(document_manager: Weak) -> AFPlugin { .event(DocumentEvent::GetDocumentSnapshots, get_snapshot_handler) .event(DocumentEvent::CreateText, create_text_handler) .event(DocumentEvent::ApplyTextDeltaEvent, apply_text_delta_handler) - .event(DocumentEvent::ConvertDocument, convert_document) + .event(DocumentEvent::ConvertDocument, convert_document_handler) + .event( + DocumentEvent::ConvertDataToJSON, + convert_data_to_json_handler, + ) } #[derive(Debug, Clone, PartialEq, Eq, Hash, Display, ProtoBuf_Enum, Flowy_Event)] @@ -79,48 +82,17 @@ pub enum DocumentEvent { #[event(input = "TextDeltaPayloadPB")] ApplyTextDeltaEvent = 11, - /// Handler for converting a document to a JSON string, HTML string, or plain text string. - /// - /// ConvertDocumentPayloadPB is the input of this event. - /// ConvertDocumentResponsePB is the output of this event. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```txt - /// // document: [{ "block_id": "1", "type": "paragraph", "data": {"delta": [{ "insert": "Hello World!" }] } }, { "block_id": "2", "type": "paragraph", "data": {"delta": [{ "insert": "Hello World!" }] } - /// let test = DocumentEventTest::new().await; - /// let view = test.create_document().await; - /// let payload = ConvertDocumentPayloadPB { - /// document_id: view.id, - /// range: Some(RangePB { - /// start: SelectionPB { - /// block_id: "1".to_string(), - /// index: 0, - /// length: 5, - /// }, - /// end: SelectionPB { - /// block_id: "2".to_string(), - /// index: 5, - /// length: 7, - /// } - /// }), - /// export_types: ConvertTypePB { - /// json: true, - /// text: true, - /// html: true, - /// }, - /// }; - /// let result = test.convert_document(payload).await; - /// assert_eq!(result.json, Some("[{ \"block_id\": \"1\", \"type\": \"paragraph\", \"data\": {\"delta\": [{ \"insert\": \"Hello\" }] } }, { \"block_id\": \"2\", \"type\": \"paragraph\", \"data\": {\"delta\": [{ \"insert\": \" World!\" }] } }".to_string())); - /// assert_eq!(result.text, Some("Hello\n World!".to_string())); - /// assert_eq!(result.html, Some("

    Hello

    World!

    ".to_string())); - /// ``` - /// # + // document in event_handler.rs -> convert_document #[event( input = "ConvertDocumentPayloadPB", output = "ConvertDocumentResponsePB" )] ConvertDocument = 12, + + // document in event_handler.rs -> convert_data_to_json + #[event( + input = "ConvertDataToJsonPayloadPB", + output = "ConvertDataToJsonResponsePB" + )] + ConvertDataToJSON = 13, } diff --git a/frontend/rust-lib/flowy-document2/src/parser/constant.rs b/frontend/rust-lib/flowy-document2/src/parser/constant.rs index d5c4d56e6b74..c13722fcd3f8 100644 --- a/frontend/rust-lib/flowy-document2/src/parser/constant.rs +++ b/frontend/rust-lib/flowy-document2/src/parser/constant.rs @@ -32,6 +32,92 @@ pub const CODE: &str = "code"; pub const UNDERLINE: &str = "underline"; pub const FONT_COLOR: &str = "font_color"; pub const BG_COLOR: &str = "bg_color"; -pub const HREF: &str = "href"; + pub const FORMULA: &str = "formula"; pub const MENTION: &str = "mention"; + +pub const TEXT_DIRECTION: &str = "text_direction"; + +pub const HTML_TAG_NAME: &str = "html"; +pub const HR_TAG_NAME: &str = "hr"; +pub const META_TAG_NAME: &str = "meta"; +pub const LINK_TAG_NAME: &str = "link"; +pub const SCRIPT_TAG_NAME: &str = "script"; +pub const STYLE_TAG_NAME: &str = "style"; +pub const IFRAME_TAG_NAME: &str = "iframe"; +pub const NOSCRIPT_TAG_NAME: &str = "noscript"; +pub const HEAD_TAG_NAME: &str = "head"; +pub const H1_TAG_NAME: &str = "h1"; +pub const H2_TAG_NAME: &str = "h2"; +pub const H3_TAG_NAME: &str = "h3"; +pub const H4_TAG_NAME: &str = "h4"; +pub const H5_TAG_NAME: &str = "h5"; +pub const H6_TAG_NAME: &str = "h6"; +pub const P_TAG_NAME: &str = "p"; +pub const ASIDE_TAG_NAME: &str = "aside"; +pub const ARTICLE_TAG_NAME: &str = "article"; +pub const UL_TAG_NAME: &str = "ul"; +pub const OL_TAG_NAME: &str = "ol"; +pub const LI_TAG_NAME: &str = "li"; +pub const BLOCKQUOTE_TAG_NAME: &str = "blockquote"; +pub const PRE_TAG_NAME: &str = "pre"; +pub const IMG_TAG_NAME: &str = "img"; +pub const B_TAG_NAME: &str = "b"; +pub const CODE_TAG_NAME: &str = "code"; +pub const STRONG_TAG_NAME: &str = "strong"; +pub const EM_TAG_NAME: &str = "em"; +pub const U_TAG_NAME: &str = "u"; +pub const S_TAG_NAME: &str = "s"; +pub const SPAN_TAG_NAME: &str = "span"; +pub const BR_TAG_NAME: &str = "br"; + +pub const A_TAG_NAME: &str = "a"; +pub const BASE_TAG_NAME: &str = "base"; +pub const ABBR_TAG_NAME: &str = "abbr"; +pub const ADDRESS_TAG_NAME: &str = "address"; +pub const DBO_TAG_NAME: &str = "bdo"; +pub const DIR_ATTR_NAME: &str = "dir"; + +pub const RTL_ATTR_VALUE: &str = "rtl"; + +pub const CITE_TAG_NAME: &str = "cite"; + +pub const DEL_TAG_NAME: &str = "del"; + +pub const DETAILS_TAG_NAME: &str = "details"; + +pub const SUMMARY_TAG_NAME: &str = "summary"; + +pub const DFN_TAG_NAME: &str = "dfn"; + +pub const DL_TAG_NAME: &str = "dl"; + +pub const I_TAG_NAME: &str = "i"; +pub const VAR_TAG_NAME: &str = "var"; + +pub const INS_TAG_NAME: &str = "ins"; +pub const MENU_TAG_NAME: &str = "menu"; + +pub const MARK_TAG_NAME: &str = "mark"; + +pub const FONT_WEIGHT: &str = "font-weight"; +pub const FONT_STYLE: &str = "font-style"; +pub const TEXT_DECORATION: &str = "text-decoration"; + +pub const BACKGROUND_COLOR: &str = "background-color"; +pub const COLOR: &str = "color"; +pub const LINE_THROUGH: &str = "line-through"; + +pub const FONT_STYLE_ITALIC: &str = "font-style: italic;"; +pub const TEXT_DECORATION_UNDERLINE: &str = "text-decoration: underline;"; +pub const TEXT_DECORATION_LINE_THROUGH: &str = "text-decoration: line-through;"; +pub const FONT_WEIGHT_BOLD: &str = "font-weight: bold;"; +pub const FONT_FAMILY_FANTASY: &str = "font-family: fantasy;"; + +pub const SRC: &str = "src"; +pub const HREF: &str = "href"; +pub const ROLE: &str = "role"; +pub const CHECKBOX: &str = "checkbox"; +pub const ARIA_CHECKED: &str = "aria-checked"; +pub const CLASS: &str = "class"; +pub const STYLE: &str = "style"; diff --git a/frontend/rust-lib/flowy-document2/src/parser/document_data_parser.rs b/frontend/rust-lib/flowy-document2/src/parser/document_data_parser.rs index 5339c7eff30e..d92857f7b721 100644 --- a/frontend/rust-lib/flowy-document2/src/parser/document_data_parser.rs +++ b/frontend/rust-lib/flowy-document2/src/parser/document_data_parser.rs @@ -1,10 +1,7 @@ -use crate::parser::parser_entities::{ConvertBlockToHtmlParams, NestedBlock, Range}; -use crate::parser::utils::{ - block_to_nested_json, get_delta_for_block, get_delta_for_selection, get_flat_block_ids, - ConvertBlockToJsonParams, -}; +use crate::parser::constant::DELTA; +use crate::parser::parser_entities::{ConvertBlockToHtmlParams, InsertDelta, NestedBlock, Range}; +use crate::parser::utils::{get_delta_for_block, get_delta_for_selection}; use collab_document::blocks::DocumentData; -use std::collections::HashMap; use std::sync::Arc; /// DocumentDataParser is a struct for parsing a document's data and converting it to JSON, HTML, or text. @@ -61,120 +58,94 @@ impl DocumentDataParser { /// Converts the document data to a nested JSON structure, considering the optional range. pub fn to_json(&self) -> Option { let root_id = &self.document_data.page_id; - // flatten the block id list. - let block_id_list = get_flat_block_ids(root_id, &self.document_data); - - // collect the block ids in the range. - let mut in_range_block_ids = self.collect_in_range_block_ids(&block_id_list); - // insert the root block id if it is not in the in-range block ids. - if !in_range_block_ids.contains(root_id) { - in_range_block_ids.push(root_id.to_string()); - } - - // build the parameters for converting the block to JSON with the in-range block ids. - let convert_params = self.build_convert_json_params(&in_range_block_ids); - // convert the root block to JSON. - let mut root = block_to_nested_json(root_id, &convert_params)?; - - // If the start block's parent is outside the in-range selection, we need to insert the start block. - if self.should_insert_start_block() { - self.insert_start_block_json(&mut root, &convert_params); - } - - Some(root) + let mut children = vec![]; + let mut start_found = false; + let mut end_found = false; + self.block_to_nested_block(root_id, &mut children, &mut start_found, &mut end_found) } - /// Collects the block ids in the range. - fn collect_in_range_block_ids(&self, block_id_list: &Vec) -> Vec { - if let Some(range) = &self.range { - // Find the positions of start and end block IDs in the list - let mut start_index = block_id_list - .iter() - .position(|id| id == &range.start.block_id) - .unwrap_or(0); - let mut end_index = block_id_list - .iter() - .position(|id| id == &range.end.block_id) - .unwrap_or(0); - - if start_index > end_index { - // Swap start and end if they are in reverse order - std::mem::swap(&mut start_index, &mut end_index); + fn block_to_nested_block( + &self, + block_id: &str, + children: &mut Vec, + start_found: &mut bool, + end_found: &mut bool, + ) -> Option { + let block = self.document_data.blocks.get(block_id)?; + let delta = self.get_delta(block_id); + + // Prepare the data, including delta if available + let mut data = block.data.clone(); + if let Some(delta) = delta { + if let Ok(delta_value) = serde_json::to_value(delta) { + data.insert(DELTA.to_string(), delta_value); } - - // Slice the block IDs based on the positions of start and end - block_id_list[start_index..=end_index].to_vec() - } else { - // If no range is specified, return the entire list - block_id_list.to_owned() } - } - /// Builds the parameters for converting the block to JSON. - /// ConvertBlockToJsonParams format: - /// { - /// blocks: HashMap>, // in-range blocks - /// relation_map: HashMap>>, // in-range blocks' children - /// delta_map: HashMap, // in-range blocks' delta - /// } - fn build_convert_json_params(&self, block_id_list: &[String]) -> ConvertBlockToJsonParams { - let mut delta_map = HashMap::new(); - let mut in_range_blocks = HashMap::new(); - let mut relation_map = HashMap::new(); - - for block_id in block_id_list { - if let Some(block) = self.document_data.blocks.get(block_id) { - // Insert the block into the in-range block map. - in_range_blocks.insert(block_id.to_string(), Arc::new(block.to_owned())); - - // If the block has children, insert the children into the relation map. - if let Some(children) = self.document_data.meta.children_map.get(&block.children) { - relation_map.insert(block_id.to_string(), Arc::new(children.to_owned())); + // Get the child IDs for the current block + if let Some(block_children_ids) = self.document_data.meta.children_map.get(&block.children) { + for child_id in block_children_ids { + if let Some(range) = &self.range { + if child_id == &range.start.block_id { + *start_found = true; + } + + if child_id == &range.end.block_id { + *end_found = true; + // Process the "end" block recursively + self.process_child_block(child_id, children, start_found, end_found); + break; + } } - let delta = match &self.range { - Some(range) if block_id == &range.start.block_id => { - get_delta_for_selection(&range.start, &self.document_data) - }, - Some(range) if block_id == &range.end.block_id => { - get_delta_for_selection(&range.end, &self.document_data) - }, - _ => get_delta_for_block(block_id, &self.document_data), - }; - - // If the delta exists, insert it into the delta map. - if let Some(delta) = delta { - delta_map.insert(block_id.to_string(), delta); + if self.range.is_some() { + if !*start_found { + // Don't insert children before the "start" block is found + self.block_to_nested_block(child_id, children, start_found, end_found); + continue; + } + if *end_found { + // Stop inserting children after the "end" block is found + break; + } } + + // Process child blocks recursively + self.process_child_block(child_id, children, start_found, end_found); } } - ConvertBlockToJsonParams { - blocks: in_range_blocks, - relation_map, - delta_map, - } + Some(NestedBlock { + ty: block.ty.clone(), + children: children.to_owned(), + data, + }) } - // Checks if the start block should be inserted whether the start block's parent is outside the in-range selection. - fn should_insert_start_block(&self) -> bool { - if let Some(range) = &self.range { - if let Some(start_block) = self.document_data.blocks.get(&range.start.block_id) { - return start_block.parent != self.document_data.page_id; - } + fn get_delta(&self, block_id: &str) -> Option> { + match &self.range { + Some(range) if block_id == range.start.block_id => { + get_delta_for_selection(&range.start, &self.document_data) + }, + Some(range) if block_id == range.end.block_id => { + get_delta_for_selection(&range.end, &self.document_data) + }, + _ => get_delta_for_block(block_id, &self.document_data), } - false } - // Inserts the start block JSON to the root JSON. - fn insert_start_block_json( + fn process_child_block( &self, - root: &mut NestedBlock, - convert_params: &ConvertBlockToJsonParams, + child_id: &str, + children: &mut Vec, + start_found: &mut bool, + end_found: &mut bool, ) { - let start = &self.range.as_ref().unwrap().start; - if let Some(start_block_json) = block_to_nested_json(&start.block_id, convert_params) { - root.children.insert(0, start_block_json); + let mut child_children = vec![]; + if let Some(child) = + self.block_to_nested_block(child_id, &mut child_children, start_found, end_found) + { + children.push(child); } } } diff --git a/frontend/rust-lib/flowy-document2/src/parser/external/mod.rs b/frontend/rust-lib/flowy-document2/src/parser/external/mod.rs new file mode 100644 index 000000000000..8a43408ba153 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/src/parser/external/mod.rs @@ -0,0 +1,2 @@ +pub mod parser; +mod utils; diff --git a/frontend/rust-lib/flowy-document2/src/parser/external/parser.rs b/frontend/rust-lib/flowy-document2/src/parser/external/parser.rs new file mode 100644 index 000000000000..4bc3618744f5 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/src/parser/external/parser.rs @@ -0,0 +1,40 @@ +use crate::parser::external::utils::{flatten_element_to_block, parse_plaintext_to_nested_block}; +use crate::parser::parser_entities::{InputType, NestedBlock}; +use scraper::Html; + +/// External data to nested json parser. +#[derive(Debug, Clone, Default)] +pub struct ExternalDataToNestedJSONParser { + /// External data. for example: html string, plain text string. + external_data: String, + /// External data type. for example: [InputType]::Html, [InputType]::PlainText. + input_type: InputType, +} + +impl ExternalDataToNestedJSONParser { + pub fn new(data: String, input_type: InputType) -> Self { + Self { + external_data: data, + input_type, + } + } + + /// Format to nested block. + /// + /// Example: + /// - input html:

    Hello

    World!

    + /// - output json: + /// ```json + /// { "type": "page", "data": {}, "children": [{ "type": "paragraph", "children": [], "data": { "delta": [{ "insert": "Hello", attributes: { "bold": true } }] } }, { "type": "paragraph", "children": [], "data": { "delta": [{ "insert": " World!", attributes: null }] } }] } + /// ``` + pub fn to_nested_block(&self) -> Option { + match self.input_type { + InputType::Html => { + let fragment = Html::parse_fragment(&self.external_data); + let root_element = fragment.root_element(); + flatten_element_to_block(root_element) + }, + InputType::PlainText => parse_plaintext_to_nested_block(&self.external_data), + } + } +} diff --git a/frontend/rust-lib/flowy-document2/src/parser/external/utils.rs b/frontend/rust-lib/flowy-document2/src/parser/external/utils.rs new file mode 100644 index 000000000000..d170706cd3f4 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/src/parser/external/utils.rs @@ -0,0 +1,559 @@ +use crate::parser::constant::*; +use crate::parser::parser_entities::{InsertDelta, NestedBlock}; +use scraper::node::Attrs; +use scraper::ElementRef; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::HashMap; + +const INLINE_TAGS: [&str; 18] = [ + A_TAG_NAME, + EM_TAG_NAME, + STRONG_TAG_NAME, + U_TAG_NAME, + S_TAG_NAME, + CODE_TAG_NAME, + SPAN_TAG_NAME, + ADDRESS_TAG_NAME, + BASE_TAG_NAME, + CITE_TAG_NAME, + DFN_TAG_NAME, + I_TAG_NAME, + VAR_TAG_NAME, + ABBR_TAG_NAME, + INS_TAG_NAME, + DEL_TAG_NAME, + MARK_TAG_NAME, + "", +]; + +const LINK_TAGS: [&str; 2] = [A_TAG_NAME, BASE_TAG_NAME]; +const ITALIC_TAGS: [&str; 6] = [ + EM_TAG_NAME, + I_TAG_NAME, + VAR_TAG_NAME, + CITE_TAG_NAME, + DFN_TAG_NAME, + ADDRESS_TAG_NAME, +]; + +const BOLD_TAGS: [&str; 2] = [STRONG_TAG_NAME, B_TAG_NAME]; + +const UNDERLINE_TAGS: [&str; 3] = [U_TAG_NAME, ABBR_TAG_NAME, INS_TAG_NAME]; +const STRIKETHROUGH_TAGS: [&str; 2] = [S_TAG_NAME, DEL_TAG_NAME]; +const IGNORE_TAGS: [&str; 7] = [ + META_TAG_NAME, + HEAD_TAG_NAME, + LINK_TAG_NAME, + SCRIPT_TAG_NAME, + STYLE_TAG_NAME, + NOSCRIPT_TAG_NAME, + IFRAME_TAG_NAME, +]; + +const HEADING_TAGS: [&str; 6] = [ + H1_TAG_NAME, + H2_TAG_NAME, + H3_TAG_NAME, + H4_TAG_NAME, + H5_TAG_NAME, + H6_TAG_NAME, +]; + +const SHOULD_EXPAND_TAGS: [&str; 4] = [UL_TAG_NAME, OL_TAG_NAME, DL_TAG_NAME, MENU_TAG_NAME]; + +#[derive(Debug, Serialize, Deserialize)] +pub enum JSONResult { + Block(NestedBlock), + Delta(InsertDelta), + BlockArray(Vec), + DeltaArray(Vec), +} + +/// Flatten element to block +pub fn flatten_element_to_block(node: ElementRef) -> Option { + if let Some(JSONResult::Block(block)) = flatten_element_to_json(node, &None, &None) { + return Some(block); + } + + None +} + +/// Parse plaintext to nested block +pub fn parse_plaintext_to_nested_block(plaintext: &str) -> Option { + let lines: Vec<&str> = plaintext + .lines() + .filter(|line| !line.trim().is_empty()) + .collect(); + let mut current_block = NestedBlock { + ty: PAGE.to_string(), + ..Default::default() + }; + + for line in lines { + let mut data = HashMap::new(); + + // Insert plaintext into delta + if let Ok(delta) = serde_json::to_value(vec![InsertDelta { + insert: line.to_string(), + attributes: None, + }]) { + data.insert(DELTA.to_string(), delta); + } + + // Create a new block for each non-empty line + current_block.children.push(NestedBlock { + ty: PARAGRAPH.to_string(), + data, + children: Default::default(), + }); + } + + if current_block.children.is_empty() { + return None; + } + Some(current_block) +} + +fn flatten_element_to_json( + node: ElementRef, + list_type: &Option, + attributes: &Option>, +) -> Option { + let tag_name = get_tag_name(node.to_owned()); + + if IGNORE_TAGS.contains(&tag_name.as_str()) { + return None; + } + + if INLINE_TAGS.contains(&tag_name.as_str()) { + return process_inline_element(node, attributes.to_owned()); + } + + let mut data = HashMap::new(); + // insert dir into attrs when dir is rtl + // for example: Right to left -> { "attributes": { "text_direction": "rtl" }, "insert": "Right to left" } + if let Some(dir) = find_attribute_value(node.to_owned(), DIR_ATTR_NAME) { + data.insert(TEXT_DIRECTION.to_string(), Value::String(dir)); + } + + if HEADING_TAGS.contains(&tag_name.as_str()) { + return process_heading_element(node, data); + } + + if SHOULD_EXPAND_TAGS.contains(&tag_name.as_str()) { + return process_nested_element(node); + } + + match tag_name.as_str() { + LI_TAG_NAME => process_li_element(node, list_type.to_owned(), data), + BLOCKQUOTE_TAG_NAME | DETAILS_TAG_NAME => { + process_node_summary_and_details(QUOTE.to_string(), node, data) + }, + PRE_TAG_NAME => process_code_element(node), + IMG_TAG_NAME => process_image_element(node), + B_TAG_NAME => { + // Compatible with Google Docs, is the document top level tag, so we need to process it's children + let id = find_attribute_value(node.to_owned(), "id"); + if id.is_some() { + return process_nested_element(node); + } + process_inline_element(node, attributes.to_owned()) + }, + + _ => process_default_element(node, data), + } +} + +fn process_default_element( + node: ElementRef, + mut data: HashMap, +) -> Option { + let tag_name = get_tag_name(node.to_owned()); + + let ty = match tag_name.as_str() { + HTML_TAG_NAME => PAGE, + P_TAG_NAME => PARAGRAPH, + ASIDE_TAG_NAME | ARTICLE_TAG_NAME => CALLOUT, + HR_TAG_NAME => DIVIDER, + _ => PARAGRAPH, + }; + + let (delta, children) = process_node_children(node, &None, None); + + if !delta.is_empty() { + data.insert(DELTA.to_string(), delta_to_json(&delta)); + } + Some(JSONResult::Block(NestedBlock { + ty: ty.to_string(), + children, + data, + })) +} + +fn process_image_element(node: ElementRef) -> Option { + let mut data = HashMap::new(); + if let Some(src) = find_attribute_value(node, SRC) { + data.insert(URL.to_string(), Value::String(src)); + } + Some(JSONResult::Block(NestedBlock { + ty: IMAGE.to_string(), + children: Default::default(), + data, + })) +} + +fn process_code_element(node: ElementRef) -> Option { + let mut data = HashMap::new(); + + // find code element and get language and delta, then insert into data + if let Some(code_child) = find_child_node(node.to_owned(), CODE_TAG_NAME.to_string()) { + // get language + if let Some(class) = find_attribute_value(code_child.to_owned(), CLASS) { + let lang = class.split('-').last().unwrap_or_default(); + data.insert(LANGUAGE.to_string(), Value::String(lang.to_string())); + } + // get delta + let text = code_child.text().collect::(); + if let Ok(delta) = serde_json::to_value(vec![InsertDelta { + insert: text, + attributes: None, + }]) { + data.insert(DELTA.to_string(), delta); + } + } + + Some(JSONResult::Block(NestedBlock { + ty: CODE.to_string(), + children: Default::default(), + data, + })) +} + +// process "ul" | "ol" | "dl" | "menu" element +fn process_nested_element(node: ElementRef) -> Option { + let tag_name = get_tag_name(node.to_owned()); + + let ty = match tag_name.as_str() { + UL_TAG_NAME => BULLETED_LIST, + OL_TAG_NAME => NUMBERED_LIST, + _ => PARAGRAPH, + }; + let (_, children) = process_node_children(node, &Some(ty.to_string()), None); + Some(JSONResult::BlockArray(children)) +} + +// process
  • element, if it's a checkbox, then return a todo list, otherwise return a normal list. +fn process_li_element( + node: ElementRef, + list_type: Option, + mut data: HashMap, +) -> Option { + let mut ty = list_type.unwrap_or(BULLETED_LIST.to_string()); + if let Some(role) = find_attribute_value(node.to_owned(), ROLE) { + if role == CHECKBOX { + if let Some(checked_attr) = find_attribute_value(node.to_owned(), ARIA_CHECKED) { + let checked = match checked_attr.as_str() { + "true" => true, + "false" => false, + _ => false, + }; + data.insert( + CHECKED.to_string(), + serde_json::to_value(checked).unwrap_or_default(), + ); + } + data.insert( + CHECKED.to_string(), + serde_json::to_value(false).unwrap_or_default(), + ); + ty = TODO_LIST.to_string(); + } + } + process_node_summary_and_details(ty, node, data) +} + +// Process children and handle potential nesting +//
  • +//

    title

    +//

    content

    +//
  • +// Or Process children and handle potential consecutive arrangement +//
  • title

    content

  • +// li | blockquote | details +fn process_node_summary_and_details( + ty: String, + node: ElementRef, + mut data: HashMap, +) -> Option { + let (delta, children) = process_node_children(node, &Some(ty.to_string()), None); + if delta.is_empty() { + if let Some(first_child) = children.first() { + let mut data = HashMap::new(); + if let Some(first_child_delta) = first_child.data.get(DELTA) { + data.insert(DELTA.to_string(), first_child_delta.to_owned()); + let rest_children = children.iter().skip(1).cloned().collect(); + return Some(JSONResult::Block(NestedBlock { + ty, + children: rest_children, + data, + })); + } + } + } else { + data.insert(DELTA.to_string(), delta_to_json(&delta)); + } + Some(JSONResult::Block(NestedBlock { + ty, + children, + data: data.to_owned(), + })) +} + +fn process_heading_element( + node: ElementRef, + mut data: HashMap, +) -> Option { + let tag_name = get_tag_name(node.to_owned()); + let level = match tag_name.chars().last().unwrap_or_default() { + '1' => 1, + '2' => 2, + // default to h3 even if it's h4, h5, h6 + _ => 3, + }; + + data.insert( + LEVEL.to_string(), + serde_json::to_value(level).unwrap_or_default(), + ); + + let (delta, children) = process_node_children(node, &None, None); + if !delta.is_empty() { + data.insert( + DELTA.to_string(), + serde_json::to_value(delta).unwrap_or_default(), + ); + } + + Some(JSONResult::Block(NestedBlock { + ty: HEADING.to_string(), + children, + data, + })) +} + +// process
    +fn process_inline_element( + node: ElementRef, + attributes: Option>, +) -> Option { + let tag_name = get_tag_name(node.to_owned()); + + let attributes = get_delta_attributes_for(&tag_name, &get_node_attrs(node), attributes); + let (delta, children) = process_node_children(node, &None, attributes); + Some(if !delta.is_empty() { + JSONResult::DeltaArray(delta) + } else { + JSONResult::BlockArray(children) + }) +} + +fn process_node_children( + node: ElementRef, + list_type: &Option, + attributes: Option>, +) -> (Vec, Vec) { + let tag_name = get_tag_name(node.to_owned()); + let mut delta = Vec::new(); + let mut children = Vec::new(); + + for child in node.children() { + if let Some(child_element) = ElementRef::wrap(child) { + if let Some(child_json) = flatten_element_to_json(child_element, list_type, &attributes) { + match child_json { + JSONResult::Delta(op) => delta.push(op), + JSONResult::Block(block) => children.push(block), + JSONResult::BlockArray(blocks) => children.extend(blocks), + JSONResult::DeltaArray(ops) => delta.extend(ops), + } + } + } else { + // put text into delta while child is a text node + let text = child + .value() + .as_text() + .map(|text| text.text.to_string()) + .unwrap_or_default(); + + if let Some(op) = node_to_delta(&tag_name, text, &mut get_node_attrs(node), &attributes) { + delta.push(op); + } + } + } + + (delta, children) +} + +// get attributes from style +// for example: style="font-weight: bold; font-style: italic; text-decoration: underline; text-decoration: line-through;" +fn get_attributes_with_style(style: &str) -> HashMap { + let mut attributes = HashMap::new(); + + for property in style.split(';') { + let parts: Vec<&str> = property.split(':').map(|s| s.trim()).collect::>(); + + if parts.len() != 2 { + continue; + } + + let (key, value) = (parts[0], parts[1]); + + match key { + FONT_WEIGHT if value.contains(BOLD) => { + attributes.insert(BOLD.to_string(), Value::Bool(true)); + }, + FONT_STYLE if value.contains(ITALIC) => { + attributes.insert(ITALIC.to_string(), Value::Bool(true)); + }, + TEXT_DECORATION if value.contains(UNDERLINE) => { + attributes.insert(UNDERLINE.to_string(), Value::Bool(true)); + }, + TEXT_DECORATION if value.contains(LINE_THROUGH) => { + attributes.insert(STRIKETHROUGH.to_string(), Value::Bool(true)); + }, + BACKGROUND_COLOR => { + attributes.insert(BG_COLOR.to_string(), Value::String(value.to_string())); + }, + COLOR => { + attributes.insert(FONT_COLOR.to_string(), Value::String(value.to_string())); + }, + _ => {}, + } + } + + attributes +} + +// get attributes from tag name +// input
    Google +// export attributes: { "href": "https://www.google.com" } +// input Italic +// export attributes: { "italic": true } +// input Bold +// export attributes: { "bold": true } +// input Underline +// export attributes: { "underline": true } +// input Strikethrough +// export attributes: { "strikethrough": true } +// input Code +// export attributes: { "code": true } +fn get_delta_attributes_for( + tag_name: &str, + attrs: &Attrs, + parent_attributes: Option>, +) -> Option> { + let href = find_attribute_value_from_attrs(attrs, HREF); + + let style = find_attribute_value_from_attrs(attrs, STYLE); + + let mut attributes = get_attributes_with_style(&style); + if let Some(parent_attributes) = parent_attributes { + parent_attributes.iter().for_each(|(k, v)| { + attributes.insert(k.to_string(), v.clone()); + }); + } + + match tag_name { + CODE_TAG_NAME => { + attributes.insert(CODE.to_string(), Value::Bool(true)); + }, + MARK_TAG_NAME => { + attributes.insert(BG_COLOR.to_string(), Value::String("#FFFF00".to_string())); + }, + _ => { + if LINK_TAGS.contains(&tag_name) { + attributes.insert(HREF.to_string(), Value::String(href)); + } + if ITALIC_TAGS.contains(&tag_name) { + attributes.insert(ITALIC.to_string(), Value::Bool(true)); + } + if BOLD_TAGS.contains(&tag_name) { + attributes.insert(BOLD.to_string(), Value::Bool(true)); + } + if UNDERLINE_TAGS.contains(&tag_name) { + attributes.insert(UNDERLINE.to_string(), Value::Bool(true)); + } + if STRIKETHROUGH_TAGS.contains(&tag_name) { + attributes.insert(STRIKETHROUGH.to_string(), Value::Bool(true)); + } + }, + } + if attributes.is_empty() { + None + } else { + Some(attributes) + } +} + +// transform text_node to delta +// input Google +// export delta: [{ "insert": "Google", "attributes": { "href": "https://www.google.com" } }] +fn node_to_delta( + tag_name: &str, + text: String, + attrs: &mut Attrs, + parent_attributes: &Option>, +) -> Option { + let attributes = get_delta_attributes_for(tag_name, attrs, parent_attributes.to_owned()); + if text.trim().is_empty() { + return None; + } + + Some(InsertDelta { + insert: text, + attributes, + }) +} + +// get tag name from node +fn get_tag_name(node: ElementRef) -> String { + node.value().name().to_string() +} + +fn get_node_attrs(node: ElementRef) -> Attrs { + node.value().attrs() +} +// find attribute value from node +fn find_attribute_value(node: ElementRef, attr_name: &str) -> Option { + node + .value() + .attrs() + .find(|(name, _)| *name == attr_name) + .map(|(_, value)| value.to_string()) +} + +fn find_attribute_value_from_attrs(attrs: &Attrs, attr_name: &str) -> String { + // The attrs need to be mutable, because the find method will consume the attrs + // So we clone it and use the clone one + let mut attrs = attrs.clone(); + attrs + .find(|(name, _)| *name == attr_name) + .map(|(_, value)| value.to_string()) + .unwrap_or_default() +} + +fn find_child_node(node: ElementRef, child_tag_name: String) -> Option { + node + .children() + .find(|child| { + if let Some(child_element) = ElementRef::wrap(child.to_owned()) { + return get_tag_name(child_element) == child_tag_name; + } + false + }) + .and_then(|child| ElementRef::wrap(child.to_owned())) +} + +fn delta_to_json(delta: &Vec) -> Value { + serde_json::to_value(delta).unwrap_or_default() +} diff --git a/frontend/rust-lib/flowy-document2/src/parser/mod.rs b/frontend/rust-lib/flowy-document2/src/parser/mod.rs index 0c040e6e5134..305d7ee0e80d 100644 --- a/frontend/rust-lib/flowy-document2/src/parser/mod.rs +++ b/frontend/rust-lib/flowy-document2/src/parser/mod.rs @@ -1,5 +1,6 @@ pub mod constant; pub mod document_data_parser; +pub mod external; pub mod json; pub mod parser_entities; pub mod utils; diff --git a/frontend/rust-lib/flowy-document2/src/parser/parser_entities.rs b/frontend/rust-lib/flowy-document2/src/parser/parser_entities.rs index 0fec927dcd11..cb7bf35e27e8 100644 --- a/frontend/rust-lib/flowy-document2/src/parser/parser_entities.rs +++ b/frontend/rust-lib/flowy-document2/src/parser/parser_entities.rs @@ -1,19 +1,16 @@ use crate::parse::NotEmptyStr; -use crate::parser::constant::{ - BG_COLOR, BOLD, BULLETED_LIST, CALLOUT, CHECKED, CODE, DELTA, DIVIDER, FONT_COLOR, FORMULA, - HEADING, HREF, ICON, IMAGE, ITALIC, LANGUAGE, LEVEL, MATH_EQUATION, NUMBERED_LIST, PAGE, - PARAGRAPH, QUOTE, STRIKETHROUGH, TODO_LIST, TOGGLE_LIST, UNDERLINE, URL, -}; +use crate::parser::constant::*; use crate::parser::utils::{ convert_insert_delta_from_json, convert_nested_block_children_to_html, delta_to_html, - delta_to_text, + delta_to_text, required_not_empty_str, serialize_color_attribute, }; -use flowy_derive::ProtoBuf; +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; use std::sync::Arc; +use validator::Validate; #[derive(Default, ProtoBuf)] pub struct SelectionPB { @@ -43,7 +40,7 @@ pub struct RangePB { * @field text: bool // export text data */ #[derive(Default, ProtoBuf, Debug, Clone)] -pub struct ExportTypePB { +pub struct ParseTypePB { #[pb(index = 1)] pub json: bool, @@ -57,7 +54,7 @@ pub struct ExportTypePB { * ConvertDocumentPayloadPB * @field document_id: String * @file range: Option - optional // if range is None, copy the whole document - * @field export_types: [ExportTypePB] + * @field parse_types: [ParseTypePB] */ #[derive(Default, ProtoBuf)] pub struct ConvertDocumentPayloadPB { @@ -68,7 +65,7 @@ pub struct ConvertDocumentPayloadPB { pub range: Option, #[pb(index = 3)] - pub export_types: ExportTypePB, + pub parse_types: ParseTypePB, } #[derive(Default, ProtoBuf, Debug)] @@ -92,7 +89,7 @@ pub struct Range { pub end: Selection, } -pub struct ExportType { +pub struct ParseType { pub json: bool, pub html: bool, pub text: bool, @@ -101,10 +98,10 @@ pub struct ExportType { pub struct ConvertDocumentParams { pub document_id: String, pub range: Option, - pub export_types: ExportType, + pub parse_types: ParseType, } -impl ExportType { +impl ParseType { pub fn any_enabled(&self) -> bool { self.json || self.html || self.text } @@ -129,9 +126,9 @@ impl From for Range { } } -impl From for ExportType { - fn from(data: ExportTypePB) -> Self { - ExportType { +impl From for ParseType { + fn from(data: ParseTypePB) -> Self { + ParseType { json: data.json, html: data.html, text: data.text, @@ -148,7 +145,7 @@ impl TryInto for ConvertDocumentPayloadPB { Ok(ConvertDocumentParams { document_id: document_id.0, range, - export_types: self.export_types.into(), + parse_types: self.parse_types.into(), }) } } @@ -169,88 +166,88 @@ impl InsertDelta { pub fn to_html(&self) -> String { let mut html = String::new(); let mut style = String::new(); + let mut html_attributes = String::new(); // If there are attributes, serialize them as a HashMap. if let Some(attrs) = &self.attributes { - // Serialize the font color attributes. - if let Some(color) = attrs.get(FONT_COLOR) { - style.push_str(&format!( - "color: {};", - color.to_string().replace("0x", "#").trim_matches('\"') - )); - } + // Serialize the color attributes. + style.push_str(&serialize_color_attribute(attrs, FONT_COLOR, COLOR)); // Serialize the background color attributes. - if let Some(color) = attrs.get(BG_COLOR) { - style.push_str(&format!( - "background-color: {};", - color.to_string().replace("0x", "#").trim_matches('\"') - )); - } + style.push_str(&serialize_color_attribute( + attrs, + BG_COLOR, + BACKGROUND_COLOR, + )); // Serialize the href attributes. if let Some(href) = attrs.get(HREF) { - html.push_str(&format!("", href)); + html.push_str(&format!("<{} {}={}>", A_TAG_NAME, HREF, href)); } - // Serialize the code attributes. if let Some(code) = attrs.get(CODE) { if code.as_bool().unwrap_or(false) { - html.push_str(""); + html.push_str(&format!("<{}>", CODE_TAG_NAME)); } } + // Serialize the italic, underline, strikethrough, bold, formula attributes. if let Some(italic) = attrs.get(ITALIC) { if italic.as_bool().unwrap_or(false) { - style.push_str("font-style: italic;"); + style.push_str(FONT_STYLE_ITALIC); } } if let Some(underline) = attrs.get(UNDERLINE) { if underline.as_bool().unwrap_or(false) { - style.push_str("text-decoration: underline;"); + style.push_str(TEXT_DECORATION_UNDERLINE); } } if let Some(strikethrough) = attrs.get(STRIKETHROUGH) { if strikethrough.as_bool().unwrap_or(false) { - style.push_str("text-decoration: line-through;"); + style.push_str(TEXT_DECORATION_LINE_THROUGH); } } if let Some(bold) = attrs.get(BOLD) { if bold.as_bool().unwrap_or(false) { - style.push_str("font-weight: bold;"); + style.push_str(FONT_WEIGHT_BOLD); } } if let Some(formula) = attrs.get(FORMULA) { if formula.as_bool().unwrap_or(false) { - style.push_str("font-family: fantasy;"); + style.push_str(FONT_FAMILY_FANTASY); } } + if let Some(direction) = attrs.get(TEXT_DIRECTION) { + html_attributes.push_str(&format!(" {}=\"{}\"", DIR_ATTR_NAME, direction)); + } } - // Serialize the attributes to style. if !style.is_empty() { - html.push_str(&format!("", style)); + html_attributes.push_str(&format!(" {}=\"{}\"", STYLE, style)); + } + + if !html_attributes.is_empty() { + html.push_str(&format!("<{}{}>", SPAN_TAG_NAME, html_attributes)); } // Serialize the insert field. html.push_str(&self.insert); // Close the style tag. - if !style.is_empty() { - html.push_str(""); + if !html_attributes.is_empty() { + html.push_str(&format!("", SPAN_TAG_NAME)); } // Close the tags: , . if let Some(attrs) = &self.attributes { - if attrs.contains_key(HREF) { - html.push_str(""); - } if attrs.contains_key(CODE) { - html.push_str(""); + html.push_str(&format!("", CODE_TAG_NAME)); + } + if attrs.contains_key(HREF) { + html.push_str(&format!("", A_TAG_NAME)); } } html } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct NestedBlock { #[serde(default)] - pub id: String, #[serde(rename = "type")] pub ty: String, #[serde(default)] @@ -262,7 +259,6 @@ pub struct NestedBlock { impl Eq for NestedBlock {} impl PartialEq for NestedBlock { - // ignore the id field fn eq(&self, other: &Self) -> bool { self.ty == other.ty && self.data.iter().all(|(k, v)| { @@ -278,24 +274,9 @@ impl PartialEq for NestedBlock { } } -pub struct ConvertBlockToHtmlParams { - pub prev_block_ty: Option, - pub next_block_ty: Option, -} - impl NestedBlock { - pub fn new( - id: String, - ty: String, - data: HashMap, - children: Vec, - ) -> Self { - Self { - id, - ty, - data, - children, - } + pub fn new(ty: String, data: HashMap, children: Vec) -> Self { + Self { ty, data, children } } pub fn add_child(&mut self, child: NestedBlock) { @@ -316,115 +297,147 @@ impl NestedBlock { let next_block_ty = params.next_block_ty.unwrap_or_default(); match self.ty.as_str() { + //

    Hello

    HEADING => { let level = self.data.get(LEVEL).unwrap_or(&Value::Null); if level.as_u64().unwrap_or(0) > 6 { - html.push_str(&format!("
    {}
    ", text_html)); + html.push_str(&format!("<{}>{}", H6_TAG_NAME, text_html, H6_TAG_NAME)); } else { html.push_str(&format!("{}", level, text_html, level)); } }, + //

    Hello

    PARAGRAPH => { - html.push_str(&format!("

    {}

    ", text_html)); + html.push_str(&format!("<{}>{}", P_TAG_NAME, text_html, P_TAG_NAME)); html.push_str(&convert_nested_block_children_to_html(Arc::new( self.to_owned(), ))); }, + // CALLOUT => { html.push_str(&format!( - "

    {}{}

    ", + "<{}>{}{}", + ASIDE_TAG_NAME, self .data .get(ICON) .unwrap_or(&Value::Null) .to_string() .trim_matches('\"'), - text_html + text_html, + ASIDE_TAG_NAME )); }, + // Google Logo IMAGE => { html.push_str(&format!( - "{}", + "<{} src={} alt={} />", + IMG_TAG_NAME, self.data.get(URL).unwrap(), "AppFlowy-Image" )); }, + //
    DIVIDER => { - html.push_str("
    "); + html.push_str(&format!("<{} />", HR_TAG_NAME)); }, + //

    $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$

    MATH_EQUATION => { let formula = self.data.get(FORMULA).unwrap_or(&Value::Null); html.push_str(&format!( - "

    {}

    ", - formula.to_string().trim_matches('\"') + "<{}>{}", + P_TAG_NAME, + formula.to_string().trim_matches('\"'), + P_TAG_NAME )); }, + //
    console.log('Hello World!');
    CODE => { let language = self.data.get(LANGUAGE).unwrap_or(&Value::Null); html.push_str(&format!( - "
    {}
    ", + "<{}><{} {}=\"{}-{}\">{}", + PRE_TAG_NAME, + CODE_TAG_NAME, + CLASS, + LANGUAGE, language.to_string().trim_matches('\"'), - text_html + text_html, + CODE_TAG_NAME, + PRE_TAG_NAME + )); + }, + //
    Hello

    World!

    + TOGGLE_LIST => { + html.push_str(&format!("<{}>", DETAILS_TAG_NAME)); + html.push_str(&format!( + "<{}>{}", + SUMMARY_TAG_NAME, text_html, SUMMARY_TAG_NAME )); + html.push_str(&convert_nested_block_children_to_html(Arc::new( + self.to_owned(), + ))); + html.push_str(&format!("", DETAILS_TAG_NAME)); }, - BULLETED_LIST | NUMBERED_LIST | TODO_LIST | TOGGLE_LIST => { - let list_type = match self.ty.as_str() { - BULLETED_LIST => "ul", - NUMBERED_LIST => "ol", - TODO_LIST => "ul", - TOGGLE_LIST => "ul", - _ => "ul", // Default to "ul" for unknown types + //
    • Hello
    • World!
    + BULLETED_LIST | NUMBERED_LIST | TODO_LIST => { + let list_type = if self.ty == NUMBERED_LIST { + OL_TAG_NAME + } else { + UL_TAG_NAME }; if prev_block_ty != self.ty { html.push_str(&format!("<{}>", list_type)); } if self.ty == TODO_LIST { - let checked_str = if self + let checked = self .data .get(CHECKED) - .and_then(|checked| checked.as_bool()) - .unwrap_or(false) - { - "x" - } else { - " " - }; - html.push_str(&format!("
  • [{}] {}
  • ", checked_str, text_html)); + .and_then(|v| v.as_bool()) + .unwrap_or_default(); + //
  • Hello
  • + html.push_str(&format!( + "<{} {}=\"{}\" {}=\"{}\">{}", + LI_TAG_NAME, ROLE, CHECKBOX, ARIA_CHECKED, checked, text_html + )); } else { - html.push_str(&format!("
  • {}
  • ", text_html)); + html.push_str(&format!("<{}>{}", LI_TAG_NAME, text_html)); } html.push_str(&convert_nested_block_children_to_html(Arc::new( self.to_owned(), ))); + html.push_str(&format!("", LI_TAG_NAME)); if next_block_ty != self.ty { html.push_str(&format!("", list_type)); } }, + //

    Hello

    World!

    QUOTE => { if prev_block_ty != self.ty { - html.push_str("
    "); + html.push_str(&format!("<{}>", BLOCKQUOTE_TAG_NAME)); } - html.push_str(&format!("

    {}

    ", text_html)); + html.push_str(&format!("<{}>{}", P_TAG_NAME, text_html, P_TAG_NAME)); html.push_str(&convert_nested_block_children_to_html(Arc::new( self.to_owned(), ))); if next_block_ty != self.ty { - html.push_str("
    "); + html.push_str(&format!("", BLOCKQUOTE_TAG_NAME)); } }, + //

    Hello

    PAGE => { if !text_html.is_empty() { - html.push_str(&format!("

    {}

    ", text_html)); + html.push_str(&format!("<{}>{}", P_TAG_NAME, text_html, P_TAG_NAME)); } html.push_str(&convert_nested_block_children_to_html(Arc::new( self.to_owned(), ))); }, + //

    Hello

    _ => { - html.push_str(&format!("

    {}

    ", text_html)); + html.push_str(&format!("<{}>{}", P_TAG_NAME, text_html, P_TAG_NAME)); html.push_str(&convert_nested_block_children_to_html(Arc::new( self.to_owned(), ))); @@ -439,7 +452,7 @@ impl NestedBlock { let delta_text = self .data - .get("delta") + .get(DELTA) .and_then(convert_insert_delta_from_json) .map(|delta| delta_to_text(&delta)) .unwrap_or_default(); @@ -479,3 +492,46 @@ impl NestedBlock { text } } + +pub struct ConvertBlockToHtmlParams { + pub prev_block_ty: Option, + pub next_block_ty: Option, +} + +#[derive(PartialEq, Eq, Debug, ProtoBuf_Enum, Clone, Default)] +pub enum InputType { + #[default] + Html = 0, + PlainText = 1, +} + +#[derive(Default, ProtoBuf, Debug, Validate)] +pub struct ConvertDataToJsonPayloadPB { + #[pb(index = 1)] + #[validate(custom = "required_not_empty_str")] + pub data: String, + + #[pb(index = 2)] + pub input_type: InputType, +} + +pub struct ConvertDataToJsonParams { + pub data: String, + pub input_type: InputType, +} + +#[derive(Default, ProtoBuf, Debug)] +pub struct ConvertDataToJsonResponsePB { + #[pb(index = 1)] + pub json: String, +} + +impl TryInto for ConvertDataToJsonPayloadPB { + type Error = ErrorCode; + fn try_into(self) -> Result { + Ok(ConvertDataToJsonParams { + data: self.data, + input_type: self.input_type, + }) + } +} diff --git a/frontend/rust-lib/flowy-document2/src/parser/utils.rs b/frontend/rust-lib/flowy-document2/src/parser/utils.rs index 0897164e7052..e5365f222786 100644 --- a/frontend/rust-lib/flowy-document2/src/parser/utils.rs +++ b/frontend/rust-lib/flowy-document2/src/parser/utils.rs @@ -1,74 +1,11 @@ -use crate::parser::constant::DELTA; use crate::parser::parser_entities::{ ConvertBlockToHtmlParams, InsertDelta, NestedBlock, Selection, }; -use collab_document::blocks::{Block, DocumentData}; +use collab_document::blocks::DocumentData; use serde_json::Value; use std::collections::HashMap; use std::sync::Arc; - -pub struct ConvertBlockToJsonParams { - pub(crate) blocks: HashMap>, - pub(crate) relation_map: HashMap>>, - pub(crate) delta_map: HashMap>, -} -pub fn block_to_nested_json( - block_id: &str, - convert_params: &ConvertBlockToJsonParams, -) -> Option { - let blocks = &convert_params.blocks; - let relation_map = &convert_params.relation_map; - let delta_map = &convert_params.delta_map; - // Attempt to retrieve the block using the block_id - let block = blocks.get(block_id)?; - - // Retrieve the children for this block from the relation map - let children = relation_map.get(&block.id)?; - - // Recursively convert children blocks to JSON - let children: Vec<_> = children - .iter() - .filter_map(|child_id| block_to_nested_json(child_id, convert_params)) - .collect(); - - // Clone block data - let mut data = block.data.clone(); - - // Insert delta into data if available - if let Some(delta) = delta_map.get(&block.id) { - if let Ok(delta_value) = serde_json::to_value(delta) { - data.insert(DELTA.to_string(), delta_value); - } - } - - // Create and return the NestedBlock - Some(NestedBlock { - id: block.id.to_string(), - ty: block.ty.to_string(), - children, - data, - }) -} - -pub fn get_flat_block_ids(block_id: &str, data: &DocumentData) -> Vec { - let blocks = &data.blocks; - let children_map = &data.meta.children_map; - - if let Some(block) = blocks.get(block_id) { - let mut result = vec![block.id.clone()]; - - if let Some(child_ids) = children_map.get(&block.children) { - for child_id in child_ids { - let child_blocks = get_flat_block_ids(child_id, data); - result.extend(child_blocks); - } - - return result; - } - } - - vec![] -} +use validator::ValidationError; pub fn get_delta_for_block(block_id: &str, data: &DocumentData) -> Option> { let text_map = data.meta.text_map.as_ref()?; // Retrieve the text_map reference @@ -165,3 +102,25 @@ pub fn convert_nested_block_children_to_html(block: Arc) -> String pub fn convert_insert_delta_from_json(delta_value: &Value) -> Option> { serde_json::from_value::>(delta_value.to_owned()).ok() } + +pub fn required_not_empty_str(s: &str) -> Result<(), ValidationError> { + if s.is_empty() { + return Err(ValidationError::new("should not be empty string")); + } + Ok(()) +} + +pub fn serialize_color_attribute( + attrs: &HashMap, + attr_name: &str, + css_property: &str, +) -> String { + if let Some(color) = attrs.get(attr_name) { + return format!( + "{}: {};", + css_property, + color.to_string().replace("0x", "#").trim_matches('\"') + ); + } + "".to_string() +} diff --git a/frontend/rust-lib/flowy-document2/tests/assets/html/bulleted_list.html b/frontend/rust-lib/flowy-document2/tests/assets/html/bulleted_list.html index ae621dac0bba..bad75cfbb819 100644 --- a/frontend/rust-lib/flowy-document2/tests/assets/html/bulleted_list.html +++ b/frontend/rust-lib/flowy-document2/tests/assets/html/bulleted_list.html @@ -1 +1 @@ -
    • Highlight
    • You can also

      • nest
    \ No newline at end of file +
    • Highlight

      You can also

      • nest
    \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/html/callout.html b/frontend/rust-lib/flowy-document2/tests/assets/html/callout.html index 14e7c5d4e7d1..09c25736c709 100644 --- a/frontend/rust-lib/flowy-document2/tests/assets/html/callout.html +++ b/frontend/rust-lib/flowy-document2/tests/assets/html/callout.html @@ -1,6 +1,6 @@ -

    🥰 +

    \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/html/google_docs.html b/frontend/rust-lib/flowy-document2/tests/assets/html/google_docs.html new file mode 100644 index 000000000000..0de659e9ba01 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/html/google_docs.html @@ -0,0 +1 @@ +

    The Notion Document

    Heading-1

    Heading - 2

    Heading - 3

    Heading - 4

    This is a paragraph

    paragraph’s child

    • This is a bulleted list - 1

      • This is a bulleted list - 1 - 1

    • This is a bulleted list - 2

    This is a paragraph

    • unticked

      This is a todo - 1

      • unticked

        This is a todo - 1-1

    This is a paragraph

    1. This is a numbered list -1

    2. This is a numbered list -2

      1. This is a numbered list-1-1

    This is a paragraph

    This is a paragraph


    This is a paragraph font-color bg-color bold italic underline strike-through inline-code inline-formula link
    \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/html/notion.html b/frontend/rust-lib/flowy-document2/tests/assets/html/notion.html new file mode 100644 index 000000000000..ebd0b8eb3f26 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/html/notion.html @@ -0,0 +1,34 @@ +

    The Notion Document

    +

    Heading-1

    +

    Heading - 2

    +

    Heading - 3

    +

    This is a paragraph

    +

    paragraph’s child

    +
    • This is a bulleted list - 1
      • This is a bulleted list - 1 - 1
    • This is a bulleted list - 2
    +

    This is a paragraph

    +
    • [ ] This is a todo - 1
      • [ ] This is a paragraph - 1-1
    +
    1. This is a numbered list -1
    +

    This is a paragraph

    +
    • This is a toggle list

      This is a toggle child

    • +
    +

    This is a quote

    This is a quote child

    +

    This is a paragraph

    +
    +
    // This is the main function.
    +fn main() {
    +    // Print text to the console.
    +    **println**!("Hello World!");
    +}
    +

    This is a paragraph

    +

    <aside> + 💡 callout

    +

    </aside>

    +

    This is a paragraph font-color bg-color bold italic underline strike-through inline-code $inline-formula$ link

    +

    $$ + |x| = \begin{cases} + x, &\quad x \geq 0 \\ + -x, &\quad x < 0 + \end{cases} + $$

    +

    End

    + \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/html/numbered_list.html b/frontend/rust-lib/flowy-document2/tests/assets/html/numbered_list.html index 7bcc0ec06b98..d4e8134c029e 100644 --- a/frontend/rust-lib/flowy-document2/tests/assets/html/numbered_list.html +++ b/frontend/rust-lib/flowy-document2/tests/assets/html/numbered_list.html @@ -1 +1 @@ -
    1. Highlight
    2. You can also

      1. nest
    \ No newline at end of file +
    1. Highlight

      You can also

      1. nest
    \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/html/todo_list.html b/frontend/rust-lib/flowy-document2/tests/assets/html/todo_list.html index 19f48f241001..46dcabd198e6 100644 --- a/frontend/rust-lib/flowy-document2/tests/assets/html/todo_list.html +++ b/frontend/rust-lib/flowy-document2/tests/assets/html/todo_list.html @@ -1 +1 @@ -
    • [x] Highlight
    • You can also

      • [ ] nest
    \ No newline at end of file +
    • Highlight

      You can also

      • nest
    \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/html/toggle_list.html b/frontend/rust-lib/flowy-document2/tests/assets/html/toggle_list.html index a8e93bdf74ea..11df3f80b0a2 100644 --- a/frontend/rust-lib/flowy-document2/tests/assets/html/toggle_list.html +++ b/frontend/rust-lib/flowy-document2/tests/assets/html/toggle_list.html @@ -1 +1 @@ -
    • Click ? at the bottom right for help and support.
    • This is a paragraph

      • This is a toggle list
    \ No newline at end of file +
    Click ? at the bottom right for help and support.

    This is a paragraph

    This is a toggle list
    \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/json/google_docs.json b/frontend/rust-lib/flowy-document2/tests/assets/json/google_docs.json new file mode 100644 index 000000000000..27aa86f46211 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/json/google_docs.json @@ -0,0 +1,351 @@ +{ + "children": [ + { + "children": [], + "data": { + "delta": [ + { + "attributes": { + "bg_color": "transparent", + "font_color": "#000000" + }, + "insert": "The Notion Document" + } + ], + "level": 1, + "text_direction": "ltr" + }, + "type": "heading" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": { + "bg_color": "transparent", + "font_color": "#000000" + }, + "insert": "Heading-1" + } + ], + "level": 1, + "text_direction": "ltr" + }, + "type": "heading" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": { + "bg_color": "transparent", + "font_color": "#000000" + }, + "insert": "Heading - 2" + } + ], + "level": 2, + "text_direction": "ltr" + }, + "type": "heading" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": { + "bg_color": "transparent", + "font_color": "#000000" + }, + "insert": "Heading - 3" + } + ], + "level": 3, + "text_direction": "ltr" + }, + "type": "heading" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": { + "bg_color": "transparent", + "font_color": "#000000" + }, + "insert": "Heading - 4" + } + ], + "level": 3, + "text_direction": "ltr" + }, + "type": "heading" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": { + "bg_color": "transparent", + "font_color": "#000000" + }, + "insert": "This is a paragraph" + } + ], + "text_direction": "ltr" + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": { + "bg_color": "transparent", + "font_color": "#000000" + }, + "insert": "paragraph’s child" + } + ], + "text_direction": "ltr" + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": { + "bg_color": "transparent", + "font_color": "#000000" + }, + "insert": "This is a bulleted list - 1" + } + ] + }, + "type": "bulleted_list" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": { + "bg_color": "transparent", + "font_color": "#000000" + }, + "insert": "This is a bulleted list - 1 - 1" + } + ] + }, + "type": "bulleted_list" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": { + "bg_color": "transparent", + "font_color": "#000000" + }, + "insert": "This is a bulleted list - 2" + } + ] + }, + "type": "bulleted_list" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": { + "bg_color": "transparent", + "font_color": "#000000" + }, + "insert": "This is a paragraph" + } + ], + "text_direction": "ltr" + }, + "type": "paragraph" + }, + { + "children": [ + { + "children": [], + "data": { + "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAA1ElEQVR4Ae3bMQ4BURSFYY2xBuwQ7BIkTGxFRj9Oo9RdkXn5TvL3L19u+2ZmZmZmZhVbpH26pFcaJ9IrndMudb/CWadHGiden1bll9MIzqd79SUd0thY20qga4NA50qgoUGgoRJo/NL/V/N+QIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEyFeEZyXQpUGgUyXQrkGgTSVQl/qGcG5pnkq3Sn0jOMv0k3Vpm05pmNjfsGPalFyOmZmZmdkbSS9cKbtzhxMAAAAASUVORK5CYII=" + }, + "type": "image" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": { + "bg_color": "transparent", + "font_color": "#000000" + }, + "insert": "This is a todo - 1" + } + ], + "text_direction": "ltr" + }, + "type": "paragraph" + } + ], + "data": { + "checked": false, + "text_direction": "ltr" + }, + "type": "todo_list" + }, + { + "children": [ + { + "children": [], + "data": { + "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAA1ElEQVR4Ae3bMQ4BURSFYY2xBuwQ7BIkTGxFRj9Oo9RdkXn5TvL3L19u+2ZmZmZmZhVbpH26pFcaJ9IrndMudb/CWadHGiden1bll9MIzqd79SUd0thY20qga4NA50qgoUGgoRJo/NL/V/N+QIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIEyFeEZyXQpUGgUyXQrkGgTSVQl/qGcG5pnkq3Sn0jOMv0k3Vpm05pmNjfsGPalFyOmZmZmdkbSS9cKbtzhxMAAAAASUVORK5CYII=" + }, + "type": "image" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": { + "bg_color": "transparent", + "font_color": "#000000" + }, + "insert": "This is a todo - 1-1" + } + ], + "text_direction": "ltr" + }, + "type": "paragraph" + } + ], + "data": { + "checked": false, + "text_direction": "ltr" + }, + "type": "todo_list" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": { + "bg_color": "transparent", + "font_color": "#000000" + }, + "insert": "This is a paragraph" + } + ], + "text_direction": "ltr" + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": { + "bg_color": "transparent", + "font_color": "#000000" + }, + "insert": "This is a numbered list -1" + } + ] + }, + "type": "numbered_list" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": { + "bg_color": "transparent", + "font_color": "#000000" + }, + "insert": "This is a numbered list -2" + } + ] + }, + "type": "numbered_list" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": { + "bg_color": "transparent", + "font_color": "#000000" + }, + "insert": "This is a numbered list-1-1" + } + ] + }, + "type": "numbered_list" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": { + "bg_color": "transparent", + "font_color": "#000000" + }, + "insert": "This is a paragraph" + } + ], + "text_direction": "ltr" + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": { + "bg_color": "transparent", + "font_color": "#000000" + }, + "insert": "This is a paragraph" + } + ], + "text_direction": "ltr" + }, + "type": "paragraph" + }, + { + "children": [], + "data": {}, + "type": "divider" + }, + { + "children": [], + "data": {}, + "type": "paragraph" + } + ], + "data": {}, + "type": "page" +} \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/json/notion.json b/frontend/rust-lib/flowy-document2/tests/assets/json/notion.json new file mode 100644 index 000000000000..0e5f83fd1349 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/json/notion.json @@ -0,0 +1,371 @@ +{ + "type": "page", + "data": {}, + "children": [ + { + "type": "heading", + "data": { + "delta": [ + { + "attributes": null, + "insert": "The Notion Document" + } + ], + "level": 1 + }, + "children": [] + }, + { + "type": "heading", + "data": { + "level": 1, + "delta": [ + { + "attributes": null, + "insert": "Heading-1" + } + ] + }, + "children": [] + }, + { + "type": "heading", + "data": { + "level": 2, + "delta": [ + { + "attributes": null, + "insert": "Heading - 2" + } + ] + }, + "children": [] + }, + { + "type": "heading", + "data": { + "level": 3, + "delta": [ + { + "attributes": null, + "insert": "Heading - 3" + } + ] + }, + "children": [] + }, + { + "type": "paragraph", + "data": { + "delta": [ + { + "attributes": null, + "insert": "This is a paragraph" + } + ] + }, + "children": [] + }, + { + "type": "paragraph", + "data": { + "delta": [ + { + "attributes": null, + "insert": "paragraph’s child" + } + ] + }, + "children": [] + }, + { + "type": "bulleted_list", + "data": { + "delta": [ + { + "attributes": null, + "insert": "This is a bulleted list - 1" + } + ] + }, + "children": [ + { + "type": "bulleted_list", + "data": { + "delta": [ + { + "attributes": null, + "insert": "This is a bulleted list - 1 - 1" + } + ] + }, + "children": [] + } + ] + }, + { + "type": "bulleted_list", + "data": { + "delta": [ + { + "attributes": null, + "insert": "This is a bulleted list - 2" + } + ] + }, + "children": [] + }, + { + "type": "paragraph", + "data": { + "delta": [ + { + "attributes": null, + "insert": "This is a paragraph" + } + ] + }, + "children": [] + }, + { + "type": "bulleted_list", + "data": { + "delta": [ + { + "attributes": null, + "insert": "[ ] This is a todo - 1" + } + ] + }, + "children": [ + { + "type": "bulleted_list", + "data": { + "delta": [ + { + "attributes": null, + "insert": "[ ] This is a paragraph - 1-1" + } + ] + }, + "children": [] + } + ] + }, + { + "type": "numbered_list", + "data": { + "delta": [ + { + "attributes": null, + "insert": "This is a numbered list -1" + } + ] + }, + "children": [] + }, + { + "type": "paragraph", + "data": { + "delta": [ + { + "attributes": null, + "insert": "This is a paragraph" + } + ] + }, + "children": [] + }, + { + "type": "bulleted_list", + "data": { + "delta": [ + { + "attributes": null, + "insert": "This is a toggle list" + } + ] + }, + "children": [ + { + "type": "paragraph", + "data": { + "delta": [ + { + "attributes": null, + "insert": "This is a toggle child" + } + ] + }, + "children": [] + } + ] + }, + { + "type": "quote", + "data": { + "delta": [ + { + "attributes": null, + "insert": "This is a quote" + } + ] + }, + "children": [ + { + "type": "paragraph", + "data": { + "delta": [ + { + "attributes": null, + "insert": "This is a quote child" + } + ] + }, + "children": [] + } + ] + }, + { + "type": "paragraph", + "data": { + "delta": [ + { + "attributes": null, + "insert": "This is a paragraph" + } + ] + }, + "children": [] + }, + { + "type": "divider", + "data": {}, + "children": [] + }, + { + "type": "code", + "data": { + "delta": [ + { + "attributes": null, + "insert": "// This is the main function.\nfn main() {\n // Print text to the console.\n **println**!(\"Hello World!\");\n}" + } + ], + "language": "jsx" + }, + "children": [] + }, + { + "type": "paragraph", + "data": { + "delta": [ + { + "attributes": null, + "insert": "This is a paragraph" + } + ] + }, + "children": [] + }, + { + "type": "paragraph", + "data": { + "delta": [ + { + "attributes": null, + "insert": "" + } + ] + }, + "children": [] + }, + { + "type": "paragraph", + "data": { + "delta": [ + { + "attributes": null, + "insert": "This is a paragraph font-color bg-color " + }, + { + "attributes": { + "bold": true + }, + "insert": "bold" + }, + { + "attributes": { + "italic": true + }, + "insert": "italic underline " + }, + { + "attributes": { + "italic": true, + "strikethrough": true + }, + "insert": "strike-through" + }, + { + "attributes": { + "code": true, + "italic": true + }, + "insert": "inline-code" + }, + { + "attributes": { + "italic": true + }, + "insert": " $inline-formula$ " + }, + { + "attributes": { + "href": "https://www.notion.so/The-Notion-Document-d4236da306b84f6199e4091705042d78?pvs=21", + "italic": true + }, + "insert": "link" + } + ] + }, + "children": [] + }, + { + "type": "paragraph", + "data": { + "delta": [ + { + "attributes": null, + "insert": "$$\n |x| = \\begin{cases}\n x, &\\quad x \\geq 0 \\\\\n -x, &\\quad x < 0\n \\end{cases}\n $$" + } + ] + }, + "children": [] + }, + { + "type": "paragraph", + "data": { + "delta": [ + { + "attributes": null, + "insert": "End" + } + ] + }, + "children": [] + } + ] +} \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/json/plain_text.json b/frontend/rust-lib/flowy-document2/tests/assets/json/plain_text.json new file mode 100644 index 000000000000..33d86667e05b --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/json/plain_text.json @@ -0,0 +1,510 @@ +{ + "children": [ + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": "# The Notion Document" + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": "# Heading-1" + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": "## Heading - 2" + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": "### Heading - 3" + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": "This is a paragraph" + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": "paragraph’s child" + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": "- This is a bulleted list - 1" + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": " - This is a bulleted list - 1 - 1" + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": "- This is a bulleted list - 2" + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": "This is a paragraph" + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": "- [ ] This is a todo - 1" + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": " - [ ] This is a paragraph - 1-1" + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": "1. This is a numbered list -1" + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": "This is a paragraph" + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": "- This is a toggle list" + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": " This is a toggle child" + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": "> This is a quote" + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": ">" + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": ">" + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": "> This is a quote child" + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": ">" + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": "This is a paragraph" + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": "---" + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": "```jsx" + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": "// This is the main function." + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": "fn main() {" + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": " // Print text to the console." + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": " **println**!(\"Hello World!\");" + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": "}" + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": "```" + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": "This is a paragraph" + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": "" + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": "This is a paragraph font-color bg-color **bold** *italic underline ~~strike-through~~ `inline-code` $inline-formula$ [link](https://www.notion.so/The-Notion-Document-d4236da306b84f6199e4091705042d78?pvs=21)*" + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": "$$" + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": "|x| = \\begin{cases}             " + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": "  x, &\\quad x \\geq 0 \\\\           " + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": " -x, &\\quad x < 0             " + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": "\\end{cases}" + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": "$$" + } + ] + }, + "type": "paragraph" + }, + { + "children": [], + "data": { + "delta": [ + { + "attributes": null, + "insert": "End" + } + ] + }, + "type": "paragraph" + } + ], + "data": {}, + "type": "page" +} \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/text/plain_text.txt b/frontend/rust-lib/flowy-document2/tests/assets/text/plain_text.txt new file mode 100644 index 000000000000..71c07e6b788d --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/text/plain_text.txt @@ -0,0 +1,64 @@ +# The Notion Document + +# Heading-1 + +## Heading - 2 + +### Heading - 3 + +This is a paragraph + +paragraph’s child + +- This is a bulleted list - 1 + - This is a bulleted list - 1 - 1 +- This is a bulleted list - 2 + +This is a paragraph + +- [ ] This is a todo - 1 + - [ ] This is a paragraph - 1-1 +1. This is a numbered list -1 + +This is a paragraph + +- This is a toggle list + + This is a toggle child + + +> This is a quote +> +> +> This is a quote child +> + +This is a paragraph + +--- + +```jsx +// This is the main function. +fn main() { + // Print text to the console. + **println**!("Hello World!"); +} +``` + +This is a paragraph + + + +This is a paragraph font-color bg-color **bold** *italic underline ~~strike-through~~ `inline-code` $inline-formula$ [link](https://www.notion.so/The-Notion-Document-d4236da306b84f6199e4091705042d78?pvs=21)* + +$$ +|x| = \begin{cases}              +  x, &\quad x \geq 0 \\            + -x, &\quad x < 0              +\end{cases} +$$ + +End \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/parser/html/mod.rs b/frontend/rust-lib/flowy-document2/tests/parser/html/mod.rs new file mode 100644 index 000000000000..945eb97109c6 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/parser/html/mod.rs @@ -0,0 +1 @@ +mod parser_test; diff --git a/frontend/rust-lib/flowy-document2/tests/parser/html/parser_test.rs b/frontend/rust-lib/flowy-document2/tests/parser/html/parser_test.rs new file mode 100644 index 000000000000..c70c38bea1cf --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/parser/html/parser_test.rs @@ -0,0 +1,45 @@ +use flowy_document2::parser::external::parser::ExternalDataToNestedJSONParser; +use flowy_document2::parser::parser_entities::{InputType, NestedBlock}; + +macro_rules! generate_test_cases { + ($($ty:ident),*) => { + [ + $( + ( + include_str!(concat!("../../assets/json/", stringify!($ty), ".json")), + include_str!(concat!("../../assets/html/", stringify!($ty), ".html")), + ) + ),* + ] + }; +} + +/// test convert data to json +/// - input html:

    Hello

    World!

    +#[tokio::test] +async fn html_to_document_test() { + let test_cases = generate_test_cases!(notion, google_docs); + + for (json, html) in test_cases.iter() { + let parser = ExternalDataToNestedJSONParser::new(html.to_string(), InputType::Html); + let block = parser.to_nested_block(); + assert!(block.is_some()); + let block = block.unwrap(); + let expect_block = serde_json::from_str::(json).unwrap(); + assert_eq!(block, expect_block); + } +} + +/// test convert data to json +/// - input plain text: Hello World! +#[tokio::test] +async fn plain_text_to_document_test() { + let plain_text = include_str!("../../assets/text/plain_text.txt"); + let parser = ExternalDataToNestedJSONParser::new(plain_text.to_string(), InputType::PlainText); + let block = parser.to_nested_block(); + assert!(block.is_some()); + let block = block.unwrap(); + let expect_json = include_str!("../../assets/json/plain_text.json"); + let expect_block = serde_json::from_str::(expect_json).unwrap(); + assert_eq!(block, expect_block); +} diff --git a/frontend/rust-lib/flowy-document2/tests/parser/mod.rs b/frontend/rust-lib/flowy-document2/tests/parser/mod.rs index 18ec9c997681..71758c3fe477 100644 --- a/frontend/rust-lib/flowy-document2/tests/parser/mod.rs +++ b/frontend/rust-lib/flowy-document2/tests/parser/mod.rs @@ -1,3 +1,4 @@ mod document_data_parser_test; -mod html_text; +mod html; mod json; +mod parse_to_html_text; diff --git a/frontend/rust-lib/flowy-document2/tests/parser/html_text/mod.rs b/frontend/rust-lib/flowy-document2/tests/parser/parse_to_html_text/mod.rs similarity index 100% rename from frontend/rust-lib/flowy-document2/tests/parser/html_text/mod.rs rename to frontend/rust-lib/flowy-document2/tests/parser/parse_to_html_text/mod.rs diff --git a/frontend/rust-lib/flowy-document2/tests/parser/html_text/test.rs b/frontend/rust-lib/flowy-document2/tests/parser/parse_to_html_text/test.rs similarity index 90% rename from frontend/rust-lib/flowy-document2/tests/parser/html_text/test.rs rename to frontend/rust-lib/flowy-document2/tests/parser/parse_to_html_text/test.rs index 9935443a14c0..894d27e0450f 100644 --- a/frontend/rust-lib/flowy-document2/tests/parser/html_text/test.rs +++ b/frontend/rust-lib/flowy-document2/tests/parser/parse_to_html_text/test.rs @@ -1,4 +1,4 @@ -use crate::parser::html_text::utils::{assert_document_html_eq, assert_document_text_eq}; +use crate::parser::parse_to_html_text::utils::{assert_document_html_eq, assert_document_text_eq}; macro_rules! generate_test_cases { ($($block_ty:ident),*) => { diff --git a/frontend/rust-lib/flowy-document2/tests/parser/html_text/utils.rs b/frontend/rust-lib/flowy-document2/tests/parser/parse_to_html_text/utils.rs similarity index 100% rename from frontend/rust-lib/flowy-document2/tests/parser/html_text/utils.rs rename to frontend/rust-lib/flowy-document2/tests/parser/parse_to_html_text/utils.rs From a93d325e6a08bc29ae2cedb0884c1b0d994801bc Mon Sep 17 00:00:00 2001 From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Date: Thu, 2 Nov 2023 17:17:30 +0100 Subject: [PATCH 18/56] fix: notification actions lemonade darkmode (#3862) --- .../presentation/notifications/widgets/notification_item.dart | 3 +++ .../packages/flowy_infra_ui/lib/style_widget/icon_button.dart | 1 + 2 files changed, 4 insertions(+) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_item.dart index 3562b614d379..87ac72ed9f1b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_item.dart @@ -244,6 +244,7 @@ class NotificationItemActions extends StatelessWidget { tooltipText: LocaleKeys.reminderNotification_tooltipMarkUnread.tr(), icon: const FlowySvg(FlowySvgs.restore_s), + iconColorOnHover: Theme.of(context).colorScheme.onSurface, onPressed: () => onReadChanged?.call(false), ), ] else ...[ @@ -251,6 +252,7 @@ class NotificationItemActions extends StatelessWidget { height: 28, tooltipText: LocaleKeys.reminderNotification_tooltipMarkRead.tr(), + iconColorOnHover: Theme.of(context).colorScheme.onSurface, icon: const FlowySvg(FlowySvgs.messages_s), onPressed: () => onReadChanged?.call(true), ), @@ -266,6 +268,7 @@ class NotificationItemActions extends StatelessWidget { height: 28, tooltipText: LocaleKeys.reminderNotification_tooltipDelete.tr(), icon: const FlowySvg(FlowySvgs.delete_s), + iconColorOnHover: Theme.of(context).colorScheme.onSurface, onPressed: onDelete, ), ], diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/icon_button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/icon_button.dart index 2f8ce2aaf5f3..fd65bb94363e 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/icon_button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/icon_button.dart @@ -85,6 +85,7 @@ class FlowyIconButton extends StatelessWidget { iconColorOnHover ?? Theme.of(context).iconTheme.color, //Do not set background here. Use [fillColor] instead. ), + resetHoverOnRebuild: false, child: Padding( padding: iconPadding, child: Center( From 73ea1a0685b033427494403c21268cf8dff3549c Mon Sep 17 00:00:00 2001 From: Yijing Huang Date: Thu, 2 Nov 2023 11:15:15 -0700 Subject: [PATCH 19/56] feat:add toast in trash and confirm dialog + fix issues from launch review (#3787) * chore: improve trash button * feat: improve restore all&delete all * refactor: add showFlowyMobileConfirmDialog * feat: add toast in delete/restore single file * refactor: refactor to TrashActionType enum * fix: text invisible in signin page in dark mode * feat: add FlowyMobileErrorStateContainer to display error state * refactor: add FlowyMobileStateContainer to handle empty or error state - Replace MobileErrorPage by FlowyMobileStateContainer.error - Implement app version in reporting issue on github - Implement FlowyMobileStateContainer in trash,setting,favorite and mobile view page * refactor: unify bottom sheet style - Unify bottom sheet style in add new page, page action, and trash action - Add icon color in BottomSheetActionWidget for future use - Add theme color in MobileBottomSheetDragHandle * chore: unify Appbar style * chore: remove the more button when trash list is empty * fix: show bottom sheet error * fix: fix merge and ui issue * refactor: refactor ViewPageBottomSheet and origanize code * chore: add icon color for favorite button * fix: add missing icon color and delete comments --------- Co-authored-by: Lucas.Xu --- .../mobile/application/mobile_theme_data.dart | 5 +- .../presentation/base/mobile_view_page.dart | 17 +- .../bottom_sheet/bottom_sheet.dart | 155 +-------- .../bottom_sheet_action_widget.dart | 46 +-- .../bottom_sheet_add_new_page.dart | 47 +-- .../bottom_sheet_drag_handler.dart | 2 +- .../bottom_sheet_view_item_body.dart | 17 +- .../bottom_sheet_view_item_header.dart | 25 +- .../bottom_sheet/bottom_sheet_view_page.dart | 51 +-- .../default_mobile_action_pane.dart | 2 +- ...tem.dart => show_mobile_bottom_sheet.dart} | 52 +-- .../mobile/presentation/error/error_page.dart | 49 --- .../favorite/mobile_favorite_folder.dart | 34 +- .../favorite/mobile_favorite_page.dart | 9 +- .../presentation/home/mobile_home_page.dart | 10 +- .../home/mobile_home_page_recent_files.dart | 19 +- .../home/mobile_home_setting_page.dart | 50 ++- .../home/mobile_home_trash_page.dart | 309 ++++++++++-------- .../page_item/mobile_view_item.dart | 34 +- .../personal_info_setting_group.dart | 6 +- .../setting/support_setting_group.dart | 50 +-- .../widgets/flowy_mobile_state_container.dart | 102 ++++++ .../show_flowy_mobile_confirm_dialog.dart | 62 ++++ .../mobile/presentation/widgets/widgets.dart | 3 + frontend/resources/translations/en.json | 8 +- 25 files changed, 592 insertions(+), 572 deletions(-) rename frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/{bottom_sheet_view_item.dart => show_mobile_bottom_sheet.dart} (84%) delete mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/error/error_page.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_state_container.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/widgets/widgets.dart diff --git a/frontend/appflowy_flutter/lib/mobile/application/mobile_theme_data.dart b/frontend/appflowy_flutter/lib/mobile/application/mobile_theme_data.dart index 55e827f10316..0b4557f9feae 100644 --- a/frontend/appflowy_flutter/lib/mobile/application/mobile_theme_data.dart +++ b/frontend/appflowy_flutter/lib/mobile/application/mobile_theme_data.dart @@ -64,7 +64,6 @@ ThemeData getMobileThemeData( appBarTheme: AppBarTheme( foregroundColor: mobileColorTheme.onBackground, backgroundColor: mobileColorTheme.background, - elevation: 80, centerTitle: false, titleTextStyle: TextStyle( color: mobileColorTheme.onBackground, @@ -116,7 +115,7 @@ ThemeData getMobileThemeData( foregroundColor: MaterialStateProperty.all( mobileColorTheme.onBackground, ), - backgroundColor: MaterialStateProperty.all(Colors.white), + backgroundColor: MaterialStateProperty.all(mobileColorTheme.background), shape: MaterialStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular(6), @@ -129,7 +128,7 @@ ThemeData getMobileThemeData( ), ), padding: MaterialStateProperty.all( - const EdgeInsets.symmetric(horizontal: 16), + const EdgeInsets.symmetric(horizontal: 8, vertical: 12), ), // splash color overlayColor: MaterialStateProperty.all( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart index ec85278ea3b8..bf798a11056a 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart @@ -1,8 +1,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart'; import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart'; -import 'package:appflowy/mobile/presentation/error/error_page.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; @@ -56,8 +55,11 @@ class _MobileViewPageState extends State { child: CircularProgressIndicator(), ); } else if (!state.hasData) { - body = MobileErrorPage( - message: LocaleKeys.error_loadingViewError.tr(), + body = FlowyMobileStateContainer.error( + emoji: '😔', + title: LocaleKeys.error_weAreSorry.tr(), + description: LocaleKeys.error_loadingViewError.tr(), + errorMsg: state.error.toString(), ); } else { body = state.data!.fold((view) { @@ -65,8 +67,11 @@ class _MobileViewPageState extends State { actions.add(_buildAppBarMoreButton(view)); return view.plugin().widgetBuilder.buildWidget(shrinkWrap: false); }, (error) { - return MobileErrorPage( - message: error.toString(), + return FlowyMobileStateContainer.error( + emoji: '😔', + title: LocaleKeys.error_weAreSorry.tr(), + description: LocaleKeys.error_loadingViewError.tr(), + errorMsg: error.toString(), ); }); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet.dart index c1f43af889a0..a128f5434778 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet.dart @@ -1,146 +1,9 @@ -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_drag_handler.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_rename_widget.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_view_item_header.dart'; -import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; -import 'package:appflowy/workspace/application/view/view_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; - -Future showMobileBottomSheet({ - required BuildContext context, - required WidgetBuilder builder, -}) async { - showModalBottomSheet( - context: context, - isScrollControlled: true, - enableDrag: true, - useSafeArea: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(8.0), - topRight: Radius.circular(8.0), - ), - ), - builder: builder, - ); -} - -enum MobileBottomSheetType { - view, - rename, -} - -class MobileViewItemBottomSheet extends StatefulWidget { - const MobileViewItemBottomSheet({ - super.key, - required this.view, - this.defaultType = MobileBottomSheetType.view, - }); - - final ViewPB view; - final MobileBottomSheetType defaultType; - - @override - State createState() => - _MobileViewItemBottomSheetState(); -} - -class _MobileViewItemBottomSheetState extends State { - MobileBottomSheetType type = MobileBottomSheetType.view; - - @override - initState() { - super.initState(); - - type = widget.defaultType; - } - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - // drag handler - const MobileBottomSheetDragHandler(), - - // header - _buildHeader(), - const VSpace(8.0), - const Divider(), - - // body - _buildBody(), - const VSpace(12.0), - ], - ); - } - - Widget _buildHeader() { - switch (type) { - case MobileBottomSheetType.view: - case MobileBottomSheetType.rename: - // header - return MobileViewItemBottomSheetHeader( - showBackButton: type != MobileBottomSheetType.view, - view: widget.view, - onBack: () { - setState(() { - type = MobileBottomSheetType.view; - }); - }, - ); - } - } - - Widget _buildBody() { - switch (type) { - case MobileBottomSheetType.view: - return MobileViewItemBottomSheetBody( - isFavorite: widget.view.isFavorite, - onAction: (action) { - switch (action) { - case MobileViewItemBottomSheetBodyAction.rename: - setState(() { - type = MobileBottomSheetType.rename; - }); - break; - case MobileViewItemBottomSheetBodyAction.duplicate: - context.pop(); - context.read().add(const ViewEvent.duplicate()); - break; - case MobileViewItemBottomSheetBodyAction.share: - // unimplemented - context.pop(); - break; - case MobileViewItemBottomSheetBodyAction.delete: - context.pop(); - context.read().add(const ViewEvent.delete()); - - break; - case MobileViewItemBottomSheetBodyAction.addToFavorites: - case MobileViewItemBottomSheetBodyAction.removeFromFavorites: - context.pop(); - context - .read() - .add(FavoriteEvent.toggle(widget.view)); - break; - } - }, - ); - case MobileBottomSheetType.rename: - return MobileBottomSheetRenameWidget( - name: widget.view.name, - onRename: (name) { - if (name != widget.view.name) { - context.read().add(ViewEvent.rename(name)); - } - context.pop(); - }, - ); - } - } -} +export 'bottom_sheet_action_widget.dart'; +export 'bottom_sheet_add_new_page.dart'; +export 'bottom_sheet_drag_handler.dart'; +export 'bottom_sheet_rename_widget.dart'; +export 'bottom_sheet_view_item_body.dart'; +export 'bottom_sheet_view_item_header.dart'; +export 'bottom_sheet_view_page.dart'; +export 'default_mobile_action_pane.dart'; +export 'show_mobile_bottom_sheet.dart'; diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart index 34c243d92ef0..b4aa9deee101 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart @@ -1,8 +1,5 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/mobile/presentation/base/box_container.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; class BottomSheetActionWidget extends StatelessWidget { const BottomSheetActionWidget({ @@ -10,42 +7,31 @@ class BottomSheetActionWidget extends StatelessWidget { required this.svg, required this.text, required this.onTap, + this.iconColor, }); final FlowySvgData svg; final String text; final VoidCallback onTap; + final Color? iconColor; @override Widget build(BuildContext context) { - return FlowyBoxContainer( - child: InkWell( - onTap: () { - HapticFeedback.mediumImpact(); - onTap(); - }, - enableFeedback: true, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 10.0, - horizontal: 12.0, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FlowySvg( - svg, - size: const Size.square(24.0), - blendMode: BlendMode.dst, - ), - const HSpace(6.0), - FlowyText(text), - const Spacer(), - ], - ), - ), + final iconColor = + this.iconColor ?? Theme.of(context).colorScheme.onBackground; + + return OutlinedButton.icon( + icon: FlowySvg( + svg, + size: const Size.square(22.0), + color: iconColor, ), + label: Text(text), + style: Theme.of(context) + .outlinedButtonTheme + .style + ?.copyWith(alignment: Alignment.centerLeft), + onPressed: onTap, ); } } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_add_new_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_add_new_page.dart index 4fec21177ed0..a89cf44f7b08 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_add_new_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_add_new_page.dart @@ -1,8 +1,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_drag_handler.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_view_item_header.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -21,46 +19,9 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget { @override Widget build(BuildContext context) { return Column( - mainAxisSize: MainAxisSize.min, children: [ - // drag handler - const MobileBottomSheetDragHandler(), - - // header - MobileViewItemBottomSheetHeader( - showBackButton: false, - view: view, - onBack: () {}, - ), - const VSpace(8.0), - const Divider(), - - // body - _AddNewPageBody( - onAction: onAction, - ), - const VSpace(24.0), - ], - ); - } -} - -class _AddNewPageBody extends StatelessWidget { - const _AddNewPageBody({ - required this.onAction, - }); - - final void Function(ViewLayoutPB layout) onAction; - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - // rename, duplicate + // new document, new grid Row( - mainAxisSize: MainAxisSize.min, children: [ Expanded( child: BottomSheetActionWidget( @@ -69,6 +30,7 @@ class _AddNewPageBody extends StatelessWidget { onTap: () => onAction(ViewLayoutPB.Document), ), ), + const HSpace(8), Expanded( child: BottomSheetActionWidget( svg: FlowySvgs.grid_s, @@ -78,10 +40,10 @@ class _AddNewPageBody extends StatelessWidget { ), ], ), + const VSpace(8), - // share, delete + // new board, new calendar Row( - mainAxisSize: MainAxisSize.min, children: [ Expanded( child: BottomSheetActionWidget( @@ -90,6 +52,7 @@ class _AddNewPageBody extends StatelessWidget { onTap: () => onAction(ViewLayoutPB.Board), ), ), + const HSpace(8), Expanded( child: BottomSheetActionWidget( svg: FlowySvgs.date_s, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_drag_handler.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_drag_handler.dart index 9a8d6d2253a5..4e9fcd3d7e3e 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_drag_handler.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_drag_handler.dart @@ -12,7 +12,7 @@ class MobileBottomSheetDragHandler extends StatelessWidget { height: 4, decoration: BoxDecoration( borderRadius: BorderRadius.circular(2.0), - color: Theme.of(context).colorScheme.onSecondary, + color: Theme.of(context).hintColor, ), ), ); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart index 6c4cf8924dcc..d4278317ba40 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart @@ -2,6 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; enum MobileViewItemBottomSheetBodyAction { @@ -26,12 +27,11 @@ class MobileViewItemBottomSheetBody extends StatelessWidget { @override Widget build(BuildContext context) { return Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // rename, duplicate Row( - mainAxisSize: MainAxisSize.min, + mainAxisSize: MainAxisSize.max, children: [ Expanded( child: BottomSheetActionWidget( @@ -42,6 +42,7 @@ class MobileViewItemBottomSheetBody extends StatelessWidget { ), ), ), + const HSpace(8), Expanded( child: BottomSheetActionWidget( svg: FlowySvgs.m_duplicate_m, @@ -53,10 +54,11 @@ class MobileViewItemBottomSheetBody extends StatelessWidget { ), ], ), + const VSpace(8), // share, delete Row( - mainAxisSize: MainAxisSize.min, + mainAxisSize: MainAxisSize.max, children: [ Expanded( child: BottomSheetActionWidget( @@ -67,6 +69,7 @@ class MobileViewItemBottomSheetBody extends StatelessWidget { ), ), ), + const HSpace(8), Expanded( child: BottomSheetActionWidget( svg: FlowySvgs.m_delete_m, @@ -78,13 +81,15 @@ class MobileViewItemBottomSheetBody extends StatelessWidget { ), ], ), + const VSpace(8), - // remove from favorites - + // remove from favorites/add to favorites BottomSheetActionWidget( svg: isFavorite ? FlowySvgs.m_favorite_selected_lg : FlowySvgs.m_favorite_unselected_lg, + //TODO(yijing): switch to theme color + iconColor: isFavorite ? Colors.yellow : null, text: isFavorite ? LocaleKeys.button_removeFromFavorites.tr() : LocaleKeys.button_addToFavorites.tr(), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_header.dart index 1f186af7ae39..9474653789c4 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_header.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_header.dart @@ -1,6 +1,6 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; class MobileViewItemBottomSheetHeader extends StatelessWidget { const MobileViewItemBottomSheetHeader({ @@ -16,8 +16,8 @@ class MobileViewItemBottomSheetHeader extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = Theme.of(context); return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ // back button, showBackButton @@ -31,14 +31,23 @@ class MobileViewItemBottomSheetHeader extends StatelessWidget { ), ), ) - : const HSpace(40.0), + : const SizedBox.shrink(), // title - FlowyText.regular( - view.name, - fontSize: 16.0, + Expanded( + child: Text( + view.name, + style: theme.textTheme.labelSmall, + ), ), - // placeholder, ensure the title is centered - const HSpace(40.0), + IconButton( + icon: Icon( + Icons.close, + color: theme.hintColor, + ), + onPressed: () { + context.pop(); + }, + ) ], ); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart index 69cbc1bc2c72..4317f3afa2c2 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart @@ -1,10 +1,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_drag_handler.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_rename_widget.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_view_item.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_view_item_header.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -47,21 +43,18 @@ class _ViewPageBottomSheetState extends State { @override Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - // drag handler - const MobileBottomSheetDragHandler(), - - // header - _buildHeader(), - const VSpace(8.0), - const Divider(), - - // body - _buildBody(), - const VSpace(24.0), - ], + return Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // header + _buildHeader(), + const VSpace(16), + // body + _buildBody(), + ], + ), ); } @@ -125,12 +118,11 @@ class MobileViewBottomSheetBody extends StatelessWidget { Widget build(BuildContext context) { final isFavorite = view.isFavorite; return Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // undo, redo Row( - mainAxisSize: MainAxisSize.min, + mainAxisSize: MainAxisSize.max, children: [ Expanded( child: BottomSheetActionWidget( @@ -141,6 +133,7 @@ class MobileViewBottomSheetBody extends StatelessWidget { ), ), ), + const HSpace(8), Expanded( child: BottomSheetActionWidget( svg: FlowySvgs.m_redo_m, @@ -152,10 +145,11 @@ class MobileViewBottomSheetBody extends StatelessWidget { ), ], ), + const VSpace(8), // rename, duplicate Row( - mainAxisSize: MainAxisSize.min, + mainAxisSize: MainAxisSize.max, children: [ Expanded( child: BottomSheetActionWidget( @@ -166,6 +160,7 @@ class MobileViewBottomSheetBody extends StatelessWidget { ), ), ), + const HSpace(8), Expanded( child: BottomSheetActionWidget( svg: FlowySvgs.m_duplicate_m, @@ -177,10 +172,11 @@ class MobileViewBottomSheetBody extends StatelessWidget { ), ], ), + const VSpace(8), // share, delete Row( - mainAxisSize: MainAxisSize.min, + mainAxisSize: MainAxisSize.max, children: [ Expanded( child: BottomSheetActionWidget( @@ -191,6 +187,7 @@ class MobileViewBottomSheetBody extends StatelessWidget { ), ), ), + const HSpace(8), Expanded( child: BottomSheetActionWidget( svg: FlowySvgs.m_delete_m, @@ -202,12 +199,15 @@ class MobileViewBottomSheetBody extends StatelessWidget { ), ], ), + const VSpace(8), // favorites BottomSheetActionWidget( svg: isFavorite ? FlowySvgs.m_favorite_selected_lg : FlowySvgs.m_favorite_unselected_lg, + //TODO(yijing): switch to theme color + iconColor: isFavorite ? Colors.yellow : null, text: isFavorite ? LocaleKeys.button_removeFromFavorites.tr() : LocaleKeys.button_addToFavorites.tr(), @@ -217,6 +217,7 @@ class MobileViewBottomSheetBody extends StatelessWidget { : MobileViewBottomSheetBodyAction.addToFavorites, ), ), + const VSpace(8), // help center BottomSheetActionWidget( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart index 34ec310da50f..fd41ca52bd07 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart @@ -1,5 +1,5 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/page_item/mobile_slide_action_button.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart similarity index 84% rename from frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item.dart rename to frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart index d6d252fe7c0f..c8116ff380e9 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart @@ -1,7 +1,4 @@ -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_drag_handler.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_rename_widget.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_view_item_header.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart'; @@ -10,6 +7,19 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; +Future showMobileBottomSheet({ + required BuildContext context, + required WidgetBuilder builder, +}) async { + showModalBottomSheet( + context: context, + isScrollControlled: true, + enableDrag: true, + useSafeArea: true, + builder: builder, + ); +} + enum MobileBottomSheetType { view, rename, @@ -42,21 +52,18 @@ class _MobileViewItemBottomSheetState extends State { @override Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - // drag handler - const MobileBottomSheetDragHandler(), - - // header - _buildHeader(), - const VSpace(8.0), - const Divider(), - - // body - _buildBody(), - const VSpace(24.0), - ], + return Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // header + _buildHeader(), + const VSpace(16), + // body + _buildBody(), + ], + ), ); } @@ -90,23 +97,24 @@ class _MobileViewItemBottomSheetState extends State { }); break; case MobileViewItemBottomSheetBodyAction.duplicate: - context.read().add(const ViewEvent.duplicate()); context.pop(); + context.read().add(const ViewEvent.duplicate()); break; case MobileViewItemBottomSheetBodyAction.share: // unimplemented context.pop(); break; case MobileViewItemBottomSheetBodyAction.delete: - context.read().add(const ViewEvent.delete()); context.pop(); + context.read().add(const ViewEvent.delete()); + break; case MobileViewItemBottomSheetBodyAction.addToFavorites: case MobileViewItemBottomSheetBodyAction.removeFromFavorites: + context.pop(); context .read() .add(FavoriteEvent.toggle(widget.view)); - context.pop(); break; } }, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/error/error_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/error/error_page.dart deleted file mode 100644 index 09ef2cf62709..000000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/error/error_page.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -class MobileErrorPage extends StatelessWidget { - const MobileErrorPage({ - super.key, - this.header, - this.title, - required this.message, - }); - - final Widget? header; - final String? title; - final String message; - - @override - Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - header != null - ? header! - : const FlowyText.semibold( - '😔', - fontSize: 50, - ), - const VSpace(14.0), - FlowyText.semibold( - title ?? LocaleKeys.error_weAreSorry.tr(), - fontSize: 32, - textAlign: TextAlign.center, - ), - const VSpace(4.0), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0), - child: FlowyText.regular( - message, - fontSize: 16, - maxLines: 100, - color: Colors.grey, // FIXME: use theme color - textAlign: TextAlign.center, - ), - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart index 0f7499b94be3..933292f38f70 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart @@ -1,7 +1,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/mobile_router.dart'; -import 'package:appflowy/mobile/presentation/error/error_page.dart'; import 'package:appflowy/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart'; +import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/menu/menu_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart'; @@ -49,27 +49,27 @@ class MobileFavoritePageFolder extends StatelessWidget { builder: (context) { final favoriteState = context.watch().state; if (favoriteState.views.isEmpty) { - return MobileErrorPage( - header: const FlowyText.semibold( - '😁', - fontSize: 50, - ), + return FlowyMobileStateContainer.info( + emoji: '😁', title: LocaleKeys.favorite_noFavorite.tr(), - message: LocaleKeys.favorite_noFavoriteHintText.tr(), + description: LocaleKeys.favorite_noFavoriteHintText.tr(), ); } return Scrollbar( child: SingleChildScrollView( - child: SlidableAutoCloseBehavior( - child: Column( - children: [ - MobileFavoriteFolder( - showHeader: false, - forceExpanded: true, - views: favoriteState.views, - ), - const VSpace(100.0), - ], + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SlidableAutoCloseBehavior( + child: Column( + children: [ + MobileFavoriteFolder( + showHeader: false, + forceExpanded: true, + views: favoriteState.views, + ), + const VSpace(100.0), + ], + ), ), ), ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart index f0b45373bc03..5541588d1008 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart @@ -82,12 +82,9 @@ class MobileFavoritePage extends StatelessWidget { // Folder Expanded( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: MobileFavoritePageFolder( - userProfile: userProfile, - workspaceSetting: workspaceSetting, - ), + child: MobileFavoritePageFolder( + userProfile: userProfile, + workspaceSetting: workspaceSetting, ), ), ], diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart index 8ed1a81f49fb..87da5a6b3c90 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart @@ -110,7 +110,10 @@ class MobileHomePage extends StatelessWidget { ), ), const SizedBox(height: 8), - const _TrashButton(), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: _TrashButton(), + ), ], ), ), @@ -142,7 +145,10 @@ class _TrashButton extends StatelessWidget { LocaleKeys.trash_text.tr(), style: Theme.of(context).textTheme.labelMedium, ), - style: const ButtonStyle(alignment: Alignment.centerLeft), + style: const ButtonStyle( + alignment: Alignment.centerLeft, + splashFactory: NoSplash.splashFactory, + ), ), ); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_recent_files.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_recent_files.dart index 9fa880553d98..010e8d30a96a 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_recent_files.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_recent_files.dart @@ -28,6 +28,7 @@ class MobileHomePageRecentFilesWidget extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = Theme.of(context); // TODO: implement the details later. return SizedBox( height: 168, @@ -54,13 +55,10 @@ class MobileHomePageRecentFilesWidget extends StatelessWidget { return Container( width: 120, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, + color: theme.colorScheme.background, borderRadius: BorderRadius.circular(8), border: Border.all( - color: Theme.of(context) - .colorScheme - .outline - .withOpacity(0.5), + color: theme.colorScheme.outline.withOpacity(0.5), ), ), child: Stack( @@ -104,14 +102,9 @@ class MobileHomePageRecentFilesWidget extends StatelessWidget { child: Text( recentFilesList[index].title, softWrap: true, - style: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith( - color: Theme.of(context) - .colorScheme - .onBackground, - ), + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onBackground, + ), maxLines: 2, ), ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_setting_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_setting_page.dart index 9f7289188547..0623b77430c3 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_setting_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_setting_page.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/presentation.dart'; - +import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -23,33 +23,47 @@ class _MobileHomeSettingPageState extends State { return FutureBuilder( future: getIt().getUser(), builder: ((context, snapshot) { + String? errorMsg; if (!snapshot.hasData) { return const Center(child: CircularProgressIndicator.adaptive()); } - final userProfile = snapshot.data?.fold((error) => null, (userProfile) { + final userProfile = snapshot.data?.fold((error) { + errorMsg = error.msg; + return null; + }, (userProfile) { return userProfile; }); + return Scaffold( appBar: AppBar( title: Text(LocaleKeys.settings_title.tr()), ), - body: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - PersonalInfoSettingGroup( - userProfile: userProfile, + body: userProfile == null + ? FlowyMobileStateContainer.error( + emoji: '🛸', + title: LocaleKeys.settings_mobile_userprofileError.tr(), + description: LocaleKeys + .settings_mobile_userprofileErrorDescription + .tr(), + errorMsg: errorMsg, + ) + : SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + PersonalInfoSettingGroup( + userProfile: userProfile, + ), + // TODO(yijing): implement this along with Notification Page + const NotificationsSettingGroup(), + const AppearanceSettingGroup(), + const SupportSettingGroup(), + const AboutSettingGroup(), + ], + ), ), - // TODO(yijing): implement this along with Notification Page - const NotificationsSettingGroup(), - const AppearanceSettingGroup(), - const SupportSettingGroup(), - const AboutSettingGroup(), - ], - ), - ), - ), + ), ); }), ); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart index 989107e2ffea..945bdfeee86d 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart @@ -1,12 +1,13 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart'; -import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart'; +import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:appflowy/plugins/trash/application/prelude.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:fluttertoast/fluttertoast.dart'; import 'package:go_router/go_router.dart'; class MobileHomeTrashPage extends StatelessWidget { @@ -18,61 +19,53 @@ class MobileHomeTrashPage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => getIt()..add(const TrashEvent.initial()), - child: Builder( - builder: (context) { + child: BlocBuilder( + builder: (context, state) { return Scaffold( appBar: AppBar( title: Text(LocaleKeys.trash_text.tr()), - elevation: 0, actions: [ - IconButton( - splashRadius: 20, - icon: const Icon(Icons.more_horiz), - onPressed: () { - showFlowyMobileBottomSheet( - context, - title: LocaleKeys.trash_mobile_actions.tr(), - builder: (_) => Row( - children: [ - Expanded( - child: BottomSheetActionWidget( - svg: FlowySvgs.m_restore_m, - text: LocaleKeys.trash_restoreAll.tr(), - onTap: () { - context - ..read() - .add(const TrashEvent.restoreAll()) - ..pop(); - }, + state.objects.isEmpty + ? const SizedBox.shrink() + : IconButton( + splashRadius: 20, + icon: const Icon(Icons.more_horiz), + onPressed: () { + final trashBloc = context.read(); + showFlowyMobileBottomSheet( + context, + title: LocaleKeys.trash_mobile_actions.tr(), + builder: (_) => Row( + children: [ + Expanded( + child: _TrashActionAllButton( + trashBloc: trashBloc, + type: _TrashActionType.deleteAll, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: _TrashActionAllButton( + trashBloc: trashBloc, + type: _TrashActionType.restoreAll, + ), + ) + ], ), - ), - Expanded( - child: BottomSheetActionWidget( - svg: FlowySvgs.m_delete_m, - text: LocaleKeys.trash_deleteAll.tr(), - onTap: () { - context - ..read() - .add(const TrashEvent.deleteAll()) - ..pop(); - }, - ), - ) - ], + ); + }, ), - ); - }, - ), ], ), - body: BlocBuilder( - builder: (_, state) { - if (state.objects.isEmpty) { - return const _TrashEmptyPage(); - } - return _DeletedFilesListView(state); - }, - ), + body: state.objects.isEmpty + ? FlowyMobileStateContainer.info( + emoji: '🗑️', + title: LocaleKeys.trash_mobile_empty.tr(), + description: LocaleKeys.trash_mobile_emptyDescription.tr(), + ) + : _DeletedFilesListView(state), ); }, ), @@ -80,104 +73,154 @@ class MobileHomeTrashPage extends StatelessWidget { } } -class _DeletedFilesListView extends StatelessWidget { - const _DeletedFilesListView( - this.state, - ); +enum _TrashActionType { + restoreAll, + deleteAll, +} + +class _TrashActionAllButton extends StatelessWidget { + /// Switch between 'delete all' and 'restore all' feature + const _TrashActionAllButton({ + this.type = _TrashActionType.deleteAll, + required this.trashBloc, + }); + final _TrashActionType type; + final TrashBloc trashBloc; - final TrashState state; @override Widget build(BuildContext context) { final theme = Theme.of(context); - return ListView.builder( - itemBuilder: (context, index) { - final object = state.objects[index]; - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), - child: ListTile( - // TODO(Yijing): implement file type after TrashPB has file type - leading: FlowySvg( - FlowySvgs.documents_s, - size: const Size.square(24), - color: theme.colorScheme.onSurface, - ), - title: Text( - object.name, - style: theme.textTheme.labelMedium - ?.copyWith(color: theme.colorScheme.onBackground), - ), - horizontalTitleGap: 0, - // TODO(yiing): needs improve by container/surface theme color - tileColor: theme.colorScheme.onSurface.withOpacity(0.1), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - // TODO(yijing): extract icon button - IconButton( - splashRadius: 20, - icon: FlowySvg( - FlowySvgs.m_restore_m, - size: const Size.square(24), - color: theme.colorScheme.onSurface, - ), - onPressed: () { - context - .read() - .add(TrashEvent.putback(object.id)); - }, - ), - IconButton( - splashRadius: 20, - icon: FlowySvg( - FlowySvgs.m_delete_m, - size: const Size.square(24), - color: theme.colorScheme.onSurface, - ), - onPressed: () { - context.read().add(TrashEvent.delete(object)); - }, - ) - ], - ), - ), - ); - }, - itemCount: state.objects.length, + final isDeleteAll = type == _TrashActionType.deleteAll; + return BlocProvider.value( + value: trashBloc, + child: BottomSheetActionWidget( + svg: isDeleteAll ? FlowySvgs.m_delete_m : FlowySvgs.m_restore_m, + text: isDeleteAll + ? LocaleKeys.trash_deleteAll.tr() + : LocaleKeys.trash_restoreAll.tr(), + onTap: () { + final trashList = trashBloc.state.objects; + if (trashList.isNotEmpty) { + context.pop(); + showFlowyMobileConfirmDialog( + context, + title: isDeleteAll + ? LocaleKeys.trash_confirmDeleteAll_title.tr() + : LocaleKeys.trash_restoreAll.tr(), + content: isDeleteAll + ? LocaleKeys.trash_confirmDeleteAll_caption.tr() + : LocaleKeys.trash_confirmRestoreAll_caption.tr(), + actionButtonTitle: isDeleteAll + ? LocaleKeys.trash_deleteAll.tr() + : LocaleKeys.trash_restoreAll.tr(), + actionButtonColor: isDeleteAll + ? theme.colorScheme.error + : theme.colorScheme.primary, + onActionButtonPressed: () { + if (isDeleteAll) { + trashBloc.add( + const TrashEvent.deleteAll(), + ); + } else { + trashBloc.add( + const TrashEvent.restoreAll(), + ); + } + }, + cancelButtonTitle: LocaleKeys.button_cancel.tr(), + ); + } else { + // when there is no deleted files + // show toast + Fluttertoast.showToast( + msg: LocaleKeys.trash_mobile_empty.tr(), + gravity: ToastGravity.CENTER, + ); + } + }, + ), ); } } -class _TrashEmptyPage extends StatelessWidget { - const _TrashEmptyPage(); +class _DeletedFilesListView extends StatelessWidget { + const _DeletedFilesListView( + this.state, + ); + final TrashState state; @override Widget build(BuildContext context) { final theme = Theme.of(context); - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - '🗑️', - style: TextStyle(fontSize: 40), - ), - const SizedBox(height: 8), - Text( - LocaleKeys.trash_mobile_empty.tr(), - style: theme.textTheme.labelLarge, - ), - const SizedBox(height: 4), - Text( - LocaleKeys.trash_mobile_emptyDescription.tr(), - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.hintColor, + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: ListView.builder( + itemBuilder: (context, index) { + final object = state.objects[index]; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + child: ListTile( + // TODO(Yijing): implement file type after TrashPB has file type + leading: FlowySvg( + FlowySvgs.documents_s, + size: const Size.square(24), + color: theme.colorScheme.onSurface, + ), + title: Text( + object.name, + style: theme.textTheme.labelMedium + ?.copyWith(color: theme.colorScheme.onBackground), + ), + horizontalTitleGap: 0, + // TODO(yiing): needs improve by container/surface theme color + tileColor: theme.colorScheme.onSurface.withOpacity(0.1), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // TODO(yijing): extract icon button + IconButton( + splashRadius: 20, + icon: FlowySvg( + FlowySvgs.m_restore_m, + size: const Size.square(24), + color: theme.colorScheme.onSurface, + ), + onPressed: () { + context + .read() + .add(TrashEvent.putback(object.id)); + Fluttertoast.showToast( + msg: + '${object.name} ${LocaleKeys.trash_mobile_isRestored.tr()}', + gravity: ToastGravity.BOTTOM, + ); + }, + ), + IconButton( + splashRadius: 20, + icon: FlowySvg( + FlowySvgs.m_delete_m, + size: const Size.square(24), + color: theme.colorScheme.onSurface, + ), + onPressed: () { + context.read().add(TrashEvent.delete(object)); + Fluttertoast.showToast( + msg: + '${object.name} ${LocaleKeys.trash_mobile_isDeleted.tr()}', + gravity: ToastGravity.BOTTOM, + ); + }, + ) + ], + ), ), - ), - ], + ); + }, + itemCount: state.objects.length, ), ); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart index 18cc9a1bb9eb..f6d9c44ef1cf 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart @@ -1,8 +1,8 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/mobile_router.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_add_new_page.dart'; import 'package:appflowy/mobile/presentation/page_item/mobile_view_item_add_button.dart'; +import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; @@ -395,20 +395,24 @@ class _SingleMobileInnerViewItemState extends State { Widget _buildViewAddButton(BuildContext context) { return MobileViewAddButton( onPressed: () { - showMobileBottomSheet( - context: context, - builder: (_) => AddNewPageWidgetBottomSheet( - view: widget.view, - onAction: (layout) { - context.pop(); - context.read().add( - ViewEvent.createView( - LocaleKeys.menuAppHeader_defaultNewPageName.tr(), - layout, - ), - ); - }, - ), + final title = widget.view.name; + showFlowyMobileBottomSheet( + context, + title: title, + builder: (_) { + return AddNewPageWidgetBottomSheet( + view: widget.view, + onAction: (layout) { + context.pop(); + context.read().add( + ViewEvent.createView( + LocaleKeys.menuAppHeader_defaultNewPageName.tr(), + layout, + ), + ); + }, + ); + }, ); }, ); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/personal_info_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/personal_info_setting_group.dart index 1f177bed22ec..397621248c51 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/personal_info_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/personal_info_setting_group.dart @@ -16,7 +16,7 @@ class PersonalInfoSettingGroup extends StatelessWidget { required this.userProfile, }); - final UserProfilePB? userProfile; + final UserProfilePB userProfile; @override Widget build(BuildContext context) { @@ -33,9 +33,9 @@ class PersonalInfoSettingGroup extends StatelessWidget { settingItemList: [ MobileSettingItem( name: userName, - subtitle: isCloudEnabled && userProfile != null + subtitle: isCloudEnabled ? Text( - userProfile!.email, + userProfile.email, style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurface, ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/support_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/support_setting_group.dart index 5fcbf62fe8ac..3995e11b983c 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/support_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/support_setting_group.dart @@ -4,6 +4,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:package_info_plus/package_info_plus.dart'; import 'widgets/widgets.dart'; @@ -14,32 +15,33 @@ class SupportSettingGroup extends StatelessWidget { @override Widget build(BuildContext context) { - return MobileSettingGroup( - groupTitle: LocaleKeys.settings_mobile_support.tr(), - settingItemList: [ - // 'Help Center' - MobileSettingItem( - name: LocaleKeys.settings_mobile_joinDiscord.tr(), - trailing: const Icon( - Icons.chevron_right, + return FutureBuilder( + future: PackageInfo.fromPlatform(), + builder: (context, snapshot) => MobileSettingGroup( + groupTitle: LocaleKeys.settings_mobile_support.tr(), + settingItemList: [ + MobileSettingItem( + name: LocaleKeys.settings_mobile_joinDiscord.tr(), + trailing: const Icon( + Icons.chevron_right, + ), + onTap: () => safeLaunchUrl('https://discord.gg/JucBXeU2FE'), ), - onTap: () => safeLaunchUrl('https://discord.gg/JucBXeU2FE'), - ), - MobileSettingItem( - name: LocaleKeys.workspace_errorActions_reportIssue.tr(), - trailing: const Icon( - Icons.chevron_right, + MobileSettingItem( + name: LocaleKeys.workspace_errorActions_reportIssue.tr(), + trailing: const Icon( + Icons.chevron_right, + ), + onTap: () { + final String? version = snapshot.data?.version; + final String os = Platform.operatingSystem; + safeLaunchUrl( + 'https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&projects=&template=bug_report.yaml&title=[Bug]%20Mobile:%20&version=$version&os=$os', + ); + }, ), - onTap: () { - // TODO(yijing): get app version before release - const String version = 'Beta'; - final String os = Platform.operatingSystem; - safeLaunchUrl( - 'https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&projects=&template=bug_report.yaml&title=[Bug]%20Mobile:%20&version=$version&os=$os', - ); - }, - ), - ], + ], + ), ); } } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_state_container.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_state_container.dart new file mode 100644 index 000000000000..ad5aa3c894b8 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_state_container.dart @@ -0,0 +1,102 @@ +import 'dart:io'; + +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +enum _FlowyMobileStateContainerType { + info, + error, +} + +/// Used to display info(like empty state) or error state +/// error state has two buttons to report issue with error message or reach out on discord +class FlowyMobileStateContainer extends StatelessWidget { + const FlowyMobileStateContainer.error({ + this.emoji, + required this.title, + this.description, + required this.errorMsg, + super.key, + }) : _stateType = _FlowyMobileStateContainerType.error; + + const FlowyMobileStateContainer.info({ + this.emoji, + required this.title, + this.description, + super.key, + }) : errorMsg = null, + _stateType = _FlowyMobileStateContainerType.info; + + final String? emoji; + final String title; + final String? description; + final String? errorMsg; + final _FlowyMobileStateContainerType _stateType; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return SizedBox.expand( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 32), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + emoji ?? '', + style: const TextStyle(fontSize: 40), + ), + const SizedBox(height: 8), + Text( + title, + style: theme.textTheme.labelLarge, + ), + const SizedBox(height: 4), + Text( + description ?? '', + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.hintColor, + ), + ), + if (_stateType == _FlowyMobileStateContainerType.error) ...[ + const SizedBox(height: 8), + FutureBuilder( + future: PackageInfo.fromPlatform(), + builder: (context, snapshot) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + OutlinedButton( + onPressed: () { + final String? version = snapshot.data?.version; + final String os = Platform.operatingSystem; + safeLaunchUrl( + 'https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&projects=&template=bug_report.yaml&title=[Bug]%20Mobile:%20&version=$version&os=$os&context=Error%20log:%20$errorMsg', + ); + }, + child: Text( + LocaleKeys.workspace_errorActions_reportIssue.tr(), + ), + ), + OutlinedButton( + onPressed: () => + safeLaunchUrl('https://discord.gg/JucBXeU2FE'), + child: Text( + LocaleKeys.workspace_errorActions_reachOut.tr(), + ), + ), + ], + ); + }, + ) + ] + ], + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart new file mode 100644 index 000000000000..4966c1d9cd95 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart @@ -0,0 +1,62 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; + +///show the dialog to confirm one single action +///[onActionButtonPressed] and [onCancelButtonPressed] end with close the dialog +Future showFlowyMobileConfirmDialog( + BuildContext context, { + String? title, + String? content, + required String actionButtonTitle, + Color? actionButtonColor, + String? cancelButtonTitle, + required void Function()? onActionButtonPressed, + void Function()? onCancelButtonPressed, +}) async { + return showDialog( + context: context, + builder: (dialogContext) { + final foregroundColor = Theme.of(context).colorScheme.onSurface; + return AlertDialog( + title: Text( + title ?? "", + ), + content: Text( + content ?? "", + ), + actions: [ + TextButton( + child: Text( + actionButtonTitle, + style: TextStyle( + color: actionButtonColor ?? foregroundColor, + ), + ), + onPressed: () { + onActionButtonPressed?.call(); + // we cannot use dialogContext.pop() here because this is no GoRouter in dialogContext. Use Navigator instead to close the dialog. + Navigator.of( + dialogContext, + ).pop(); + }, + ), + TextButton( + child: Text( + cancelButtonTitle ?? LocaleKeys.button_cancel.tr(), + style: TextStyle( + color: foregroundColor, + ), + ), + onPressed: () { + onCancelButtonPressed?.call(); + Navigator.of( + dialogContext, + ).pop(); + }, + ), + ], + ); + }, + ); +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/widgets.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/widgets.dart new file mode 100644 index 000000000000..8c8b0ad0bd44 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/widgets.dart @@ -0,0 +1,3 @@ +export 'show_flowy_mobile_confirm_dialog.dart'; +export 'show_flowy_mobile_bottom_sheet.dart'; +export 'flowy_mobile_state_container.dart'; diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 5aea6e5b446a..fff0e8ec08a1 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -120,7 +120,9 @@ "mobile": { "actions": "Trash Actions", "empty": "Trash Bin is Empty", - "emptyDescription": "You don't have any deleted file" + "emptyDescription": "You don't have any deleted file", + "isDeleted": "is deleted", + "isRestored": "is restored" } }, "deletePagePrompt": { @@ -394,7 +396,9 @@ "support": "Support", "joinDiscord": "Join us in Discord", "privacyPolicy": "Privacy Policy", - "userAgreement": "User Agreement" + "userAgreement": "User Agreement", + "userprofileError": "Failed to load user profile", + "userprofileErrorDescription": "Please try to log out and log back in to check if the issue still persists." } }, "grid": { From 5f49c1748f2dd06295f6e7a5eea52308e452570d Mon Sep 17 00:00:00 2001 From: "Kilu.He" <108015703+qinluhe@users.noreply.github.com> Date: Fri, 3 Nov 2023 15:13:49 +0800 Subject: [PATCH 20/56] Fix/tauri warning to error (#3869) * feat: sort basic function * fix: eslint error * fix: deal with conflict * fix: prevent submit eslint warning code * fix: modify tauri warning to error --------- Co-authored-by: fangwufeng-v --- frontend/appflowy_tauri/.eslintrc.cjs | 20 +- frontend/appflowy_tauri/package.json | 2 +- .../src/appflowy_app/assets/close.svg | 4 + .../BlockDraggable/BlockDragDropContext.tsx | 2 +- .../_shared/BlockDraggable/index.tsx | 2 +- .../components/_shared/Database.hooks.ts | 9 +- .../DatabaseFilter/DatabaseFilterItem.tsx | 1 + .../_shared/DatabaseSort/DatabaseSortItem.tsx | 1 + .../DatabaseSort/DatabaseSortPopup.tsx | 2 +- .../EditRow/CheckList/NewCheckListOption.tsx | 1 + .../_shared/EditRow/Date/DateFormatPopup.tsx | 1 + .../_shared/EditRow/Date/DatePickerPopup.tsx | 2 + .../EditRow/Date/DateTimeFormat.hooks.ts | 9 +- .../_shared/EditRow/Date/DateTypeOptions.tsx | 8 +- .../_shared/EditRow/Date/EditCellDate.tsx | 1 + .../EditRow/Date/NumberFormat.hooks.ts | 3 + .../EditRow/Date/NumberFormatPopup.tsx | 1 + .../_shared/EditRow/Date/TimeFormatPopup.tsx | 1 + .../_shared/EditRow/EditFieldPopup.tsx | 1 + .../components/_shared/EditRow/EditRow.tsx | 2 + .../_shared/EditRow/FieldTypeName.tsx | 1 + .../InlineEditFields/EditCheckboxCell.tsx | 1 - .../Options/MultiSelectTypeOptions.tsx | 2 - .../_shared/LanguageSelectPopup.tsx | 1 + .../_shared/database-hooks/loadField.ts | 7 +- .../_shared/database-hooks/useCell.ts | 5 + .../_shared/database-hooks/useDatabase.ts | 1 + .../_shared/database-hooks/useRow.ts | 2 + .../components/_shared/useOutsideClick.ts | 3 + .../components/auth/Login/Login.hooks.ts | 7 +- .../components/auth/ProtectedRoutes.tsx | 1 + .../components/auth/SignUp/SignUp.hooks.ts | 6 +- .../components/board/BoardCheckboxCell.tsx | 1 + .../components/board/BoardDateCell.tsx | 1 + .../components/board/BoardSettingsPopup.tsx | 1 + .../components/database/Database.hooks.ts | 84 +++ .../components/database/Database.tsx | 67 +- .../components/database/DatabaseLoader.tsx | 22 + .../{DatabaseHeader.tsx => DatabaseTitle.tsx} | 4 +- .../components/database/DatabaseView.tsx | 19 + .../database/_shared/dnd/drag.hooks.ts | 82 +- .../components/database/_shared/dnd/utils.ts | 1 - .../database/application/cell/cell_service.ts | 120 +++ .../database/application/cell/cell_types.ts | 128 ++++ .../database/application/cell/index.ts | 2 + .../application/database/database_service.ts | 87 +++ .../application/database/database_types.ts | 18 + .../database/application/database/index.ts | 2 + .../database_view/database_view_service.ts | 70 ++ .../database_view/database_view_types.ts | 25 + .../application/database_view/index.ts | 2 + .../application/field/field_service.ts | 126 ++++ .../database/application/field/field_types.ts | 41 + .../database/application/field/index.ts | 4 + .../application/field/select_option/index.ts | 2 + .../select_option/select_option_service.ts | 61 ++ .../select_option/select_option_types.ts | 15 + .../application/field/type_option/index.ts | 2 + .../field/type_option/type_option_service.ts | 18 + .../field/type_option/type_option_types.ts | 68 ++ .../application/filter/filter_service.ts | 73 ++ .../application/filter/filter_types.ts | 86 +++ .../database/application/filter/index.ts | 2 + .../application/group/group_service.ts | 64 ++ .../database/application/group/group_types.ts | 34 + .../database/application/group/index.ts | 2 + .../components/database/application/index.ts | 8 + .../database/application/row/index.ts | 3 + .../database/application/row/row_listeners.ts | 61 ++ .../database/application/row/row_service.ts | 124 ++++ .../database/application/row/row_types.ts | 28 + .../database/application/sort/index.ts | 3 + .../application/sort/sort_listeners.ts | 35 + .../database/application/sort/sort_service.ts | 77 ++ .../database/application/sort/sort_types.ts | 17 + .../components/database/board/Board.tsx | 5 + .../components/database/board/index.ts | 1 + .../components/database/calendar/Calendar.tsx | 5 + .../components/database/calendar/index.ts | 1 + .../database/components/cell/Cell.hooks.ts | 24 + .../database/components/cell/Cell.tsx | 42 ++ .../database/components/cell/CheckboxCell.tsx | 35 + .../cell/SelectCell}/CreateOption.tsx | 0 .../cell/SelectCell/SelectCell.tsx} | 29 +- .../cell/SelectCell}/SelectOptionItem.tsx | 6 +- .../cell/SelectCell}/SelectOptionMenu.tsx | 19 +- .../cell/SelectCell}/Tag.tsx | 0 .../cell/SelectCell}/constants.ts | 2 +- .../components/cell/SelectCell/index.ts | 1 + .../cell/TextCell.tsx} | 34 +- .../database/components/cell/index.ts | 1 + .../database_settings/DatabaseSettings.tsx | 13 + .../components/database_settings/index.ts | 1 + .../database/components/field/Field.tsx | 20 + .../database/components/field/FieldSelect.tsx | 32 + .../components/field/FieldTypeSvg.tsx | 30 + .../components/field/FieldTypeText.tsx | 19 + .../database/components/field/FieldsMenu.tsx | 30 + .../database/components/field/index.ts | 3 + .../components/database/components/index.ts | 2 + .../components/sort/SortConditionSelect.tsx | 17 + .../database/components/sort/SortItem.tsx | 63 ++ .../database/components/sort/SortMenu.tsx | 59 ++ .../database/components/sort/Sorts.tsx | 37 + .../database/components/sort/index.ts | 2 + .../components/tab_bar/DatabaseTabBar.tsx | 64 ++ .../components/tab_bar/TextButton.tsx | 9 + .../database/components/tab_bar/ViewTabs.tsx | 21 + .../database/components/tab_bar/index.ts | 1 + .../components/database/database.context.ts | 16 - .../components/database/database.hooks.ts | 138 ---- .../components/database/database_bd_svc.ts | 698 ------------------ .../components/database/grid/Grid/Grid.tsx | 10 +- .../database/grid/GridCell/GridCell.hooks.ts | 34 - .../database/grid/GridCell/GridCell.tsx | 40 +- .../grid/GridCell/GridCheckboxCell.tsx | 31 - .../grid/GridCell/GridNotSupportedCell.tsx | 5 - .../grid/GridCell/GridSelectCell/index.ts | 1 - .../database/grid/GridCell/index.ts | 2 +- .../database/grid/GridField/FieldTypeSvg.tsx | 6 +- .../database/grid/GridField/GridField.tsx | 12 +- .../database/grid/GridField/GridFieldMenu.tsx | 11 +- .../GridRow/{ => GridCellRow}/GridCellRow.tsx | 26 +- .../{ => GridCellRow}/GridCellRowActions.tsx | 8 +- .../grid/GridRow/GridCellRow/index.ts | 1 + .../database/grid/GridRow/GridFieldRow.tsx | 10 +- .../database/grid/GridRow/GridNewRow.tsx | 27 +- .../database/grid/GridRow/GridRow.tsx | 4 +- .../database/grid/GridRow/constants.ts | 34 +- .../database/grid/GridTable/GridTable.tsx | 51 +- .../database/grid/GridToolbar/GridToolbar.tsx | 41 - .../database/grid/GridToolbar/index.ts | 1 - .../appflowy_app/components/database/index.ts | 4 +- .../BlockRectSelection.hooks.ts | 2 +- .../BlockSelection/NodesRect.hooks.ts | 4 + .../BlockSelection/RangeKeyDown.hooks.ts | 21 +- .../document/BlockSideToolbar/BlockMenu.tsx | 4 +- .../document/BlockSideToolbar/index.tsx | 6 +- .../document/BlockSlash/BlockSlashMenu.tsx | 64 +- .../document/CodeBlock/SelectLanguage.tsx | 2 +- .../document/CodeBlock/useKeyDown.ts | 7 +- .../document/ColumnListBlock/Column.tsx | 39 - .../document/ColumnListBlock/index.tsx | 31 - .../DocumentBanner/DocumentBanner.hooks.ts | 5 +- .../DocumentBanner/cover/ChangeImages.tsx | 4 +- .../document/DocumentBanner/index.tsx | 1 + .../document/ImageBlock/ImageAlign.tsx | 2 +- .../document/ImageBlock/ImageToolbar.tsx | 2 +- .../components/document/ImageBlock/index.tsx | 2 +- .../document/ImageBlock/useImageBlock.ts | 4 +- .../NumberedListBlock.hooks.ts | 6 +- .../TextActionMenu/menu/CustomColorPicker.tsx | 2 +- .../TextActionMenu/menu/FormatButton.tsx | 2 +- .../TextActionMenu/menu/index.hooks.ts | 1 + .../document/TextBlock/useKeyDown.ts | 15 +- .../TextBlock/useTurnIntoBlockEvents.ts | 14 +- .../document/ToggleListBlock/index.tsx | 2 +- .../BlockPopover/BlockPopover.hooks.tsx | 2 +- .../_shared/CopyPasteHooks/useCopy.ts | 3 +- .../_shared/CopyPasteHooks/usePaste.ts | 4 +- .../_shared/EditorHooks/useCommonKeyEvents.ts | 14 +- .../document/_shared/EditorHooks/useDelta.ts | 2 +- .../_shared/EditorHooks/useSelection.ts | 6 +- .../_shared/InlineBlock/CodeInline.tsx | 2 +- .../_shared/InlineBlock/FormulaInline.tsx | 6 +- .../_shared/InlineBlock/LinkInline.tsx | 5 +- .../_shared/InlineBlock/PageInline.tsx | 15 +- .../document/_shared/SlateEditor/TextLeaf.tsx | 1 + .../document/_shared/SlateEditor/useEditor.ts | 9 +- .../_shared/SlateEditor/useSlateYjs.ts | 1 + .../_shared/SubscribeMention.hooks.ts | 1 + .../document/_shared/SubscribeNode.hooks.ts | 5 + .../_shared/SubscribeSelection.hooks.ts | 4 +- .../_shared/TemporaryInput/TemporaryLink.tsx | 3 +- .../TemporaryInput/TemporaryPopover.tsx | 1 - .../document/_shared/TemporaryInput/index.tsx | 1 + .../_shared/TurnInto/TurnInto.hooks.ts | 4 +- .../document/_shared/TurnInto/index.tsx | 8 +- .../document/_shared/UndoHooks/useUndoRedo.ts | 15 +- .../_shared/UploadImage/ImageEditPopover.tsx | 1 - .../document/_shared/UploadImage/TabPanel.tsx | 1 + .../_shared/UploadImage/UploadImage.tsx | 5 +- .../document/_shared/useBindArrowKey.ts | 1 + .../document/_shared/usePanelSearchText.ts | 2 + .../components/error/Error.hooks.ts | 19 +- .../grid/GridTableCount/GridTableCount.tsx | 1 + .../GridTableHeader/GridTableHeaderItem.tsx | 3 +- .../layout/Breadcrumb/Breadcrumb.hooks.ts | 21 +- .../layout/NestedPage/NestedPage.hooks.ts | 33 +- .../components/layout/Share/Share.hooks.ts | 2 +- .../components/layout/TopBar/MoreButton.tsx | 5 +- .../layout/TopBar/MoreOptions.hooks.ts | 3 +- .../components/layout/TopBar/MoreOptions.tsx | 4 - .../layout/UserSetting/LanguageSetting.tsx | 2 +- .../components/layout/UserSetting/index.tsx | 2 +- .../layout/WorkspaceManager/NewPageButton.tsx | 4 +- .../WorkspaceManager/Workspace.hooks.ts | 5 +- .../layout/WorkspaceManager/Workspace.tsx | 4 - .../components/tests/DatabaseTestHelper.ts | 52 +- .../components/tests/TestDocument.tsx | 3 +- .../components/tests/TestFolder.tsx | 2 +- .../components/tests/TestFonts.tsx | 4 +- .../components/tests/TestGrid.tsx | 61 ++ .../components/tests/TestGroup.tsx | 10 + .../components/trash/Trash.hooks.ts | 6 +- .../appflowy_app/components/trash/Trash.tsx | 4 +- .../components/trash/TrashItem.tsx | 4 +- .../user/application/notifications/parser.ts | 1 + .../notifications/user_listener.ts | 2 + .../src/appflowy_app/hooks/ViewId.hooks.ts | 6 + .../src/appflowy_app/hooks/index.ts | 1 + .../appflowy_app/hooks/notification.hooks.ts | 48 +- .../src/appflowy_app/i18n/config.ts | 2 +- .../appflowy_app/interfaces/database/index.ts | 2 - .../interfaces/database/transform.ts | 72 -- .../appflowy_app/interfaces/database/types.ts | 227 ------ .../src/appflowy_app/interfaces/document.ts | 27 +- .../src/appflowy_app/interfaces/index.ts | 1 - .../effects/database/cell/cell_bd_svc.ts | 1 + .../effects/database/cell/cell_cache.ts | 5 + .../effects/database/cell/cell_controller.ts | 7 + .../effects/database/cell/cell_observer.ts | 1 + .../effects/database/cell/data_parser.ts | 4 +- .../effects/database/cell/data_persistence.ts | 2 + .../database/cell/select_option_bd_svc.ts | 6 +- .../effects/database/database_bd_svc.ts | 2 +- .../effects/database/database_controller.ts | 1 - .../effects/database/field/field_bd_svc.ts | 2 + .../database/field/field_controller.ts | 8 + .../effects/database/field/field_observer.ts | 2 + .../field/type_option/type_option_bd_svc.ts | 3 + .../field/type_option/type_option_context.ts | 10 + .../type_option/type_option_controller.ts | 9 + .../database/group/group_controller.ts | 14 +- .../effects/database/group/group_observer.ts | 2 + .../database/notifications/observer.ts | 1 + .../effects/database/notifications/parser.ts | 1 + .../stores/effects/database/row/row_cache.ts | 37 +- .../database/view/database_view_cache.ts | 2 +- .../database/view/view_row_observer.ts | 4 + .../document/notifications/observer.ts | 1 + .../effects/document/notifications/parser.ts | 1 + .../stores/effects/user/user_bd_svc.ts | 4 +- .../effects/workspace/page/page_controller.ts | 7 +- .../effects/workspace/trash/controller.ts | 8 +- .../effects/workspace/workspace_controller.ts | 19 +- .../workspace/workspace_manager_controller.ts | 7 +- .../stores/reducers/block-draggable/slice.ts | 14 +- .../stores/reducers/current-user/slice.ts | 1 - .../async-actions/blocks/duplicate.ts | 2 +- .../document/async-actions/blocks/insert.ts | 10 +- .../document/async-actions/blocks/update.ts | 16 +- .../document/async-actions/copy_paste.ts | 4 +- .../document/async-actions/keydown.ts | 7 +- .../document/async-actions/mention.ts | 8 +- .../reducers/document/async-actions/menu.ts | 9 +- .../reducers/document/async-actions/range.ts | 4 +- .../document/async-actions/temporary.ts | 1 - .../document/async-actions/turn_to.ts | 8 +- .../stores/reducers/pages/async_actions.ts | 40 +- .../stores/reducers/pages/slice.ts | 3 + .../src/appflowy_app/utils/async_queue.ts | 6 +- .../src/appflowy_app/utils/change_notifier.ts | 1 + .../src/appflowy_app/utils/document/action.ts | 23 +- .../utils/document/block_delta.ts | 4 +- .../src/appflowy_app/utils/document/format.ts | 1 + .../src/appflowy_app/utils/document/node.ts | 4 +- .../utils/document/quill_editor.ts | 1 + .../utils/document/slate_editor.ts | 1 + .../appflowy_app/utils/document/toolbar.ts | 2 + .../src/appflowy_app/utils/draggable.ts | 2 - .../src/appflowy_app/utils/region_grid.ts | 3 +- .../src/appflowy_app/utils/tool.ts | 6 +- .../src/appflowy_app/views/DatabasePage.tsx | 24 +- 274 files changed, 2970 insertions(+), 2052 deletions(-) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/assets/close.svg create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/Database.hooks.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseLoader.tsx rename frontend/appflowy_tauri/src/appflowy_app/components/database/{DatabaseHeader.tsx => DatabaseTitle.tsx} (93%) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseView.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/cell/cell_service.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/cell/cell_types.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/cell/index.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/database/database_service.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/database/database_types.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/database/index.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/database_view/database_view_service.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/database_view/database_view_types.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/database_view/index.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/field_service.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/field_types.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/index.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/select_option/index.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/select_option/select_option_service.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/select_option/select_option_types.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/type_option/index.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/type_option/type_option_service.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/type_option/type_option_types.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/filter/filter_service.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/filter/filter_types.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/filter/index.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/group/group_service.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/group/group_types.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/group/index.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/index.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/row/index.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/row/row_listeners.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/row/row_service.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/row/row_types.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/sort/index.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/sort/sort_listeners.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/sort/sort_service.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/sort/sort_types.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/board/Board.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/board/index.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/calendar/Calendar.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/calendar/index.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/Cell.hooks.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/Cell.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/CheckboxCell.tsx rename frontend/appflowy_tauri/src/appflowy_app/components/database/{grid/GridCell/GridSelectCell => components/cell/SelectCell}/CreateOption.tsx (100%) rename frontend/appflowy_tauri/src/appflowy_app/components/database/{grid/GridCell/GridSelectCell/GridSelectCell.tsx => components/cell/SelectCell/SelectCell.tsx} (79%) rename frontend/appflowy_tauri/src/appflowy_app/components/database/{grid/GridCell/GridSelectCell => components/cell/SelectCell}/SelectOptionItem.tsx (93%) rename frontend/appflowy_tauri/src/appflowy_app/components/database/{grid/GridCell/GridSelectCell => components/cell/SelectCell}/SelectOptionMenu.tsx (90%) rename frontend/appflowy_tauri/src/appflowy_app/components/database/{grid/GridCell/GridSelectCell => components/cell/SelectCell}/Tag.tsx (100%) rename frontend/appflowy_tauri/src/appflowy_app/components/database/{grid/GridCell/GridSelectCell => components/cell/SelectCell}/constants.ts (94%) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell/index.ts rename frontend/appflowy_tauri/src/appflowy_app/components/database/{grid/GridCell/GridTextCell.tsx => components/cell/TextCell.tsx} (68%) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/index.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/DatabaseSettings.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/index.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/Field.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldSelect.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldTypeSvg.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldTypeText.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldsMenu.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/index.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/index.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/SortConditionSelect.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/SortItem.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/SortMenu.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/Sorts.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/index.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/DatabaseTabBar.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/TextButton.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/ViewTabs.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/index.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/database.context.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/database.hooks.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/database_bd_svc.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridCell/GridCell.hooks.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridCell/GridCheckboxCell.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridCell/GridNotSupportedCell.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridCell/GridSelectCell/index.ts rename frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/{ => GridCellRow}/GridCellRow.tsx (87%) rename frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/{ => GridCellRow}/GridCellRowActions.tsx (84%) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/index.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridToolbar/GridToolbar.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridToolbar/index.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/ColumnListBlock/Column.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/ColumnListBlock/index.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/hooks/ViewId.hooks.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/interfaces/database/index.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/interfaces/database/transform.ts delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/interfaces/database/types.ts diff --git a/frontend/appflowy_tauri/.eslintrc.cjs b/frontend/appflowy_tauri/.eslintrc.cjs index 7223e5cb39d9..2047d4f962d3 100644 --- a/frontend/appflowy_tauri/.eslintrc.cjs +++ b/frontend/appflowy_tauri/.eslintrc.cjs @@ -15,20 +15,20 @@ module.exports = { plugins: ['@typescript-eslint', "react-hooks"], rules: { "react-hooks/rules-of-hooks": "error", - "react-hooks/exhaustive-deps": "warn", + "react-hooks/exhaustive-deps": "error", '@typescript-eslint/adjacent-overload-signatures': 'error', '@typescript-eslint/no-empty-function': 'error', - '@typescript-eslint/no-empty-interface': 'warn', - '@typescript-eslint/no-floating-promises': 'warn', + '@typescript-eslint/no-empty-interface': 'error', + '@typescript-eslint/no-floating-promises': 'error', '@typescript-eslint/await-thenable': 'error', '@typescript-eslint/no-namespace': 'error', '@typescript-eslint/no-unnecessary-type-assertion': 'error', '@typescript-eslint/no-redeclare': 'error', - '@typescript-eslint/prefer-for-of': 'warn', + '@typescript-eslint/prefer-for-of': 'error', '@typescript-eslint/triple-slash-reference': 'error', - '@typescript-eslint/unified-signatures': 'warn', + '@typescript-eslint/unified-signatures': 'error', 'no-shadow': 'off', - '@typescript-eslint/no-shadow': 'warn', + '@typescript-eslint/no-shadow': 'off', 'constructor-super': 'error', eqeqeq: ['error', 'always'], 'no-cond-assign': 'error', @@ -47,18 +47,18 @@ module.exports = { 'no-throw-literal': 'error', 'no-unsafe-finally': 'error', 'no-unused-labels': 'error', - 'no-var': 'warn', + 'no-var': 'error', 'no-void': 'off', - 'prefer-const': 'warn', + 'prefer-const': 'error', 'prefer-spread': 'off', '@typescript-eslint/no-unused-vars': [ - 'warn', + 'error', { argsIgnorePattern: '^_', } ], 'padding-line-between-statements': [ - "warn", + "error", { blankLine: "always", prev: ["const", "let", "var"], next: "*"}, { blankLine: "any", prev: ["const", "let", "var"], next: ["const", "let", "var"]}, { blankLine: "always", prev: "import", next: "*" }, diff --git a/frontend/appflowy_tauri/package.json b/frontend/appflowy_tauri/package.json index 6c65dae94d04..e92fdc163f91 100644 --- a/frontend/appflowy_tauri/package.json +++ b/frontend/appflowy_tauri/package.json @@ -9,7 +9,7 @@ "preview": "vite preview", "format": "prettier --write .", "test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .", - "test:errors": "pnpm sync:i18n && tsc --noEmit && eslint --quiet --ext .js,.ts,.tsx .", + "test:errors": "pnpm sync:i18n && tsc --noEmit && eslint --ext .js,.ts,.tsx .", "test:prettier": "pnpm prettier --list-different src", "tauri:clean": "cargo make --cwd .. tauri_clean", "tauri:dev": "pnpm sync:i18n && tauri dev", diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/close.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/close.svg new file mode 100644 index 000000000000..b519b419c0bc --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/close.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/BlockDraggable/BlockDragDropContext.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/BlockDraggable/BlockDragDropContext.tsx index 2b0a307a37c4..0d7076b9bc4f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/BlockDraggable/BlockDragDropContext.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/BlockDraggable/BlockDragDropContext.tsx @@ -66,7 +66,7 @@ function BlockDragDropContext({ children }: { children: React.ReactNode }) { }; const onDragEnd = () => { - dispatch(onDragEndThunk()); + void dispatch(onDragEndThunk()); unlisten(); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/BlockDraggable/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/BlockDraggable/index.tsx index eaf0530c21aa..9ae0ebc545d5 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/BlockDraggable/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/BlockDraggable/index.tsx @@ -18,7 +18,7 @@ function BlockDraggable( } & HTMLAttributes, ref: React.Ref ) { - const { onDragStart, beforeDropping, afterDropping, childDropping, isDragging } = useDraggableState(id, type); + const { onDragStart, beforeDropping, afterDropping, childDropping } = useDraggableState(id, type); const commonCls = 'pointer-events-none absolute z-10 w-[100%] bg-fill-hover transition-all duration-200'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/Database.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/Database.hooks.ts index 50de845116f2..9b0955793e55 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/Database.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/Database.hooks.ts @@ -1,11 +1,6 @@ -import { useAppDispatch, useAppSelector } from '../../stores/store'; -import { useEffect, useState } from 'react'; -import { databaseActions, IDatabase } from '../../stores/reducers/database/slice'; -import { nanoid } from 'nanoid'; -import { FieldType } from '../../../services/backend'; +import { useAppSelector } from '$app/stores/store'; export const useDatabase = () => { - const dispatch = useAppDispatch(); const database = useAppSelector((state) => state.database); const newField = () => { @@ -22,7 +17,7 @@ export const useDatabase = () => { console.log('depreciated'); }; - const renameField = (fieldId: string, newTitle: string) => { + const renameField = (_fieldId: string, _newTitle: string) => { /* const field = database.fields[fieldId]; field.title = newTitle; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseFilter/DatabaseFilterItem.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseFilter/DatabaseFilterItem.tsx index 87c766c8146a..b0b7d4132263 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseFilter/DatabaseFilterItem.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseFilter/DatabaseFilterItem.tsx @@ -75,6 +75,7 @@ export const DatabaseFilterItem = ({ value: currentValue, }); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentFieldId, currentFieldType, currentOperator, currentValue, textInputActive]); // 1. not all field types support filtering diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseSort/DatabaseSortItem.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseSort/DatabaseSortItem.tsx index bc308e574ce7..5e096dce6ea7 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseSort/DatabaseSortItem.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseSort/DatabaseSortItem.tsx @@ -54,6 +54,7 @@ export const DatabaseSortItem = ({ fieldType: fields[currentFieldId].fieldType, }); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentFieldId, currentOrder]); const onSelectFieldClick = (id: string) => { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseSort/DatabaseSortPopup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseSort/DatabaseSortPopup.tsx index fe2c89772a33..98247e3a1a7e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseSort/DatabaseSortPopup.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseSort/DatabaseSortPopup.tsx @@ -1,5 +1,5 @@ import { t } from 'i18next'; -import { MouseEventHandler, useMemo, useRef, useState } from 'react'; +import { MouseEventHandler, useMemo, useState } from 'react'; import { useAppSelector } from '$app/stores/store'; import { IDatabaseSort } from '$app_reducers/database/slice'; import { DatabaseSortItem } from '$app/components/_shared/DatabaseSort/DatabaseSortItem'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CheckList/NewCheckListOption.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CheckList/NewCheckListOption.tsx index 1b9a24c1166b..940929660a09 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CheckList/NewCheckListOption.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CheckList/NewCheckListOption.tsx @@ -19,6 +19,7 @@ export const NewCheckListOption = ({ const updateNewOption = (value: string) => { const newOptionsCopy = [...newOptions]; + newOptionsCopy[index] = value; setNewOptions(newOptionsCopy); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/DateFormatPopup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/DateFormatPopup.tsx index 0ba94e577722..58e68fd28151 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/DateFormatPopup.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/DateFormatPopup.tsx @@ -29,6 +29,7 @@ export const DateFormatPopup = ({ useEffect(() => { setDateType(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as IDateType); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [databaseStore]); const changeFormat = async (format: DateFormatPB) => { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/DatePickerPopup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/DatePickerPopup.tsx index e9eb02af8ef8..8d5de41e4135 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/DatePickerPopup.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/DatePickerPopup.tsx @@ -30,6 +30,7 @@ export const DatePickerPopup = ({ useEffect(() => { const date_pb = data as DateCellDataPB | undefined; + if (!date_pb || !date_pb?.date.length) return; setSelectedDate(dayjs(date_pb.date).toDate()); @@ -39,6 +40,7 @@ export const DatePickerPopup = ({ if (v instanceof Date) { setSelectedDate(v); const date = new CalendarData(dayjs(v).add(dayjs().utcOffset(), 'minutes').toDate(), false); + await cellController?.saveCellData(date); } }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/DateTimeFormat.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/DateTimeFormat.hooks.ts index e024881350a0..25784c2efc1b 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/DateTimeFormat.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/DateTimeFormat.hooks.ts @@ -8,11 +8,14 @@ import { FieldController } from '$app/stores/effects/database/field/field_contro export const useDateTimeFormat = (cellIdentifier: CellIdentifier, fieldController: FieldController) => { const changeFormat = async (change: (option: DateTypeOptionPB) => void) => { const fieldInfo = fieldController.getField(cellIdentifier.fieldId); + if (!fieldInfo) return; const typeOptionController = new TypeOptionController(cellIdentifier.viewId, Some(fieldInfo), FieldType.DateTime); + await typeOptionController.initialize(); const dateTypeOptionContext = makeDateTypeOptionContext(typeOptionController); const typeOption = await dateTypeOptionContext.getTypeOption().then((a) => a.unwrap()); + change(typeOption); await dateTypeOptionContext.setTypeOption(typeOption); }; @@ -20,11 +23,13 @@ export const useDateTimeFormat = (cellIdentifier: CellIdentifier, fieldControlle const changeDateFormat = async (format: DateFormatPB) => { await changeFormat((option) => (option.date_format = format)); }; + const changeTimeFormat = async (format: TimeFormatPB) => { await changeFormat((option) => (option.time_format = format)); }; - const includeTime = async (include: boolean) => { - await changeFormat((option) => { + + const includeTime = async (_include: boolean) => { + await changeFormat((_option) => { // option.include_time = include; }); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/DateTypeOptions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/DateTypeOptions.tsx index 0b3f9415d216..c5e813b8b518 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/DateTypeOptions.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/DateTypeOptions.tsx @@ -1,8 +1,6 @@ import { DateFormatPopup } from '$app/components/_shared/EditRow/Date/DateFormatPopup'; import { TimeFormatPopup } from '$app/components/_shared/EditRow/Date/TimeFormatPopup'; import { MoreSvg } from '$app/components/_shared/svg/MoreSvg'; -import { EditorCheckSvg } from '$app/components/_shared/svg/EditorCheckSvg'; -import { EditorUncheckSvg } from '$app/components/_shared/svg/EditorUncheckSvg'; import { MouseEventHandler, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { IDateType } from '$app_reducers/database/slice'; @@ -27,14 +25,16 @@ export const DateTypeOptions = ({ const [showTimeFormatPopup, setShowTimeFormatPopup] = useState(false); const [timeFormatTop, setTimeFormatTop] = useState(0); const [timeFormatLeft, setTimeFormatLeft] = useState(0); - + // eslint-disable-next-line @typescript-eslint/no-unused-vars const [dateType, setDateType] = useState(); const databaseStore = useAppSelector((state) => state.database); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { includeTime } = useDateTimeFormat(cellIdentifier, fieldController); useEffect(() => { setDateType(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as IDateType); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [databaseStore]); const onDateFormatClick = (_left: number, _top: number) => { @@ -87,7 +87,7 @@ export const DateTypeOptions = ({ return (
    -
    +
    - } - /> -
    - - ); -}; \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridToolbar/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridToolbar/index.ts deleted file mode 100644 index e4a94567a0e1..000000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridToolbar/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './GridToolbar'; \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/index.ts index fafeee00d449..42a6f3159281 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/index.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/index.ts @@ -1,3 +1,3 @@ -export * from './database.context'; -export * from './database.hooks'; +export * from './Database.hooks'; export * from './Database'; +export * from './DatabaseTitle'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/BlockRectSelection.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/BlockRectSelection.hooks.ts index 5122af621b1c..f0258354c156 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/BlockRectSelection.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/BlockRectSelection.hooks.ts @@ -90,7 +90,7 @@ export function useBlockRectSelection({ container, getIntersectedBlockIds }: Blo const blockIds = getIntersectedBlockIds(newRect); setRect(newRect); - dispatch( + void dispatch( setRectSelectionThunk({ selection: blockIds, docId, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/NodesRect.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/NodesRect.hooks.ts index d528e41050ba..a0824836bdec 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/NodesRect.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/NodesRect.hooks.ts @@ -16,6 +16,7 @@ export function useNodesRect(container: HTMLDivElement) { (node: Element) => { const { x, y, width, height } = node.getBoundingClientRect(); const id = node.getAttribute('data-block-id'); + if (!id) return; const rect = { id, @@ -24,6 +25,7 @@ export function useNodesRect(container: HTMLDivElement) { width, height, }; + regionGrid?.updateBlock(rect); }, [container.scrollLeft, container.scrollTop, regionGrid] @@ -31,6 +33,7 @@ export function useNodesRect(container: HTMLDivElement) { const updateViewPortNodesRect = useCallback(() => { const nodes = container.querySelectorAll('[data-block-id]'); + nodes.forEach(updateNodeRect); }, [container, updateNodeRect]); @@ -55,6 +58,7 @@ export function useNodesRect(container: HTMLDivElement) { const y = Math.min(startY, endY); const width = Math.abs(endX - startX); const height = Math.abs(endY - startY); + return regionGrid .getIntersectingBlocks({ x, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/RangeKeyDown.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/RangeKeyDown.hooks.ts index 8a85fcb71a26..21d18b07b6dc 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/RangeKeyDown.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSelection/RangeKeyDown.hooks.ts @@ -2,7 +2,6 @@ import { useCallback, useMemo } from 'react'; import { Keyboard } from '$app/constants/document/keyboard'; import { useAppDispatch } from '$app/stores/store'; import { arrowActionForRangeThunk, deleteRangeAndInsertThunk } from '$app_reducers/document/async-actions'; -import Delta from 'quill-delta'; import isHotkey from 'is-hotkey'; import { deleteRangeAndInsertEnterThunk } from '$app_reducers/document/async-actions/range'; import { useRangeRef } from '$app/components/document/_shared/SubscribeSelection.hooks'; @@ -26,7 +25,7 @@ export function useRangeKeyDown() { }, handler: (_: KeyboardEvent) => { if (!controller) return; - dispatch( + void dispatch( deleteRangeAndInsertThunk({ controller, }) @@ -40,7 +39,7 @@ export function useRangeKeyDown() { }, handler: (e: KeyboardEvent) => { if (!controller) return; - dispatch( + void dispatch( deleteRangeAndInsertThunk({ controller, insertChar: e.key, @@ -53,9 +52,9 @@ export function useRangeKeyDown() { canHandle: (e: KeyboardEvent) => { return isHotkey(Keyboard.keys.SHIFT_ENTER, e); }, - handler: (e: KeyboardEvent) => { + handler: () => { if (!controller) return; - dispatch( + void dispatch( deleteRangeAndInsertEnterThunk({ controller, shiftKey: true, @@ -68,9 +67,9 @@ export function useRangeKeyDown() { canHandle: (e: KeyboardEvent) => { return isHotkey(Keyboard.keys.ENTER, e); }, - handler: (e: KeyboardEvent) => { + handler: () => { if (!controller) return; - dispatch( + void dispatch( deleteRangeAndInsertEnterThunk({ controller, shiftKey: false, @@ -89,7 +88,7 @@ export function useRangeKeyDown() { ); }, handler: (e: KeyboardEvent) => { - dispatch( + void dispatch( arrowActionForRangeThunk({ key: e.key, docId, @@ -105,7 +104,7 @@ export function useRangeKeyDown() { const format = parseFormat(e); if (!format) return; - dispatch( + void dispatch( toggleFormatThunk({ format, controller, @@ -117,7 +116,7 @@ export function useRangeKeyDown() { [controller, dispatch, docId] ); - const onKeyDownCapture = useCallback( + return useCallback( (e: KeyboardEvent) => { if (!rangeRef.current) { return; @@ -147,6 +146,4 @@ export function useRangeKeyDown() { }, [interceptEvents, rangeRef] ); - - return onKeyDownCapture; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideToolbar/BlockMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideToolbar/BlockMenu.tsx index cef2c021841e..932a3ad3304c 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideToolbar/BlockMenu.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideToolbar/BlockMenu.tsx @@ -93,9 +93,7 @@ function BlockMenu({ id, onClose }: { id: string; onClose: () => void }) { if (hovered) { const option = options.find((option) => option.key === hovered); - if (option) { - option.operate?.(); - } + void option?.operate?.(); } else { onClose(); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideToolbar/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideToolbar/index.tsx index b48d34516d29..a086fed1dce3 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideToolbar/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideToolbar/index.tsx @@ -38,7 +38,7 @@ export default function BlockSideToolbar({ id }: { id: string }) { pointerEvents: show ? 'auto' : 'none', }} onClick={(_: React.MouseEvent) => { - dispatch( + void dispatch( addBlockBelowClickThunk({ id, controller, @@ -74,8 +74,8 @@ export default function BlockSideToolbar({ id }: { id: string }) { pointerEvents: show ? 'auto' : 'none', }} data-draggable-anchor={id} - onClick={(e: React.MouseEvent) => { - dispatch( + onClick={async (e: React.MouseEvent) => { + await dispatch( setRectSelectionThunk({ docId, selection: [id], diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSlash/BlockSlashMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSlash/BlockSlashMenu.tsx index 234ff085f847..583d57594db3 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSlash/BlockSlashMenu.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSlash/BlockSlashMenu.tsx @@ -27,7 +27,7 @@ import { slashCommandActions } from '$app_reducers/document/slice'; import { Keyboard } from '$app/constants/document/keyboard'; import { selectOptionByUpDown } from '$app/utils/document/menu'; import { turnToBlockThunk } from '$app_reducers/document/async-actions'; -import {useTranslation} from "react-i18next"; +import { useTranslation } from 'react-i18next'; function BlockSlashMenu({ id, @@ -43,11 +43,11 @@ function BlockSlashMenu({ container: HTMLDivElement; }) { const dispatch = useAppDispatch(); - const { t } = useTranslation() + const { t } = useTranslation(); const ref = useRef(null); const { docId, controller } = useSubscribeDocument(); const handleInsert = useCallback( - async (type: BlockType, data?: BlockData) => { + async (type: BlockType, data?: BlockData) => { if (!controller) return; await dispatch( turnToBlockThunk({ @@ -245,7 +245,7 @@ function BlockSlashMenu({ e.preventDefault(); if (isEnter) { if (hoverOption) { - handleInsert(hoverOption.type, hoverOption.data); + void handleInsert(hoverOption.type, hoverOption.data); } return; @@ -282,9 +282,9 @@ function BlockSlashMenu({ ); const renderEmptyContent = useCallback(() => { - return
    - {t('findAndReplace.noResult')} -
    + return ( +
    {t('findAndReplace.noResult')}
    + ); }, [t]); return ( @@ -296,30 +296,32 @@ function BlockSlashMenu({ className={'flex h-[100%] max-h-[40vh] w-[324px] min-w-[180px] max-w-[calc(100vw-32px)] flex-col p-1'} >
    - {options.length === 0 ? renderEmptyContent(): Object.entries(optionsByGroup).map(([group, options]) => ( -
    -
    {group}
    -
    - {options.map((option) => { - return ( - { - onHoverOption(option); - }} - isHovered={hoverOption?.key === option.key} - onClick={() => { - handleInsert(option.type, option.data); - }} - /> - ); - })} -
    -
    - ))} + {options.length === 0 + ? renderEmptyContent() + : Object.entries(optionsByGroup).map(([group, options]) => ( +
    +
    {group}
    +
    + {options.map((option) => { + return ( + { + onHoverOption(option); + }} + isHovered={hoverOption?.key === option.key} + onClick={() => { + void handleInsert(option.type, option.data); + }} + /> + ); + })} +
    +
    + ))}
    ); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/CodeBlock/SelectLanguage.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/CodeBlock/SelectLanguage.tsx index f1a5132818ba..01a96028df7b 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/CodeBlock/SelectLanguage.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/CodeBlock/SelectLanguage.tsx @@ -17,7 +17,7 @@ function SelectLanguage({ id, language }: { id: string; language: string }) { if (!controller) return; const language = event.target.value; - dispatch( + void dispatch( updateNodeDataThunk({ id, controller, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/CodeBlock/useKeyDown.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/CodeBlock/useKeyDown.ts index 2a64f350deeb..a24413a69bc5 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/CodeBlock/useKeyDown.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/CodeBlock/useKeyDown.ts @@ -1,5 +1,5 @@ import isHotkey from 'is-hotkey'; -import { useCallback, useContext, useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { useAppDispatch } from '$app/stores/store'; import { Keyboard } from '$app/constants/document/keyboard'; import { useCommonKeyEvents } from '$app/components/document/_shared/EditorHooks/useCommonKeyEvents'; @@ -8,7 +8,7 @@ import { useSubscribeDocument } from '$app/components/document/_shared/Subscribe export function useKeyDown(id: string) { const dispatch = useAppDispatch(); - const { docId, controller } = useSubscribeDocument(); + const { controller } = useSubscribeDocument(); const commonKeyEvents = useCommonKeyEvents(id); const customEvents = useMemo(() => { @@ -22,7 +22,7 @@ export function useKeyDown(id: string) { handler: (e: React.KeyboardEvent) => { e.preventDefault(); if (!controller) return; - dispatch( + void dispatch( enterActionForBlockThunk({ id, controller, @@ -37,6 +37,7 @@ export function useKeyDown(id: string) { (e) => { e.stopPropagation(); const keyEvents = [...customEvents]; + keyEvents.forEach((keyEvent) => { // Here we check if the key event can be handled by the current key event if (keyEvent.canHandle(e)) { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/ColumnListBlock/Column.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/ColumnListBlock/Column.tsx deleted file mode 100644 index f88031e64fd1..000000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/ColumnListBlock/Column.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import NodeComponent from '$app/components/document/Node'; -import React from 'react'; - -export function ColumnBlock({ id, index, width }: { id: string; index: number; width: string }) { - const renderResizer = () => { - return ( -
    - ); - }; - - return ( - <> - {index === 0 ? ( -
    -
    - {renderResizer()} -
    -
    - ) : ( - renderResizer() - )} - - - - ); -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/ColumnListBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/ColumnListBlock/index.tsx deleted file mode 100644 index b0c406908bca..000000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/ColumnListBlock/index.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React, { useMemo } from 'react'; -import { Node } from '$app/interfaces/document'; -import { ColumnBlock } from './Column'; - -export default function ColumnListBlock({ - node, - childIds, -}: { - node: Node & { - data: Record; - }; - childIds?: string[]; -}) { - const resizerWidth = useMemo(() => { - return 46 * (node.children?.length || 0); - }, [node.children?.length]); - return ( - <> -
    - {childIds?.map((item, index) => ( - - ))} -
    - - ); -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentBanner/DocumentBanner.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentBanner/DocumentBanner.hooks.ts index 6c57f6ba5be7..200816463a09 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentBanner/DocumentBanner.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentBanner/DocumentBanner.hooks.ts @@ -6,6 +6,7 @@ import { ViewIconTypePB } from '@/services/backend'; import { CoverType } from '$app/interfaces/document'; import { updateNodeDataThunk } from '$app_reducers/document/async-actions'; import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode.hooks'; + export const heightCls = { cover: 'h-[220px]', icon: 'h-[80px]', @@ -22,7 +23,7 @@ export function useDocumentBanner(id: string) { const onUpdateIcon = useCallback( (icon: string) => { - dispatch( + void dispatch( updatePageIcon({ id: docId, icon: icon @@ -39,7 +40,7 @@ export function useDocumentBanner(id: string) { const onUpdateCover = useCallback( (coverType: CoverType | null, cover: string | null) => { - dispatch( + void dispatch( updateNodeDataThunk({ id, data: { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentBanner/cover/ChangeImages.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentBanner/cover/ChangeImages.tsx index 1ff91eaea904..0a3b9d42e73f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentBanner/cover/ChangeImages.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentBanner/cover/ChangeImages.tsx @@ -7,7 +7,7 @@ import { readCoverImageUrls, readImage, writeCoverImageUrls } from '$app/utils/d import { Log } from '$app/utils/log'; import { Image } from '$app/components/document/DocumentBanner/cover/GalleryItem'; -function ChangeImages({ cover, onChange }: { onChange: (url: string) => void; cover: string }) { +function ChangeImages({ onChange }: { onChange: (url: string) => void; cover: string }) { const { t } = useTranslation(); const [images, setImages] = useState([]); const loadImageUrls = useCallback(async () => { @@ -58,7 +58,7 @@ function ChangeImages({ cover, onChange }: { onChange: (url: string) => void; co }, [loadImageUrls]); useEffect(() => { - loadImageUrls(); + void loadImageUrls(); }, [loadImageUrls]); return ( diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentBanner/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentBanner/index.tsx index 036ee0e12755..f8d4c269ac86 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentBanner/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/DocumentBanner/index.tsx @@ -5,6 +5,7 @@ import DocumentIcon from './DocumentIcon'; function DocumentBanner({ id, hover }: { id: string; hover: boolean }) { const { onUpdateCover, node, onUpdateIcon, icon, cover, className, coverType } = useDocumentBanner(id); + return ( <>
    { - dispatch( + void dispatch( updateNodeDataThunk({ id, data: { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/ImageBlock/ImageToolbar.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/ImageBlock/ImageToolbar.tsx index c2175ec3f58e..c9fc838a815c 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/ImageBlock/ImageToolbar.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/ImageBlock/ImageToolbar.tsx @@ -27,7 +27,7 @@ function ImageToolbar({ id, open, align }: { id: string; open: boolean; align: A
    { - dispatch(deleteNodeThunk({ id, controller })); + void dispatch(deleteNodeThunk({ id, controller })); }} className='flex items-center justify-center p-1' > diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/ImageBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/ImageBlock/index.tsx index 9aecb525f506..b497989eb34e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/ImageBlock/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/ImageBlock/index.tsx @@ -20,7 +20,7 @@ function ImageBlock({ node }: { node: NestedBlock }) { ({ onClose }: { onClose: () => void }) => { const onSubmitUrl = (url: string) => { if (!url) return; - dispatch( + void dispatch( updateNodeDataThunk({ id, data: { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/ImageBlock/useImageBlock.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/ImageBlock/useImageBlock.ts index 80c0940f170f..f9b7c7da5dbe 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/ImageBlock/useImageBlock.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/ImageBlock/useImageBlock.ts @@ -47,7 +47,7 @@ export function useImageBlock(node: NestedBlock) { const updateWidth = useCallback( (width: number, height: number) => { - dispatch( + void dispatch( updateNodeDataThunk({ id: node.id, data: { @@ -93,7 +93,7 @@ export function useImageBlock(node: NestedBlock) { }); }; - const onResizeEnd = (e: MouseEvent) => { + const onResizeEnd = () => { setResizing(false); if (!startResizePoint.current) return; startResizePoint.current = undefined; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/NumberedListBlock/NumberedListBlock.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/NumberedListBlock/NumberedListBlock.hooks.ts index 535645655894..f9ff2c55ed93 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/NumberedListBlock/NumberedListBlock.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/NumberedListBlock/NumberedListBlock.hooks.ts @@ -10,16 +10,20 @@ export function useNumberedListBlock(node: NestedBlock { return nodes[id].type !== BlockType.NumberedListBlock; }); + if (lastIndex === -1) return prevNodeIds.length; return lastIndex; }); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/CustomColorPicker.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/CustomColorPicker.tsx index ed0e4e6cce2b..eee5f9c126a7 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/CustomColorPicker.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/CustomColorPicker.tsx @@ -46,7 +46,7 @@ function CustomColorPicker({ onClose={onClose} > { + onChange={(color) => { setColor(color.rgb); }} color={color} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/FormatButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/FormatButton.tsx index 8180ebc8681f..9d8412e5e309 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/FormatButton.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/FormatButton.tsx @@ -61,7 +61,7 @@ const FormatButton = ({ format, icon }: { format: TextAction; icon: string }) => const addTemporaryInput = useCallback( (type: TemporaryType) => { - dispatch(createTemporary({ type, docId })); + void dispatch(createTemporary({ type, docId })); }, [dispatch, docId] ); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/index.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/index.hooks.ts index a1098e99c203..315ccd333758 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/index.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextActionMenu/menu/index.hooks.ts @@ -36,6 +36,7 @@ export function useTextActionMenu() { return groups.map((group) => { return group.filter((item) => items.includes(item)); }); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [JSON.stringify(items), node]); return { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/useKeyDown.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/useKeyDown.ts index 931f81a3962a..0c49ef54e47e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/useKeyDown.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/useKeyDown.ts @@ -10,10 +10,9 @@ import { import { useTurnIntoBlockEvents } from './useTurnIntoBlockEvents'; import { useCommonKeyEvents } from '../_shared/EditorHooks/useCommonKeyEvents'; import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks'; -import { openMention } from '$app_reducers/document/async-actions/mention'; export function useKeyDown(id: string) { - const { controller, docId } = useSubscribeDocument(); + const { controller } = useSubscribeDocument(); const dispatch = useAppDispatch(); const turnIntoEvents = useTurnIntoBlockEvents(id); const commonKeyEvents = useCommonKeyEvents(id); @@ -34,9 +33,9 @@ export function useKeyDown(id: string) { canHandle: (e: React.KeyboardEvent) => { return isHotkey(Keyboard.keys.ENTER, e); }, - handler: (e: React.KeyboardEvent) => { + handler: () => { if (!controller) return; - dispatch( + void dispatch( enterActionForBlockThunk({ id, controller, @@ -58,9 +57,9 @@ export function useKeyDown(id: string) { canHandle: (e: React.KeyboardEvent) => { return isHotkey(Keyboard.keys.TAB, e); }, - handler: (e: React.KeyboardEvent) => { + handler: () => { if (!controller) return; - dispatch( + void dispatch( tabActionForBlockThunk({ id, controller, @@ -73,9 +72,9 @@ export function useKeyDown(id: string) { canHandle: (e: React.KeyboardEvent) => { return isHotkey(Keyboard.keys.SHIFT_TAB, e); }, - handler: (e: React.KeyboardEvent) => { + handler: () => { if (!controller) return; - dispatch( + void dispatch( shiftTabActionForBlockThunk({ id, controller, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/useTurnIntoBlockEvents.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/useTurnIntoBlockEvents.ts index bb4859017e3c..d06f17b1b0a6 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/useTurnIntoBlockEvents.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/TextBlock/useTurnIntoBlockEvents.ts @@ -6,7 +6,7 @@ import { blockConfig } from '$app/constants/document/config'; import Delta from 'quill-delta'; import { useRangeRef } from '$app/components/document/_shared/SubscribeSelection.hooks'; -import { getBlockDelta } from '$app/components/document/_shared/SubscribeNode.hooks'; +import { getBlockDelta } from '$app/components/document/_shared/SubscribeNode.hooks'; import { getDeltaText } from '$app/utils/document/delta'; import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks'; import { turnIntoConfig } from './shortchut'; @@ -155,7 +155,7 @@ export function useTurnIntoBlockEvents(id: string) { const data = getData(); if (!data) return; - dispatch(turnToBlockThunk({ id, data, type: blockType, controller })); + void dispatch(turnToBlockThunk({ id, data, type: blockType, controller })); }, }; }); @@ -167,9 +167,8 @@ export function useTurnIntoBlockEvents(id: string) { handler: (e: React.KeyboardEvent) => { e.preventDefault(); if (!controller) return; - const delta = getDeltaContent(); - dispatch( + void dispatch( turnToBlockThunk({ id, controller, @@ -186,7 +185,7 @@ export function useTurnIntoBlockEvents(id: string) { if (!controller) return; const defaultData = blockConfig[BlockType.CodeBlock].defaultData; - dispatch( + void dispatch( turnToBlockThunk({ id, data: { @@ -208,10 +207,11 @@ export function useTurnIntoBlockEvents(id: string) { formula, }; - dispatch(turnToBlockThunk({ id, data, type: BlockType.EquationBlock, controller })); + void dispatch(turnToBlockThunk({ id, data, type: BlockType.EquationBlock, controller })); }, - } + }, ]; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [canHandle, controller, dispatch, getAttrs, getDeltaContent, id, spaceTriggerMap]); return turnIntoBlockEvents; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/ToggleListBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/ToggleListBlock/index.tsx index 9c3b980bdf0a..54387fe9226c 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/ToggleListBlock/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/ToggleListBlock/index.tsx @@ -3,13 +3,13 @@ import { BlockType, NestedBlock } from '$app/interfaces/document'; import TextBlock from '$app/components/document/TextBlock'; import NodeChildren from '$app/components/document/Node/NodeChildren'; import { useToggleListBlock } from '$app/components/document/ToggleListBlock/ToggleListBlock.hooks'; -import { IconButton } from '@mui/material'; import { DropDownShowSvg } from '$app/components/_shared/svg/DropDownShowSvg'; import Button from '@mui/material/Button'; function ToggleListBlock({ node, childIds }: { node: NestedBlock; childIds?: string[] }) { const { toggleCollapsed, handleShortcut } = useToggleListBlock(node.id, node.data); const collapsed = node.data.collapsed; + return ( <>
    diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/BlockPopover/BlockPopover.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/BlockPopover/BlockPopover.hooks.tsx index 81c9cd0e4c5c..a86f5e8e9e6e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/BlockPopover/BlockPopover.hooks.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/BlockPopover/BlockPopover.hooks.tsx @@ -42,7 +42,7 @@ export function useBlockPopover({ }, [dispatch, docId, id, onAfterClose]); const selectBlock = useCallback(() => { - dispatch( + void dispatch( setRectSelectionThunk({ docId, selection: [id], diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/CopyPasteHooks/useCopy.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/CopyPasteHooks/useCopy.ts index 2c31f9ab95ce..0a34e4971ca1 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/CopyPasteHooks/useCopy.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/CopyPasteHooks/useCopy.ts @@ -19,7 +19,8 @@ export function useCopy(container: HTMLDivElement) { e.clipboardData?.setData(clipboardTypes.TEXT, data.text); e.clipboardData?.setData(clipboardTypes.HTML, data.html); }; - dispatch( + + void dispatch( copyThunk({ setClipboardData, controller, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/CopyPasteHooks/usePaste.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/CopyPasteHooks/usePaste.ts index a2cc09d643f8..6ed3c9fd6bbf 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/CopyPasteHooks/usePaste.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/CopyPasteHooks/usePaste.ts @@ -1,4 +1,4 @@ -import { useCallback, useContext, useEffect } from 'react'; +import { useCallback, useEffect } from 'react'; import { useAppDispatch } from '$app/stores/store'; import { pasteThunk } from '$app_reducers/document/async-actions/copy_paste'; import { clipboardTypes } from '$app/constants/document/copy_paste'; @@ -12,7 +12,7 @@ export function usePaste(container: HTMLDivElement) { if (!controller) return; e.stopPropagation(); e.preventDefault(); - dispatch( + void dispatch( pasteThunk({ controller, data: { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/EditorHooks/useCommonKeyEvents.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/EditorHooks/useCommonKeyEvents.ts index 84a95e5bd9bc..e7da44baf886 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/EditorHooks/useCommonKeyEvents.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/EditorHooks/useCommonKeyEvents.ts @@ -12,8 +12,6 @@ import { useAppDispatch } from '$app/stores/store'; import { isFormatHotkey, parseFormat } from '$app/utils/document/format'; import { toggleFormatThunk } from '$app_reducers/document/async-actions/format'; import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks'; -import { getBlock } from '$app/components/document/_shared/SubscribeNode.hooks'; -import Delta from 'quill-delta'; export function useCommonKeyEvents(id: string) { const { focused, caretRef } = useFocused(id); @@ -35,7 +33,7 @@ export function useCommonKeyEvents(id: string) { handler: (e: React.KeyboardEvent) => { e.preventDefault(); if (!controller) return; - dispatch(backspaceDeleteActionForBlockThunk({ id, controller })); + void dispatch(backspaceDeleteActionForBlockThunk({ id, controller })); }, }, { @@ -45,7 +43,7 @@ export function useCommonKeyEvents(id: string) { }, handler: (e: React.KeyboardEvent) => { e.preventDefault(); - dispatch(upDownActionForBlockThunk({ docId, id })); + void dispatch(upDownActionForBlockThunk({ docId, id })); }, }, { @@ -55,7 +53,7 @@ export function useCommonKeyEvents(id: string) { }, handler: (e: React.KeyboardEvent) => { e.preventDefault(); - dispatch(upDownActionForBlockThunk({ docId, id, down: true })); + void dispatch(upDownActionForBlockThunk({ docId, id, down: true })); }, }, { @@ -66,7 +64,7 @@ export function useCommonKeyEvents(id: string) { handler: (e: React.KeyboardEvent) => { e.preventDefault(); e.stopPropagation(); - dispatch(leftActionForBlockThunk({ docId, id })); + void dispatch(leftActionForBlockThunk({ docId, id })); }, }, { @@ -76,7 +74,7 @@ export function useCommonKeyEvents(id: string) { }, handler: (e: React.KeyboardEvent) => { e.preventDefault(); - dispatch(rightActionForBlockThunk({ docId, id })); + void dispatch(rightActionForBlockThunk({ docId, id })); }, }, { @@ -87,7 +85,7 @@ export function useCommonKeyEvents(id: string) { const format = parseFormat(e); if (!format) return; - dispatch( + void dispatch( toggleFormatThunk({ format, controller, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/EditorHooks/useDelta.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/EditorHooks/useDelta.ts index 98baf3012197..9d690d9b0a60 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/EditorHooks/useDelta.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/EditorHooks/useDelta.ts @@ -1,5 +1,5 @@ import { useSubscribeNode } from '$app/components/document/_shared/SubscribeNode.hooks'; -import { useCallback, useContext, useEffect, useMemo, useRef } from 'react'; +import { useCallback, useEffect, useMemo, useRef } from 'react'; import { useAppDispatch } from '$app/stores/store'; import { updateNodeDeltaThunk } from '$app_reducers/document/async-actions'; import Delta, { Op } from 'quill-delta'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/EditorHooks/useSelection.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/EditorHooks/useSelection.ts index 92517f57207f..563241fec383 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/EditorHooks/useSelection.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/EditorHooks/useSelection.ts @@ -19,8 +19,8 @@ export function useSelection(id: string) { const { docId } = useSubscribeDocument(); const storeRange = useCallback( - (range: RangeStatic) => { - dispatch(storeRangeThunk({ id, range, docId })); + async (range: RangeStatic) => { + await dispatch(storeRangeThunk({ id, range, docId })); }, [docId, id, dispatch] ); @@ -38,7 +38,7 @@ export function useSelection(id: string) { }, }) ); - storeRange(range); + void storeRange(range); }, [docId, id, dispatch, storeRange] ); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/InlineBlock/CodeInline.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/InlineBlock/CodeInline.tsx index 4f2c031602f2..f28892ab22cd 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/InlineBlock/CodeInline.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/InlineBlock/CodeInline.tsx @@ -1,6 +1,6 @@ import React from 'react'; -function CodeInline({ text, children, selected }: { text: string; children: React.ReactNode; selected: boolean }) { +function CodeInline({ children, selected }: { text: string; children: React.ReactNode; selected: boolean }) { return ( { + async (node: HTMLSpanElement) => { const selection = getSelection(node); if (!selection) return; - dispatch( + await dispatch( createTemporary({ docId, state: { @@ -57,7 +57,7 @@ function FormulaInline({ getSelection={getSelection} isFirst={isFirst} isLast={isLast} - renderNode={() => } + renderNode={() => } > {children} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/InlineBlock/LinkInline.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/InlineBlock/LinkInline.tsx index 070ec257f077..ebb5c0f4c2c7 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/InlineBlock/LinkInline.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/InlineBlock/LinkInline.tsx @@ -26,17 +26,18 @@ function LinkInline({ const dispatch = useAppDispatch(); const onClick = useCallback( - (e: React.MouseEvent) => { + async (e: React.MouseEvent) => { if (!ref.current) return; const selection = getSelection(ref.current); if (!selection) return; const rect = ref.current?.getBoundingClientRect(); + if (!rect) return; e.stopPropagation(); e.preventDefault(); - dispatch( + await dispatch( createTemporary({ docId, state: { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/InlineBlock/PageInline.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/InlineBlock/PageInline.tsx index a97726adedf4..6786ba838956 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/InlineBlock/PageInline.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/InlineBlock/PageInline.tsx @@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { pageTypeMap } from '$app/constants'; import { LinearProgress } from '@mui/material'; -import Tooltip from "@mui/material/Tooltip"; +import Tooltip from '@mui/material/Tooltip'; function PageInline({ pageId }: { pageId: string }) { const { t } = useTranslation(); @@ -18,12 +18,14 @@ function PageInline({ pageId }: { pageId: string }) { const controller = new PageController(id); const page = await controller.getPage(); + setCurrentPage(page); }, []); const navigateToPage = useCallback( (page: Page) => { const pageType = pageTypeMap[page.layout]; + navigate(`/page/${pageType}/${page.id}`); }, [navigate] @@ -31,14 +33,12 @@ function PageInline({ pageId }: { pageId: string }) { useEffect(() => { if (!page) { - loadPage(pageId); + void loadPage(pageId); } else { setCurrentPage(page); } - }, [page, loadPage, pageId]); - return currentPage ? ( - {currentPage.icon?.value ||
    } - {currentPage.name || t('menuAppHeader.defaultNewPageName')} - + {currentPage.icon?.value ||
    } + {currentPage.name || t('menuAppHeader.defaultNewPageName')} + - ) : ( diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SlateEditor/TextLeaf.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SlateEditor/TextLeaf.tsx index 83bd7fe680b9..6e2e87bb6429 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SlateEditor/TextLeaf.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SlateEditor/TextLeaf.tsx @@ -92,6 +92,7 @@ const TextLeaf = (props: TextLeafProps) => { } const mention = leaf.mention; + if (mention && mention.type === MentionType.PAGE && leaf.text) { newChildren = ( 0; + // eslint-disable-next-line @typescript-eslint/no-unused-vars const [_, path] = editor.node(currentSelection); if (removeMark) { @@ -113,9 +113,10 @@ export function useEditor({ const decorate = useCallback( (entry: NodeEntry) => { - const [node, path] = entry; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [_, path] = entry; - const ranges: Range[] = [ + return [ getDecorateRange(path, decorateSelection, { selection_high_lighted: true, }), @@ -123,8 +124,6 @@ export function useEditor({ temporary: true, }), ].filter((range) => range !== null) as Range[]; - - return ranges; }, [temporarySelection, decorateSelection, getDecorateRange] ); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SlateEditor/useSlateYjs.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SlateEditor/useSlateYjs.ts index f377567d5277..ba4669db6cc6 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SlateEditor/useSlateYjs.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SlateEditor/useSlateYjs.ts @@ -21,6 +21,7 @@ export function useSlateYjs({ delta, onChange }: { delta?: Delta; onChange: (ops // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + // eslint-disable-next-line react-hooks/exhaustive-deps const editor = useMemo(() => withYjs(withMarkdown(withReact(createEditor())), sharedType), []); // Connect editor in useEffect to comply with concurrent mode requirements. diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeMention.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeMention.hooks.ts index 17d103a30d6f..2043209b409d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeMention.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeMention.hooks.ts @@ -7,6 +7,7 @@ const initialState: MentionState = { open: false, blockId: '', }; + export function useSubscribeMentionState() { const { docId } = useSubscribeDocument(); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts index c687177c6dda..0efefa462137 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeNode.hooks.ts @@ -18,14 +18,17 @@ export function useSubscribeNode(id: string) { }>((state) => { const documentState = state[DOCUMENT_NAME][docId]; const node = documentState?.nodes[id]; + // if node is root, return page name if (!node?.parent) { const delta = state.pages?.pageMap[docId]?.name; + return { node, delta: delta ? JSON.stringify(new Delta().insert(delta)) : '', }; } + const externalId = node?.externalId; return { @@ -51,7 +54,9 @@ export function useSubscribeNode(id: string) { // Memoize the node and its children // So that the component will not be re-rendered when other node is changed // It very important for performance + // eslint-disable-next-line react-hooks/exhaustive-deps const memoizedNode = useMemo(() => node, [JSON.stringify(node)]); + // eslint-disable-next-line react-hooks/exhaustive-deps const memoizedChildIds = useMemo(() => childIds, [JSON.stringify(childIds)]); return { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeSelection.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeSelection.hooks.ts index 649d33bc38fe..fa1ee30fd124 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeSelection.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/SubscribeSelection.hooks.ts @@ -2,7 +2,7 @@ import { useAppSelector } from '$app/stores/store'; import { RangeState, RangeStatic } from '$app/interfaces/document'; import { useMemo, useRef } from 'react'; import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks'; -import { RANGE_NAME, TEMPORARY_NAME, TEXT_LINK_NAME } from '$app/constants/document/name'; +import { RANGE_NAME, TEMPORARY_NAME } from '$app/constants/document/name'; export function useSubscribeDecorate(id: string) { const { docId } = useSubscribeDocument(); @@ -51,7 +51,7 @@ export function useFocused(id: string) { } export function useRangeRef() { - const { docId, controller } = useSubscribeDocument(); + const { docId } = useSubscribeDocument(); const rangeRef = useRef(); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TemporaryInput/TemporaryLink.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TemporaryInput/TemporaryLink.tsx index 117f787b1a38..f16352a31be8 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TemporaryInput/TemporaryLink.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TemporaryInput/TemporaryLink.tsx @@ -2,8 +2,9 @@ import React from 'react'; import { AddLinkOutlined } from '@mui/icons-material'; import { useTranslation } from 'react-i18next'; -function TemporaryLink({ href = '', text = '' }: { href?: string; text?: string }) { +function TemporaryLink({ text = '' }: { href?: string; text?: string }) { const { t } = useTranslation(); + return ( {text ? ( diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TemporaryInput/TemporaryPopover.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TemporaryInput/TemporaryPopover.tsx index 6f27a871bf15..ec53767774b9 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TemporaryInput/TemporaryPopover.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TemporaryInput/TemporaryPopover.tsx @@ -17,7 +17,6 @@ function TemporaryPopover() { const anchorPosition = useMemo(() => temporaryState?.popoverPosition, [temporaryState]); const open = Boolean(anchorPosition); const id = temporaryState?.id; - const type = temporaryState?.type; const dispatch = useAppDispatch(); const { docId, controller } = useSubscribeDocument(); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TemporaryInput/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TemporaryInput/index.tsx index 4afe2ae2c651..4e9460dbf3d1 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TemporaryInput/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TemporaryInput/index.tsx @@ -77,6 +77,7 @@ function TemporaryInput({ useEffect(() => { const match = getMatch(); + setMatch(match); }, [getMatch]); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TurnInto/TurnInto.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TurnInto/TurnInto.hooks.ts index edbda073334d..1f1df71304b1 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TurnInto/TurnInto.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TurnInto/TurnInto.hooks.ts @@ -12,7 +12,7 @@ export function useTurnInto({ node, onClose }: { node: NestedBlock; onClose?: () const { controller, docId } = useSubscribeDocument(); const turnIntoBlock = useCallback( - async (type: BlockType, isSelected: boolean, data?: BlockData) => { + async (type: BlockType, isSelected: boolean, data?: BlockData) => { if (!controller || isSelected) { onClose?.(); return; @@ -35,7 +35,7 @@ export function useTurnInto({ node, onClose }: { node: NestedBlock; onClose?: () ); onClose?.(); - dispatch( + await dispatch( setRectSelectionThunk({ docId, selection: [newBlockId as string], diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TurnInto/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TurnInto/index.tsx index eb3661fb5119..b1388ff6ce40 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TurnInto/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/TurnInto/index.tsx @@ -57,7 +57,7 @@ const TurnIntoPopover = ({ icon: , selected: node?.data?.level === 1, onClick: (type: BlockType, isSelected: boolean) => { - turnIntoHeading(1, isSelected); + void turnIntoHeading(1, isSelected); }, }, { @@ -67,7 +67,7 @@ const TurnIntoPopover = ({ icon: <Title />, selected: node?.data?.level === 2, onClick: (type: BlockType, isSelected: boolean) => { - turnIntoHeading(2, isSelected); + void turnIntoHeading(2, isSelected); }, }, { @@ -77,7 +77,7 @@ const TurnIntoPopover = ({ icon: <Title />, selected: node?.data?.level === 3, onClick: (type: BlockType, isSelected: boolean) => { - turnIntoHeading(3, isSelected); + void turnIntoHeading(3, isSelected); }, }, { @@ -143,7 +143,7 @@ const TurnIntoPopover = ({ (option: Option) => { const isSelected = getSelected(option); - option.onClick ? option.onClick(option.type, isSelected) : turnIntoBlock(option.type, isSelected); + option.onClick ? option.onClick(option.type, isSelected) : void turnIntoBlock(option.type, isSelected); onOk?.(); }, [onOk, getSelected, turnIntoBlock] diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/UndoHooks/useUndoRedo.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/UndoHooks/useUndoRedo.ts index f9f35799637c..3a101cac22b1 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/UndoHooks/useUndoRedo.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/UndoHooks/useUndoRedo.ts @@ -6,25 +6,26 @@ import { useSubscribeDocument } from '$app/components/document/_shared/Subscribe export function useUndoRedo(container: HTMLDivElement) { const { controller } = useSubscribeDocument(); - const onUndo = useCallback(() => { + const onUndo = useCallback(async () => { if (!controller) return; - controller.undo(); + await controller.undo(); }, [controller]); - const onRedo = useCallback(() => { + const onRedo = useCallback(async () => { if (!controller) return; - controller.redo(); + await controller.redo(); }, [controller]); const handleKeyDownCapture = useCallback( - (e: KeyboardEvent) => { + async (e: KeyboardEvent) => { if (isHotkey(Keyboard.keys.UNDO, e)) { e.stopPropagation(); - onUndo(); + await onUndo(); } + if (isHotkey(Keyboard.keys.REDO, e)) { e.stopPropagation(); - onRedo(); + await onRedo(); } }, [onRedo, onUndo] diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/UploadImage/ImageEditPopover.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/UploadImage/ImageEditPopover.tsx index af31ae70c503..e341563d579d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/UploadImage/ImageEditPopover.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/UploadImage/ImageEditPopover.tsx @@ -1,7 +1,6 @@ import React from 'react'; import Popover, { PopoverProps } from '@mui/material/Popover'; import ImageEdit from './ImageEdit'; -import { PopoverOrigin } from '@mui/material/Popover/Popover'; interface Props extends PopoverProps { onSubmitUrl: (url: string) => void; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/UploadImage/TabPanel.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/UploadImage/TabPanel.tsx index 1f0f6ab4ef4c..9356a246aa5f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/UploadImage/TabPanel.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/UploadImage/TabPanel.tsx @@ -1,4 +1,5 @@ import React from 'react'; + export enum TAB_KEYS { UPLOAD = 'upload', LINK = 'link', diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/UploadImage/UploadImage.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/UploadImage/UploadImage.tsx index ecb14d7d2f8d..5990f5b5b959 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/UploadImage/UploadImage.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/UploadImage/UploadImage.tsx @@ -42,6 +42,7 @@ function UploadImage({ onChange }: UploadImageProps) { duration: 3000, type: 'error', }); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [error]); const handleUpload = useCallback( @@ -74,7 +75,7 @@ function UploadImage({ onChange }: UploadImageProps) { if (!files || files.length === 0) return; const file = files[0]; - handleUpload(file); + void handleUpload(file); }, [handleUpload] ); @@ -87,7 +88,7 @@ function UploadImage({ onChange }: UploadImageProps) { if (!files || files.length === 0) return; const file = files[0]; - handleUpload(file); + void handleUpload(file); }, [handleUpload] ); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/useBindArrowKey.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/useBindArrowKey.ts index 8d2ac27796c1..29aebe52e909 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/useBindArrowKey.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/useBindArrowKey.ts @@ -83,6 +83,7 @@ export const useBindArrowKey = ({ } else { document.removeEventListener('keydown', handleArrowKey, true); } + return () => { document.removeEventListener('keydown', handleArrowKey, true); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/usePanelSearchText.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/usePanelSearchText.ts index 245041f702b9..034919e83875 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/usePanelSearchText.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/usePanelSearchText.ts @@ -25,9 +25,11 @@ export function useSubscribePanelSearchText({ blockId, open }: { blockId: string beforeOpenDeltaRef.current = []; return; } + if (beforeOpenDeltaRef.current.length > 0) return; const delta = new Delta(JSON.parse(deltaStr || "{}")); + beforeOpenDeltaRef.current = delta.ops; handleSearch(delta); }, [deltaStr, handleSearch, open]); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/error/Error.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/error/Error.hooks.ts index 2da03c5f5ccf..ceaa5a51a00a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/error/Error.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/error/Error.hooks.ts @@ -1,6 +1,6 @@ -import { useAppDispatch, useAppSelector } from '../../stores/store'; -import { errorActions } from '../../stores/reducers/error/slice'; -import { useEffect, useState } from 'react'; +import { useAppDispatch, useAppSelector } from '$app/stores/store'; +import { errorActions } from '$app_reducers/error/slice'; +import { useCallback, useEffect, useState } from 'react'; export const useError = (e: Error) => { const dispatch = useAppDispatch(); @@ -13,15 +13,18 @@ export const useError = (e: Error) => { setErrorMessage(error.message); }, [error]); + const showError = useCallback( + (msg: string) => { + dispatch(errorActions.showError(msg)); + }, + [dispatch] + ); + useEffect(() => { if (e) { showError(e.message); } - }, [e]); - - const showError = (msg: string) => { - dispatch(errorActions.showError(msg)); - }; + }, [e, showError]); const hideError = () => { dispatch(errorActions.hideError()); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/grid/GridTableCount/GridTableCount.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/grid/GridTableCount/GridTableCount.tsx index 727f831a8166..51ffd11186ca 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/grid/GridTableCount/GridTableCount.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/grid/GridTableCount/GridTableCount.tsx @@ -2,6 +2,7 @@ import { RowInfo } from '@/appflowy_app/stores/effects/database/row/row_cache'; export const GridTableCount = ({ rows }: { rows: readonly RowInfo[] }) => { const count = rows.length; + return ( <span> Count : <span className='font-semibold'>{count}</span> diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/grid/GridTableHeader/GridTableHeaderItem.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/grid/GridTableHeader/GridTableHeaderItem.tsx index c4b85be45b4c..194709d40cf9 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/grid/GridTableHeader/GridTableHeaderItem.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/grid/GridTableHeader/GridTableHeaderItem.tsx @@ -53,7 +53,8 @@ export const GridTableHeaderItem = ({ if (newSizeX >= MIN_COLUMN_WIDTH) { dispatch(databaseActions.changeWidth({ fieldId: field.fieldId, width: newSizeX })); } - }, [newSizeX]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [newSizeX, dispatch]); const changeFieldType = async (newType: FieldType) => { if (!editingField) return; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/Breadcrumb/Breadcrumb.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/Breadcrumb/Breadcrumb.hooks.ts index 2288657d7b8a..7063cea877e1 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/Breadcrumb/Breadcrumb.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/Breadcrumb/Breadcrumb.hooks.ts @@ -1,9 +1,9 @@ -import { useAppSelector } from "$app/stores/store"; +import { useAppSelector } from '$app/stores/store'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useParams, useLocation } from 'react-router-dom'; import { Page } from '$app_reducers/pages/slice'; import { useTranslation } from 'react-i18next'; -import { PageController } from "$app/stores/effects/workspace/page/page_controller"; +import { PageController } from '$app/stores/effects/workspace/page/page_controller'; export function useLoadExpandedPages() { const { t } = useTranslation(); @@ -25,6 +25,7 @@ export function useLoadExpandedPages() { async (pageId: string) => { let page = pageMap[pageId]; const controller = new PageController(pageId); + if (!page) { try { page = await controller.getPage(); @@ -36,22 +37,22 @@ export function useLoadExpandedPages() { return; } } - setPagePath(prev => { - return [ - page, - ...prev - ] + + setPagePath((prev) => { + return [page, ...prev]; }); await loadPagePath(page.parentId); - - }, [pageMap]); + }, + [pageMap] + ); useEffect(() => { setPagePath([]); if (!currentPageId) { return; } - loadPagePath(currentPageId); + + void loadPagePath(currentPageId); // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentPageId]); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NestedPage/NestedPage.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NestedPage/NestedPage.hooks.ts index 7acd56508752..428c77193b8c 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NestedPage/NestedPage.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NestedPage/NestedPage.hooks.ts @@ -36,30 +36,30 @@ export function useLoadChildPages(pageId: string) { [dispatch] ); + const loadPageChildren = useCallback( + async (pageId: string) => { + const childPages = await controller.getChildPages(); - const loadPageChildren = useCallback(async (pageId: string) => { - const childPages = await controller.getChildPages(); - - dispatch( - pagesActions.addChildPages({ - id: pageId, - childPages, - }) - ); - - }, [controller, dispatch]); - + dispatch( + pagesActions.addChildPages({ + id: pageId, + childPages, + }) + ); + }, + [controller, dispatch] + ); useEffect(() => { void loadPageChildren(pageId); }, [loadPageChildren, pageId]); useEffect(() => { - controller.subscribe({ + void controller.subscribe({ onPageChanged, }); return () => { - controller.dispose(); + void controller.dispose(); }; }, [controller, onPageChanged]); @@ -88,7 +88,7 @@ export function usePageActions(pageId: string) { async (layout: ViewLayoutPB) => { const newViewId = await controller.createPage({ layout, - name: "" + name: '', }); dispatch(pagesActions.expandPage(pageId)); @@ -116,7 +116,7 @@ export function usePageActions(pageId: string) { useEffect(() => { return () => { - controller.dispose(); + void controller.dispose(); }; }, [controller]); @@ -134,4 +134,3 @@ export function useSelectedPage(pageId: string) { return id === pageId; } - diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/Share/Share.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/Share/Share.hooks.ts index 8cce47a2abd8..b281706848d0 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/Share/Share.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/Share/Share.hooks.ts @@ -1,4 +1,4 @@ -import { useLocation, useParams } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; export function useShareConfig() { const params = useParams(); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/TopBar/MoreButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/TopBar/MoreButton.tsx index bf2d48d8273d..ca104218ab70 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/TopBar/MoreButton.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/TopBar/MoreButton.tsx @@ -1,8 +1,7 @@ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { Drawer, IconButton } from '@mui/material'; import { Details2Svg } from '$app/components/_shared/svg/Details2Svg'; -import { LogoutOutlined } from '@mui/icons-material'; import Tooltip from '@mui/material/Tooltip'; import MoreOptions from '$app/components/layout/TopBar/MoreOptions'; import { useMoreOptionsConfig } from '$app/components/layout/TopBar/MoreOptions.hooks'; @@ -19,7 +18,7 @@ function MoreButton() { return ( <> <Tooltip placement={'bottom-end'} title={t('moreAction.moreOptions')}> - <IconButton onClick={(e) => toggleDrawer(true)} className={'h-8 w-8 text-icon-primary'}> + <IconButton onClick={() => toggleDrawer(true)} className={'h-8 w-8 text-icon-primary'}> <Details2Svg /> </IconButton> </Tooltip> diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/TopBar/MoreOptions.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/TopBar/MoreOptions.hooks.ts index 57b10a4fede3..63f1173885ed 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/TopBar/MoreOptions.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/TopBar/MoreOptions.hooks.ts @@ -4,7 +4,8 @@ import { useMemo } from 'react'; export function useMoreOptionsConfig() { const location = useLocation(); - const { type, pageType, id } = useMemo(() => { + const { type, pageType } = useMemo(() => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars const [_, type, pageType, id] = location.pathname.split('/'); return { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/TopBar/MoreOptions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/TopBar/MoreOptions.tsx index 97a48128f7f4..28f2b467599a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/TopBar/MoreOptions.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/TopBar/MoreOptions.tsx @@ -1,12 +1,8 @@ import React from 'react'; -import { useTranslation } from 'react-i18next'; import FontSizeConfig from '$app/components/layout/TopBar/FontSizeConfig'; -import { Divider } from '@mui/material'; -import { useLocation } from 'react-router-dom'; import { useMoreOptionsConfig } from '$app/components/layout/TopBar/MoreOptions.hooks'; function MoreOptions() { - const { t } = useTranslation(); const { showStyleOptions } = useMoreOptionsConfig(); return <div className={'flex w-[220px] flex-col'}>{showStyleOptions && <FontSizeConfig />}</div>; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/UserSetting/LanguageSetting.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/UserSetting/LanguageSetting.tsx index b2395d7645c4..bcd171f4c011 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/UserSetting/LanguageSetting.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/UserSetting/LanguageSetting.tsx @@ -56,7 +56,7 @@ function LanguageSetting({ onChange({ language, }); - i18n.changeLanguage(language); + void i18n.changeLanguage(language); }} > {languages.map((option) => ( diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/UserSetting/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/UserSetting/index.tsx index 5b4ec98294af..9fa5beabcfcb 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/UserSetting/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/UserSetting/index.tsx @@ -30,7 +30,7 @@ function UserSettings({ open, onClose }: { open: boolean; onClose: () => void }) if (userSettingController) { const language = newSetting.language || 'en'; - userSettingController.setAppearanceSetting({ + void userSettingController.setAppearanceSetting({ theme: newSetting.theme || Theme.Default, theme_mode: newSetting.themeMode || ThemeModePB.Light, locale: { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/WorkspaceManager/NewPageButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/WorkspaceManager/NewPageButton.tsx index e2346fa7a9f5..ad405024a14d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/WorkspaceManager/NewPageButton.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/WorkspaceManager/NewPageButton.tsx @@ -12,7 +12,7 @@ function NewPageButton({ workspaceId }: { workspaceId: string }) { useEffect(() => { return () => { - controller.dispose(); + void controller.dispose(); }; }, [controller]); @@ -21,7 +21,7 @@ function NewPageButton({ workspaceId }: { workspaceId: string }) { <button onClick={async () => { const { id } = await controller.createView({ - name: "", + name: '', layout: ViewLayoutPB.Document, parent_view_id: workspaceId, }); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/WorkspaceManager/Workspace.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/WorkspaceManager/Workspace.hooks.ts index a6e00bbbfe2e..e04bd734a014 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/WorkspaceManager/Workspace.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/WorkspaceManager/Workspace.hooks.ts @@ -45,7 +45,7 @@ export function useLoadWorkspaces() { })(); return () => { - controller.dispose(); + void controller.dispose(); }; }, [controller, initializeWorkspaces, subscribeToWorkspaces]); @@ -85,6 +85,7 @@ export function useLoadWorkspace(workspace: WorkspaceItem) { const initializeWorkspace = useCallback(async () => { const childPages = await controller.getChildPages(); + dispatch( pagesActions.addChildPages({ id, @@ -106,7 +107,7 @@ export function useLoadWorkspace(workspace: WorkspaceItem) { })(); return () => { - controller.dispose(); + void controller.dispose(); }; }, [controller, initializeWorkspace, subscribeToWorkspace]); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/WorkspaceManager/Workspace.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/WorkspaceManager/Workspace.tsx index bafea58b3add..b30002777daf 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/WorkspaceManager/Workspace.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/WorkspaceManager/Workspace.tsx @@ -1,12 +1,8 @@ import React from 'react'; import { WorkspaceItem } from '$app_reducers/workspace/slice'; import NestedViews from '$app/components/layout/WorkspaceManager/NestedPages'; -import { useLoadWorkspace } from '$app/components/layout/WorkspaceManager/Workspace.hooks'; -import WorkspaceTitle from '$app/components/layout/WorkspaceManager/WorkspaceTitle'; function Workspace({ workspace, opened }: { workspace: WorkspaceItem; opened: boolean }) { - const { openWorkspace, deleteWorkspace } = useLoadWorkspace(workspace); - return ( <div className={'flex h-[100%] flex-col'}> <div diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/tests/DatabaseTestHelper.ts b/frontend/appflowy_tauri/src/appflowy_app/components/tests/DatabaseTestHelper.ts index 98a91855a49c..eb5ea8a9094f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/tests/DatabaseTestHelper.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/tests/DatabaseTestHelper.ts @@ -1,32 +1,23 @@ -import { - FieldType, - FlowyError, - SingleSelectTypeOptionPB, - ViewLayoutPB, - ViewPB, - WorkspaceSettingPB, -} from '../../../services/backend'; -import { DatabaseController } from '../../stores/effects/database/database_controller'; -import { RowInfo } from '../../stores/effects/database/row/row_cache'; -import { RowController } from '../../stores/effects/database/row/row_controller'; +import { FieldType, SingleSelectTypeOptionPB, ViewLayoutPB, ViewPB, WorkspaceSettingPB } from '@/services/backend'; +import { DatabaseController } from '$app/stores/effects/database/database_controller'; +import { RowInfo } from '$app/stores/effects/database/row/row_cache'; +import { RowController } from '$app/stores/effects/database/row/row_controller'; import { CellControllerBuilder, CheckboxCellController, DateCellController, - NumberCellController, SelectOptionCellController, TextCellController, URLCellController, -} from '../../stores/effects/database/cell/controller_builder'; -import { None, Ok, Option, Result, Some } from 'ts-results'; -import { TypeOptionBackendService } from '../../stores/effects/database/field/type_option/type_option_bd_svc'; -import { DatabaseBackendService } from '../../stores/effects/database/database_bd_svc'; -import { FieldInfo } from '../../stores/effects/database/field/field_controller'; -import { TypeOptionController } from '../../stores/effects/database/field/type_option/type_option_controller'; -import { makeSingleSelectTypeOptionContext } from '../../stores/effects/database/field/type_option/type_option_context'; -import { SelectOptionBackendService } from '../../stores/effects/database/cell/select_option_bd_svc'; -import { Log } from '$app/utils/log'; -import { WorkspaceController } from '../../stores/effects/workspace/workspace_controller'; +} from '$app/stores/effects/database/cell/controller_builder'; +import { None, Option, Some } from 'ts-results'; +import { TypeOptionBackendService } from '$app/stores/effects/database/field/type_option/type_option_bd_svc'; +import { DatabaseBackendService } from '$app/stores/effects/database/database_bd_svc'; +import { FieldInfo } from '$app/stores/effects/database/field/field_controller'; +import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller'; +import { makeSingleSelectTypeOptionContext } from '$app/stores/effects/database/field/type_option/type_option_context'; +import { SelectOptionBackendService } from '$app/stores/effects/database/cell/select_option_bd_svc'; +import { WorkspaceController } from '$app/stores/effects/workspace/workspace_controller'; import { FolderEventGetCurrentWorkspaceSetting } from '@/services/backend/events/flowy-folder2'; // Create a database page for specific layout type @@ -36,9 +27,8 @@ export async function createTestDatabaseView(layout: ViewLayoutPB): Promise<View result.unwrap() ); const wsSvc = new WorkspaceController(workspaceSetting.workspace_id); - const viewRes = await wsSvc.createView({ name: 'New Grid', layout }); - return viewRes; + return await wsSvc.createView({ name: 'New Grid', layout }); } export async function openTestDatabase(viewId: string): Promise<DatabaseController> { @@ -92,18 +82,6 @@ export async function makeTextCellController( return Some(builder.build() as TextCellController); } -export async function makeNumberCellController( - fieldId: string, - rowInfo: RowInfo, - databaseController: DatabaseController -): Promise<Option<NumberCellController>> { - const builder = await makeCellControllerBuilder(fieldId, rowInfo, FieldType.Number, databaseController).then( - (result) => result.unwrap() - ); - - return Some(builder.build() as NumberCellController); -} - export async function makeSingleSelectCellController( fieldId: string, rowInfo: RowInfo, @@ -167,7 +145,7 @@ export async function makeURLCellController( export async function makeCellControllerBuilder( fieldId: string, rowInfo: RowInfo, - fieldType: FieldType, + _fieldType: FieldType, databaseController: DatabaseController ): Promise<Option<CellControllerBuilder>> { const rowCache = databaseController.databaseViewCache.getRowCache(); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestDocument.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestDocument.tsx index 8508232eb954..b6f70acb02da 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestDocument.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestDocument.tsx @@ -5,7 +5,8 @@ import { DocumentBackendService } from '../../stores/effects/document/document_b async function testCreateDocument() { const view = await createTestDocument(); const svc = new DocumentBackendService(view.id); - const document = await svc.open().then((result) => result.unwrap()); + + await svc.open().then((result) => result.unwrap()); // eslint-disable-next-line @typescript-eslint/no-unused-vars // const content = JSON.parse(document.content); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestFolder.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestFolder.tsx index 44585d039a20..6e24aae8a129 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestFolder.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestFolder.tsx @@ -33,7 +33,7 @@ const testCreateFolder = async (userId?: number) => { } for (let i = 1; i <= 3; i++) { - const result = await workspaceService.createView({ + await workspaceService.createView({ name: `test board 1 ${i}`, desc: 'test description', layout: ViewLayoutPB.Board, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestFonts.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestFonts.tsx index c2c5d5d13c2c..04e4c566a665 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestFonts.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestFonts.tsx @@ -1,9 +1,9 @@ -import { useState } from 'react'; +import { ChangeEvent, useState } from 'react'; const TestFonts = () => { const [sampleText, setSampleText] = useState('Sample Text'); - const onInputChange = (e: any) => { + const onInputChange = (e: ChangeEvent<HTMLInputElement>) => { setSampleText(e.target.value); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestGrid.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestGrid.tsx index d4345b6760de..e9e687a923f9 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestGrid.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestGrid.tsx @@ -65,6 +65,7 @@ export const RunAllGridTests = () => { async function createBuildInGrid() { const view = await createTestDatabaseView(ViewLayoutPB.Grid); const databaseController = await openTestDatabase(view.id); + databaseController.subscribe({ onViewChanged: (databasePB) => { Log.debug('Did receive database:' + databasePB); @@ -87,11 +88,13 @@ async function createBuildInGrid() { async function testEditGridCell() { const view = await createTestDatabaseView(ViewLayoutPB.Grid); const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); for (const [index, row] of databaseController.databaseViewCache.rowInfos.entries()) { const cellContent = index.toString(); const fieldInfo = findFirstFieldInfoWithFieldType(row, FieldType.RichText).unwrap(); + await editTextCell(fieldInfo.field.id, row, databaseController, cellContent); await assertTextCell(fieldInfo.field.id, row, databaseController, cellContent); } @@ -100,6 +103,7 @@ async function testEditGridCell() { async function testEditTextCell() { const view = await createTestDatabaseView(ViewLayoutPB.Grid); const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); const row = databaseController.databaseViewCache.rowInfos[0]; @@ -122,9 +126,11 @@ async function testEditTextCell() { async function testEditURLCell() { const view = await createTestDatabaseView(ViewLayoutPB.Grid); const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); const typeOptionController = new TypeOptionController(view.id, None, FieldType.URL); + await typeOptionController.initialize(); const row = databaseController.databaseViewCache.rowInfos[0]; @@ -135,6 +141,7 @@ async function testEditURLCell() { urlCellController.subscribeChanged({ onCellChanged: (content) => { const pb = content.unwrap(); + Log.info('Receive url data:', pb.url, pb.content); }, }); @@ -149,9 +156,11 @@ async function testEditURLCell() { async function testEditDateCell() { const view = await createTestDatabaseView(ViewLayoutPB.Grid); const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); const typeOptionController = new TypeOptionController(view.id, None, FieldType.DateTime); + await typeOptionController.initialize(); const row = databaseController.databaseViewCache.rowInfos[0]; @@ -162,11 +171,13 @@ async function testEditDateCell() { dateCellController.subscribeChanged({ onCellChanged: (content) => { const pb = content.unwrap(); + Log.info('Receive date data:', pb.date, pb.time); }, }); const date = new CalendarData(new Date(), true, '13:00'); + await dateCellController.saveCellData(date); await new Promise((resolve) => setTimeout(resolve, 200)); } @@ -174,15 +185,18 @@ async function testEditDateCell() { async function testEditDateFormatPB() { const view = await createTestDatabaseView(ViewLayoutPB.Grid); const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); // Create date field const typeOptionController = new TypeOptionController(view.id, None, FieldType.DateTime); + await typeOptionController.initialize(); // update date type option const dateTypeOptionContext = makeDateTypeOptionContext(typeOptionController); const typeOption = await dateTypeOptionContext.getTypeOption().then((a) => a.unwrap()); + assert(typeOption.date_format === DateFormatPB.Friendly, 'Date format not match'); assert(typeOption.time_format === TimeFormatPB.TwentyFourHour, 'Time format not match'); typeOption.date_format = DateFormatPB.Local; @@ -190,6 +204,7 @@ async function testEditDateFormatPB() { await dateTypeOptionContext.setTypeOption(typeOption); const typeOption2 = await dateTypeOptionContext.getTypeOption().then((a) => a.unwrap()); + assert(typeOption2.date_format === DateFormatPB.Local, 'Date format not match'); assert(typeOption2.time_format === TimeFormatPB.TwelveHour, 'Time format not match'); @@ -199,20 +214,24 @@ async function testEditDateFormatPB() { async function testEditNumberFormatPB() { const view = await createTestDatabaseView(ViewLayoutPB.Grid); const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); // Create date field const typeOptionController = new TypeOptionController(view.id, None, FieldType.Number); + await typeOptionController.initialize(); // update date type option const dateTypeOptionContext = makeNumberTypeOptionContext(typeOptionController); const typeOption = await dateTypeOptionContext.getTypeOption().then((a) => a.unwrap()); + typeOption.format = NumberFormatPB.EUR; typeOption.name = 'Money'; await dateTypeOptionContext.setTypeOption(typeOption); const typeOption2 = await dateTypeOptionContext.getTypeOption().then((a) => a.unwrap()); + Log.info(typeOption2); await new Promise((resolve) => setTimeout(resolve, 200)); } @@ -220,9 +239,11 @@ async function testEditNumberFormatPB() { async function testCheckboxCell() { const view = await createTestDatabaseView(ViewLayoutPB.Grid); const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); const typeOptionController = new TypeOptionController(view.id, None, FieldType.Checkbox); + await typeOptionController.initialize(); const row = databaseController.databaseViewCache.rowInfos[0]; @@ -235,6 +256,7 @@ async function testCheckboxCell() { checkboxCellController.subscribeChanged({ onCellChanged: (content) => { const pb = content.unwrap(); + Log.info('Receive checkbox data:', pb); }, }); @@ -246,6 +268,7 @@ async function testCheckboxCell() { async function testCreateRow() { const view = await createTestDatabaseView(ViewLayoutPB.Grid); const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); await assertNumberOfRows(view.id, 3); @@ -258,9 +281,11 @@ async function testCreateRow() { async function testDeleteRow() { const view = await createTestDatabaseView(ViewLayoutPB.Grid); const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); const rows = databaseController.databaseViewCache.rowInfos; + await databaseController.deleteRow(rows[0].row.id); await assertNumberOfRows(view.id, 2); @@ -270,12 +295,14 @@ async function testDeleteRow() { if (databaseController.databaseViewCache.rowInfos.length !== 2) { throw Error('The number of rows is not match'); } + await databaseController.dispose(); } async function testCreateOptionInCell() { const view = await createTestDatabaseView(ViewLayoutPB.Grid); const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); for (const [index, row] of databaseController.databaseViewCache.rowInfos.entries()) { if (index === 0) { @@ -283,39 +310,47 @@ async function testCreateOptionInCell() { const cellController = await makeSingleSelectCellController(fieldInfo.field.id, row, databaseController).then( (result) => result.unwrap() ); + // eslint-disable-next-line @typescript-eslint/await-thenable await cellController.subscribeChanged({ onCellChanged: (value) => { if (value.some) { const option: SelectOptionCellDataPB = value.unwrap(); + console.log(option); } }, }); const backendSvc = new SelectOptionCellBackendService(cellController.cellIdentifier); + await backendSvc.createOption({ name: 'option' + index }); await cellController.dispose(); } } + await databaseController.dispose(); } async function testMoveField() { const view = await createTestDatabaseView(ViewLayoutPB.Grid); const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); const ids = databaseController.fieldController.fieldInfos.map((value) => value.field.id); + Log.info('Receive fields data:', ids); databaseController.subscribe({ onFieldsChanged: (values) => { const new_ids = values.map((value) => value.field.id); + Log.info('Receive fields data:', new_ids); }, }); const fieldInfos = [...databaseController.fieldController.fieldInfos]; const field_id = fieldInfos[0].field.id; + await databaseController.moveField({ fieldId: field_id, fromIndex: 0, toIndex: 1 }); await new Promise((resolve) => setTimeout(resolve, 200)); assert(databaseController.fieldController.fieldInfos[1].field.id === field_id); @@ -324,6 +359,7 @@ async function testMoveField() { async function testGetSingleSelectFieldData() { const view = await createTestDatabaseView(ViewLayoutPB.Grid); const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); // Find the single select column @@ -341,6 +377,7 @@ async function testGetSingleSelectFieldData() { // Read options const options = await singleSelectTypeOptionContext.getTypeOption().then((result) => result.unwrap()); + console.log(options); await databaseController.dispose(); @@ -349,6 +386,7 @@ async function testGetSingleSelectFieldData() { async function testSwitchFromSingleSelectToNumber() { const view = await createTestDatabaseView(ViewLayoutPB.Grid); const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); // Find the single select column @@ -357,6 +395,7 @@ async function testSwitchFromSingleSelectToNumber() { (fieldInfo) => fieldInfo.field.field_type === FieldType.SingleSelect )!; const typeOptionController = new TypeOptionController(view.id, Some(singleSelect)); + await typeOptionController.switchToField(FieldType.Number); // Check the number type option @@ -365,6 +404,7 @@ async function testSwitchFromSingleSelectToNumber() { .getTypeOption() .then((result) => result.unwrap()); const format: NumberFormatPB = numberTypeOption.format; + if (format !== NumberFormatPB.Num) { throw Error('The default format should be number'); } @@ -375,10 +415,12 @@ async function testSwitchFromSingleSelectToNumber() { async function testSwitchFromMultiSelectToRichText() { const view = await createTestDatabaseView(ViewLayoutPB.Grid); const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); // Create multi-select field const typeOptionController = new TypeOptionController(view.id, None, FieldType.MultiSelect); + await typeOptionController.initialize(); // Insert options to first row @@ -391,11 +433,13 @@ async function testSwitchFromMultiSelectToRichText() { databaseController ).then((result) => result.unwrap()); const backendSvc = new SelectOptionCellBackendService(selectOptionCellController.cellIdentifier); + await backendSvc.createOption({ name: 'A' }); await backendSvc.createOption({ name: 'B' }); await backendSvc.createOption({ name: 'C' }); const selectOptionCellData = await selectOptionCellController.getCellData().then((result) => result.unwrap()); + if (selectOptionCellData.options.length !== 3) { throw Error('The options should equal to 3'); } @@ -403,6 +447,7 @@ async function testSwitchFromMultiSelectToRichText() { if (selectOptionCellData.select_options.length !== 3) { throw Error('The selected options should equal to 3'); } + await selectOptionCellController.dispose(); // Switch to RichText field type @@ -415,6 +460,7 @@ async function testSwitchFromMultiSelectToRichText() { (result) => result.unwrap() ); const cellContent = await textCellController.getCellData(); + if (cellContent.unwrap() !== 'A,B,C') { throw Error('The cell content should be A,B,C, but receive: ' + cellContent.unwrap()); } @@ -425,14 +471,17 @@ async function testSwitchFromMultiSelectToRichText() { async function testEditField() { const view = await createTestDatabaseView(ViewLayoutPB.Grid); const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); const fieldInfos = databaseController.fieldController.fieldInfos; // Modify the name of the field const firstFieldInfo = fieldInfos[0]; const controller = new TypeOptionController(view.id, Some(firstFieldInfo)); + await controller.initialize(); const newName = 'hello world'; + await controller.setFieldName(newName); await new Promise((resolve) => setTimeout(resolve, 200)); @@ -443,11 +492,13 @@ async function testEditField() { async function testCreateNewField() { const view = await createTestDatabaseView(ViewLayoutPB.Grid); const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); await assertNumberOfFields(view.id, 3); // Modify the name of the field const controller = new TypeOptionController(view.id, None); + await controller.initialize(); await assertNumberOfFields(view.id, 4); await databaseController.dispose(); @@ -456,6 +507,7 @@ async function testCreateNewField() { async function testDeleteField() { const view = await createTestDatabaseView(ViewLayoutPB.Grid); const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); // Modify the name of the field. @@ -463,6 +515,7 @@ async function testDeleteField() { // So let choose the second fieldInfo. const fieldInfo = databaseController.fieldController.fieldInfos[1]; const controller = new TypeOptionController(view.id, Some(fieldInfo)); + await controller.initialize(); await assertNumberOfFields(view.id, 3); await controller.deleteField(); @@ -485,24 +538,31 @@ export const TestEditTextCell = () => { export const TestEditURLCell = () => { return TestButton('Test editing URL cell', testEditURLCell); }; + export const TestEditDateCell = () => { return TestButton('Test editing date cell', testEditDateCell); }; + export const TestEditDateFormat = () => { return TestButton('Test editing date format', testEditDateFormatPB); }; + export const TestEditNumberFormat = () => { return TestButton('Test editing number format', testEditNumberFormatPB); }; + export const TestEditCheckboxCell = () => { return TestButton('Test editing checkbox cell', testCheckboxCell); }; + export const TestCreateRow = () => { return TestButton('Test create row', testCreateRow); }; + export const TestDeleteRow = () => { return TestButton('Test delete row', testDeleteRow); }; + export const TestCreateSelectOptionInCell = () => { return TestButton('Test create a select option in cell', testCreateOptionInCell); }; @@ -522,6 +582,7 @@ export const TestSwitchFromMultiSelectToText = () => { export const TestMoveField = () => { return TestButton('Test move field', testMoveField); }; + export const TestEditField = () => { return TestButton('Test edit the column name', testEditField); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestGroup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestGroup.tsx index 31ea758924ed..c161eeeeb1bc 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestGroup.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/tests/TestGroup.tsx @@ -31,6 +31,7 @@ export const TestAllKanbanTests = () => { async function createBuildInBoard() { const view = await createTestDatabaseView(ViewLayoutPB.Board); const databaseController = await openTestDatabase(view.id); + databaseController.subscribe({ onGroupByField: (groups) => { console.log(groups); @@ -51,10 +52,12 @@ async function createBuildInBoard() { async function createKanbanBoardRow() { const view = await createTestDatabaseView(ViewLayoutPB.Board); const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); // Create row in no status group const noStatusGroup = databaseController.groups.getValue()[0]; + await noStatusGroup.createRow().then((result) => result.unwrap()); await assertNumberOfRowsInGroup(view.id, noStatusGroup.groupId, 1); @@ -64,11 +67,13 @@ async function createKanbanBoardRow() { async function moveKanbanBoardRow() { const view = await createTestDatabaseView(ViewLayoutPB.Board); const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); // Create row in no status group const firstGroup = databaseController.groups.getValue()[1]; const secondGroup = databaseController.groups.getValue()[2]; + // subscribe the group changes firstGroup.subscribe({ onRemoveRow: (groupId, deleteRowId) => { @@ -101,6 +106,7 @@ async function moveKanbanBoardRow() { }); const row = firstGroup.rowAtIndex(0).unwrap(); + await databaseController.moveGroupRow(row.id, secondGroup.groupId); assert(firstGroup.rows.length === 2); @@ -115,11 +121,13 @@ async function moveKanbanBoardRow() { async function createKanbanBoardColumn() { const view = await createTestDatabaseView(ViewLayoutPB.Board); const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); // Create row in no status group const firstGroup = databaseController.groups.getValue()[1]; const secondGroup = databaseController.groups.getValue()[2]; + await databaseController.moveGroup(firstGroup.groupId, secondGroup.groupId); assert(databaseController.groups.getValue()[1].groupId === secondGroup.groupId); @@ -130,6 +138,7 @@ async function createKanbanBoardColumn() { async function createColumnInBoard() { const view = await createTestDatabaseView(ViewLayoutPB.Board); const databaseController = await openTestDatabase(view.id); + await databaseController.open().then((result) => result.unwrap()); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -139,6 +148,7 @@ async function createColumnInBoard() { // Create a option which will cause creating a new group const name = 'New column'; + await createSingleSelectOptions(view.id, singleSelect, [name]); // Wait the backend posting the notification to update the groups diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/trash/Trash.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/trash/Trash.hooks.ts index a95ae94a776e..9948a2b189ed 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/trash/Trash.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/trash/Trash.hooks.ts @@ -17,7 +17,7 @@ export function useLoadTrash() { }, [controller, dispatch]); const subscribeToTrash = useCallback(async () => { - controller.subscribe({ + await controller.subscribe({ onTrashChanged: (trash) => { dispatch(trashActions.onTrashChanged(trash.map(trashPBToTrash))); }, @@ -33,7 +33,7 @@ export function useLoadTrash() { useEffect(() => { return () => { - controller.dispose(); + void controller.dispose(); }; }, [controller]); @@ -52,7 +52,7 @@ export function useTrashActions() { useEffect(() => { return () => { - controller.dispose(); + void controller.dispose(); }; }, [controller]); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/trash/Trash.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/trash/Trash.tsx index 23a78a9bef29..6387437162de 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/trash/Trash.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/trash/Trash.tsx @@ -28,11 +28,11 @@ function Trash() { <div className={'flex items-center justify-between'}> <div className={'text-2xl font-bold'}>{t('trash.text')}</div> <div className={'flex items-center justify-end'}> - <Button color={'inherit'} onClick={(e) => onClickRestoreAll()}> + <Button color={'inherit'} onClick={() => onClickRestoreAll()}> <RestoreOutlined /> <span className={'ml-1'}>{t('trash.restoreAll')}</span> </Button> - <Button color={'error'} onClick={(e) => onClickDeleteAll()}> + <Button color={'error'} onClick={() => onClickDeleteAll()}> <DeleteOutline /> <span className={'ml-1'}>{t('trash.deleteAll')}</span> </Button> diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/trash/TrashItem.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/trash/TrashItem.tsx index 78ce773423b2..3d970257fd85 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/trash/TrashItem.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/trash/TrashItem.tsx @@ -23,10 +23,10 @@ function TrashItem({ return ( <ListItem - onMouseEnter={(e) => { + onMouseEnter={() => { setHoverId(item.id); }} - onMouseLeave={(e) => { + onMouseLeave={() => { setHoverId(''); }} key={item.id} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/user/application/notifications/parser.ts b/frontend/appflowy_tauri/src/appflowy_app/components/user/application/notifications/parser.ts index 4e3a57af4ba0..866d910e8361 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/user/application/notifications/parser.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/user/application/notifications/parser.ts @@ -10,6 +10,7 @@ export class UserNotificationParser extends NotificationParser<UserNotification> params.callback, (ty) => { const notification = UserNotification[ty]; + if (isUserNotification(notification)) { return UserNotification[notification]; } else { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/user/application/notifications/user_listener.ts b/frontend/appflowy_tauri/src/appflowy_app/components/user/application/notifications/user_listener.ts index d63963eaf0da..112ee9b6a211 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/user/application/notifications/user_listener.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/user/application/notifications/user_listener.ts @@ -18,6 +18,7 @@ export class UserNotificationListener extends AFNotificationObserver<UserNotific } else { this.onProfileUpdate?.(result); } + break; default: break; @@ -26,6 +27,7 @@ export class UserNotificationListener extends AFNotificationObserver<UserNotific id: params.userId, onError: params.onError, }); + super(parser); this.onProfileUpdate = params.onProfileUpdate; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/hooks/ViewId.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/hooks/ViewId.hooks.ts new file mode 100644 index 000000000000..6711ece8c862 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/hooks/ViewId.hooks.ts @@ -0,0 +1,6 @@ +import { createContext, useContext } from 'react'; + +const ViewIdContext = createContext(''); + +export const ViewIdProvider = ViewIdContext.Provider; +export const useViewId = () => useContext(ViewIdContext); diff --git a/frontend/appflowy_tauri/src/appflowy_app/hooks/index.ts b/frontend/appflowy_tauri/src/appflowy_app/hooks/index.ts index d9223c236b0e..c29ddd04aa3a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/hooks/index.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/hooks/index.ts @@ -1 +1,2 @@ export * from './notification.hooks'; +export * from './ViewId.hooks'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/hooks/notification.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/hooks/notification.hooks.ts index 87327b71be76..5c1efe43dbf3 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/hooks/notification.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/hooks/notification.hooks.ts @@ -1,9 +1,8 @@ /* eslint-disable no-redeclare */ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { useEffect } from 'react'; import { listen } from '@tauri-apps/api/event'; -import { Ok, Err, Result } from 'ts-results'; import { SubscribeObject } from '@/services/backend/models/flowy-notification'; -import { FlowyError } from '@/services/backend/models/flowy-error'; import { DatabaseFieldChangesetPB, DatabaseNotification, @@ -14,33 +13,39 @@ import { ReorderSingleRowPB, RowsChangePB, RowsVisibilityChangePB, + SortChangesetNotificationPB, } from '@/services/backend'; const NotificationPBMap = { [DatabaseNotification.DidUpdateViewRowsVisibility]: RowsVisibilityChangePB, [DatabaseNotification.DidUpdateViewRows]: RowsChangePB, [DatabaseNotification.DidReorderRows]: ReorderAllRowsPB, - [DatabaseNotification.DidReorderSingleRow]:ReorderSingleRowPB, - [DatabaseNotification.DidUpdateFields]:DatabaseFieldChangesetPB, - [DatabaseNotification.DidGroupByField]:GroupChangesPB, - [DatabaseNotification.DidUpdateNumOfGroups]:GroupChangesPB, + [DatabaseNotification.DidReorderSingleRow]: ReorderSingleRowPB, + [DatabaseNotification.DidUpdateFields]: DatabaseFieldChangesetPB, + [DatabaseNotification.DidGroupByField]: GroupChangesPB, + [DatabaseNotification.DidUpdateNumOfGroups]: GroupChangesPB, [DatabaseNotification.DidUpdateGroupRow]: GroupRowsNotificationPB, [DatabaseNotification.DidUpdateField]: FieldPB, [DatabaseNotification.DidUpdateCell]: null, + [DatabaseNotification.DidUpdateSort]: SortChangesetNotificationPB, }; type NotificationMap = typeof NotificationPBMap; type NotificationEnum = keyof NotificationMap; -type NullableInstanceType<K extends ((abstract new (...args: any) => any) | null)> = K extends (abstract new (...args: any) => any) ? InstanceType<K> : void; +type NullableInstanceType<K extends (abstract new (...args: any) => any) | null> = K extends abstract new ( + ...args: any +) => any + ? InstanceType<K> + : void; -type NotificationHandler<K extends NotificationEnum> = (result: Result<NullableInstanceType<NotificationMap[K]>, FlowyError>) => void; +type NotificationHandler<K extends NotificationEnum> = (result: NullableInstanceType<NotificationMap[K]>) => void; /** * Subscribes to a set of notifications. - * - * This function subscribes to notifications defined by the `NotificationEnum` and + * + * This function subscribes to notifications defined by the `NotificationEnum` and * calls the appropriate `NotificationHandler` when each type of notification is received. * * @param {Object} callbacks - An object containing handlers for various notification types. @@ -48,9 +53,9 @@ type NotificationHandler<K extends NotificationEnum> = (result: Result<NullableI * * @param {Object} [options] - Optional settings for the subscription. * @param {string} [options.id] - An optional ID. If provided, only notifications with a matching ID will be processed. - * + * * @returns {Promise<() => void>} A Promise that resolves to an unsubscribe function. - * + * * @example * subscribeNotifications({ * [DatabaseNotification.DidUpdateField]: (result) => { @@ -75,16 +80,16 @@ type NotificationHandler<K extends NotificationEnum> = (result: Result<NullableI * // ... * // To unsubscribe, call `unsubscribe()` * }); - * + * * @throws {Error} Throws an error if unable to subscribe. */ export function subscribeNotifications( callbacks: { [K in NotificationEnum]?: NotificationHandler<K>; }, - options?: { id?: string }, + options?: { id?: string } ): Promise<() => void> { - return listen<ReturnType<typeof SubscribeObject.prototype.toObject>>('af-notification', event => { + return listen<ReturnType<typeof SubscribeObject.prototype.toObject>>('af-notification', (event) => { const subject = SubscribeObject.fromObject(event.payload); const { id, ty } = subject; @@ -101,13 +106,12 @@ export function subscribeNotifications( } if (subject.has_error) { - const error = FlowyError.deserializeBinary(subject.error); - - callback(Err(error)); + // const error = FlowyError.deserialize(subject.error); + return; } else { const { payload } = subject; - callback(pb ? Ok(pb.deserializeBinary(payload)) : Ok.EMPTY); + pb ? callback(pb.deserialize(payload)) : callback(); } }); } @@ -115,7 +119,7 @@ export function subscribeNotifications( export function subscribeNotification<K extends NotificationEnum>( notification: K, callback: NotificationHandler<K>, - options?: { id?: string }, + options?: { id?: string } ): Promise<() => void> { return subscribeNotifications({ [notification]: callback }, options); } @@ -123,7 +127,7 @@ export function subscribeNotification<K extends NotificationEnum>( export function useNotification<K extends NotificationEnum>( notification: K, callback: NotificationHandler<K>, - options: { id?: string }, + options: { id?: string } ): void { const { id } = options; @@ -131,7 +135,7 @@ export function useNotification<K extends NotificationEnum>( const unsubscribePromise = subscribeNotification(notification, callback, { id }); return () => { - void unsubscribePromise.then(fn => fn()); + void unsubscribePromise.then((fn) => fn()); }; }, [callback, id, notification]); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/i18n/config.ts b/frontend/appflowy_tauri/src/appflowy_app/i18n/config.ts index 59f0d77cf2f2..202b6a4220df 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/i18n/config.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/i18n/config.ts @@ -3,7 +3,7 @@ import LanguageDetector from 'i18next-browser-languagedetector'; import { initReactI18next } from 'react-i18next'; import resourcesToBackend from 'i18next-resources-to-backend'; -i18next +void i18next .use(resourcesToBackend((language: string) => import(`./translations/${language}.json`))) .use(LanguageDetector) .use(initReactI18next) diff --git a/frontend/appflowy_tauri/src/appflowy_app/interfaces/database/index.ts b/frontend/appflowy_tauri/src/appflowy_app/interfaces/database/index.ts deleted file mode 100644 index a478f9c12d32..000000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/interfaces/database/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './types'; -export * from './transform'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/interfaces/database/transform.ts b/frontend/appflowy_tauri/src/appflowy_app/interfaces/database/transform.ts deleted file mode 100644 index cb35f1e8e233..000000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/interfaces/database/transform.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { CellPB, ChecklistCellDataPB, DateCellDataPB, FieldPB, FieldType, SelectOptionCellDataPB, URLCellDataPB } from '@/services/backend'; -import type { Database } from './types'; - -export const fieldPbToField = (fieldPb: FieldPB): Database.Field => ({ - id: fieldPb.id, - name: fieldPb.name, - type: fieldPb.field_type, - visibility: fieldPb.visibility, - width: fieldPb.width, - isPrimary: fieldPb.is_primary, -}); - -const toDateCellData = (pb: DateCellDataPB): Database.DateTimeCellData => ({ - date: pb.date, - time: pb.time, - timestamp: pb.timestamp, - includeTime: pb.include_time, -}); - -const toSelectCellData = (pb: SelectOptionCellDataPB): Database.SelectCellData => { - return { - options: pb.options.map(option => ({ - id: option.id, - name: option.name, - color: option.color, - })), - selectOptions: pb.select_options.map(option => ({ - id: option.id, - name: option.name, - color: option.color, - })), - }; -}; - -const toURLCellData = (pb: URLCellDataPB): Database.UrlCellData => ({ - url: pb.url, - content: pb.content, -}); - -const toChecklistCellData = (pb: ChecklistCellDataPB): Database.ChecklistCellData => ({ - selectedOptions: pb.selected_options.map(({ id }) => id), - percentage: pb.percentage, -}); - -function parseCellData(fieldType: FieldType, data: Uint8Array) { - switch (fieldType) { - case FieldType.RichText: - case FieldType.Number: - case FieldType.Checkbox: - return new TextDecoder().decode(data); - case FieldType.DateTime: - case FieldType.LastEditedTime: - case FieldType.CreatedTime: - return toDateCellData(DateCellDataPB.deserializeBinary(data)); - case FieldType.SingleSelect: - case FieldType.MultiSelect: - return toSelectCellData(SelectOptionCellDataPB.deserializeBinary(data)); - case FieldType.URL: - return toURLCellData(URLCellDataPB.deserializeBinary(data)); - case FieldType.Checklist: - return toChecklistCellData(ChecklistCellDataPB.deserializeBinary(data)); - } -} - -export const cellPbToCell = (cellPb: CellPB, fieldType: FieldType): Database.Cell => { - return { - rowId: cellPb.row_id, - fieldId: cellPb.field_id, - fieldType: fieldType, - data: parseCellData(fieldType, cellPb.data), - }; -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/interfaces/database/types.ts b/frontend/appflowy_tauri/src/appflowy_app/interfaces/database/types.ts deleted file mode 100644 index 445406c4906a..000000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/interfaces/database/types.ts +++ /dev/null @@ -1,227 +0,0 @@ -import { - CalendarLayoutPB, - DatabaseLayoutPB, - DateFormatPB, - FieldType, - NumberFormatPB, - SelectOptionColorPB, - SelectOptionConditionPB, - SortConditionPB, - TextFilterConditionPB, - TimeFormatPB, -} from '@/services/backend'; - - -export interface Database { - id: string; - viewId: string; - name: string; - fields: Database.UndeterminedField[]; - rows: Database.Row[]; - layoutType: DatabaseLayoutPB; - layoutSetting: Database.GridLayoutSetting | Database.CalendarLayoutSetting; - isLinked: boolean; -} - -// eslint-disable-next-line @typescript-eslint/no-namespace, no-redeclare -export namespace Database { - - export interface GridLayoutSetting { - filters?: UndeterminedFilter[]; - groups?: Group[]; - sorts?: Sort[]; - } - - export interface CalendarLayoutSetting { - fieldId?: string; - layoutTy?: CalendarLayoutPB; - firstDayOfWeek?: number; - showWeekends?: boolean; - showWeekNumbers?: boolean; - } - - export interface Field { - id: string; - name: string; - type: FieldType; - typeOption?: unknown; - visibility?: boolean; - width?: number; - isPrimary?: boolean; - } - - export interface NumberTypeOption { - format?: NumberFormatPB; - scale?: number; - symbol?: string; - name?: string; - } - - export interface NumberField extends Field { - type: FieldType.Number; - typeOption: NumberTypeOption; - } - - export interface DateTimeTypeOption { - dateFormat?: DateFormatPB; - timeFormat?: TimeFormatPB; - timezoneId?: string; - fieldType?: FieldType; - } - - export interface DateTimeField extends Field { - type: FieldType.DateTime; - typeOption: DateTimeTypeOption; - } - - export interface SelectOption { - id: string; - name: string; - color: SelectOptionColorPB; - } - - export interface SelectTypeOption { - options?: SelectOption[]; - disableColor?: boolean; - } - - export interface SelectField extends Field { - type: FieldType.SingleSelect | FieldType.MultiSelect; - typeOption: SelectTypeOption; - } - - export interface ChecklistTypeOption { - config?: string; - } - - export interface ChecklistField extends Field { - type: FieldType.Checklist; - typeOption: ChecklistTypeOption; - } - - export type UndeterminedField = NumberField | DateTimeField | SelectField | ChecklistField | Field; - - export interface Sort { - id: string; - fieldId: string; - fieldType: FieldType; - condition: SortConditionPB; - } - - export interface Group { - id: string; - fieldId: string; - } - - export interface Filter { - id: string; - fieldId: string; - fieldType: FieldType; - data: unknown; - } - - export interface TextFilter extends Filter { - fieldType: FieldType.RichText; - data: TextFilterCondition; - } - - export interface TextFilterCondition { - condition?: TextFilterConditionPB; - content?: string; - } - - export interface SelectFilter extends Filter { - fieldType: FieldType.SingleSelect | FieldType.MultiSelect; - data: SelectFilterCondition; - } - - export interface SelectFilterCondition { - condition?: SelectOptionConditionPB; - /** - * link to [SelectOption's id property]{@link SelectOption#id}. - */ - optionIds?: string[]; - } - - export type UndeterminedFilter = TextFilter | SelectFilter | Filter; - - export interface Row { - id: string; - documentId?: string; - icon?: string; - cover?: string; - createdAt?: number; - modifiedAt?: number; - height?: number; - visibility?: boolean; - } - - export interface Cell { - rowId: string; - fieldId: string; - fieldType: FieldType; - data: unknown; - } - - export interface TextCell extends Cell { - fieldType: FieldType.RichText; - data: string; - } - - export interface NumberCell extends Cell { - fieldType: FieldType.Number; - data: string; - } - - export interface CheckboxCell extends Cell { - fieldType: FieldType.Checkbox; - data: 'Yes' | 'No'; - } - - export interface UrlCell extends Cell { - fieldType: FieldType.URL; - data: UrlCellData; - } - - export interface UrlCellData { - url: string; - content?: string; - } - - export interface SelectCell extends Cell { - fieldType: FieldType.SingleSelect | FieldType.MultiSelect; - data: SelectCellData; - } - - export interface SelectCellData { - options?: SelectOption[]; - selectOptions?: SelectOption[]; - } - - export interface DateTimeCell extends Cell { - fieldType: FieldType.DateTime; - data: DateTimeCellData; - } - - export interface DateTimeCellData { - date?: string; - time?: string; - timestamp?: number; - includeTime?: boolean; - } - - export interface ChecklistCell extends Cell { - fieldType: FieldType.Checklist; - data: ChecklistCellData; - } - - export interface ChecklistCellData { - /** - * link to [SelectOption's id property]{@link SelectOption#id}. - */ - selectedOptions?: string[]; - percentage?: number; - } - - export type UndeterminedCell = TextCell | NumberCell | DateTimeCell | SelectCell | CheckboxCell | UrlCell | ChecklistCell; -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts b/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts index a7a122dc2993..e66edd77af7e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts @@ -3,12 +3,6 @@ import { BlockActionTypePB } from '@/services/backend'; import { Sources } from 'quill'; import React from 'react'; -export interface DocumentBlockJSON { - type: BlockType; - data: BlockData<any>; - children: DocumentBlockJSON[]; -} - export interface RangeStatic { id: string; length: number; @@ -61,11 +55,9 @@ export interface QuoteBlockData extends TextBlockData { export interface CalloutBlockData extends TextBlockData { icon: string; } - +// eslint-disable-next-line @typescript-eslint/no-explicit-any export type TextBlockData = Record<string, any>; -export interface DividerBlockData {} - export enum Align { Left = 'left', Center = 'center', @@ -88,8 +80,11 @@ export interface PageBlockData extends TextBlockData { cover?: string; coverType?: CoverType; } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Data = any; -export type BlockData<Type> = Type extends BlockType.HeadingBlock +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type BlockData<Type = any> = Type extends BlockType.HeadingBlock ? HeadingBlockData : Type extends BlockType.PageBlock ? PageBlockData @@ -103,8 +98,6 @@ export type BlockData<Type> = Type extends BlockType.HeadingBlock ? NumberedListBlockData : Type extends BlockType.ToggleListBlock ? ToggleListBlockData - : Type extends BlockType.DividerBlock - ? DividerBlockData : Type extends BlockType.CalloutBlock ? CalloutBlockData : Type extends BlockType.EquationBlock @@ -113,12 +106,13 @@ export type BlockData<Type> = Type extends BlockType.HeadingBlock ? ImageBlockData : Type extends BlockType.TextBlock ? TextBlockData - : any; + : Data; +// eslint-disable-next-line @typescript-eslint/no-explicit-any export interface NestedBlock<Type = any> { id: string; type: BlockType; - data: BlockData<Type> | any; + data: BlockData<Type> | Data; parent: string | null; children: string; externalId?: string; @@ -152,7 +146,6 @@ export interface SlashCommandState { export enum SlashCommandOptionKey { TEXT, - PAGE, TODO, BULLET, NUMBER, @@ -170,7 +163,7 @@ export enum SlashCommandOptionKey { export interface SlashCommandOption { type: BlockType; - data?: BlockData<any>; + data?: BlockData; key: SlashCommandOptionKey; } @@ -273,7 +266,7 @@ export interface BlockConfig { /** * The default data of the block */ - defaultData?: BlockData<any>; + defaultData?: BlockData; /** * The props that will be passed to the text split function diff --git a/frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts b/frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts index d10ea3cdff38..7d1466630a21 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts @@ -1,7 +1,6 @@ import { ThemeModePB as ThemeMode } from '@/services/backend'; export { ThemeMode }; -export interface Document {} export interface UserSetting { theme?: Theme; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_bd_svc.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_bd_svc.ts index 99d3a64f0606..30ec4e58124b 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_bd_svc.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_bd_svc.ts @@ -18,6 +18,7 @@ class CellBackendService { row_id: cellId.rowId, cell_changeset: data, }); + return DatabaseEventUpdateCell(payload); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_cache.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_cache.ts index a19ad13d27b1..d81415660c99 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_cache.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_cache.ts @@ -14,6 +14,7 @@ export class CellCache { remove = (key: CellCacheKey) => { const cellDataByRowId = this.cellDataByFieldId.get(key.fieldId); + if (cellDataByRowId !== undefined) { cellDataByRowId.delete(key.rowId); } @@ -25,8 +26,10 @@ export class CellCache { insert = (key: CellCacheKey, value: any) => { const cellDataByRowId = this.cellDataByFieldId.get(key.fieldId); + if (cellDataByRowId === undefined) { const map = new Map(); + map.set(key.rowId, value); this.cellDataByFieldId.set(key.fieldId, map); } else { @@ -36,10 +39,12 @@ export class CellCache { get<T>(key: CellCacheKey): Option<T> { const cellDataByRowId = this.cellDataByFieldId.get(key.fieldId); + if (cellDataByRowId === undefined) { return None; } else { const value = cellDataByRowId.get(key.rowId); + if (typeof value === typeof undefined) { return None; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_controller.ts index d569c547ae3d..a753fb9ab130 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_controller.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_controller.ts @@ -55,6 +55,7 @@ export class CellController<T, D> { if (this.cellDataLoader.reloadOnFieldChanged) { await this._loadCellData(); } + this.subscribeCallbacks?.onFieldChanged?.(); }, }); @@ -71,6 +72,7 @@ export class CellController<T, D> { getTypeOption = async <P extends TypeOptionParser<PD>, PD>(parser: P) => { const result = await this.fieldBackendService.getTypeOptionData(this.cellIdentifier.fieldType); + if (result.ok) { return Ok(parser.fromBuffer(result.val.type_option_data)); } else { @@ -80,6 +82,7 @@ export class CellController<T, D> { saveCellData = async (data: D) => { const result = await this.cellDataPersistence.save(data); + if (result.err) { Log.error(result.val); } @@ -90,17 +93,21 @@ export class CellController<T, D> { /// subscribers of the [onCellChanged] will get noticed getCellData = async (): Promise<Option<T>> => { const cellData = this.cellCache.get<T>(this.cacheKey); + if (cellData.none) { await this._loadCellData(); return this.cellCache.get<T>(this.cacheKey); } + return cellData; }; private _loadCellData = async () => { const result = await this.cellDataLoader.loadData(); + if (result.ok) { const cellData = result.val; + if (cellData.some) { this.cellCache.insert(this.cacheKey, cellData.val); this.cellDataNotifier.cellData = cellData; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_observer.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_observer.ts index dba0290d7af6..0cd5ae35c2e8 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_observer.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/cell_observer.ts @@ -29,6 +29,7 @@ export class CellObserver { } else { this.notifier?.notify(result); } + return; default: break; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/data_parser.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/data_parser.ts index 86b2198b4e1b..80afed8f9f29 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/data_parser.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/data_parser.ts @@ -1,4 +1,3 @@ -import utf8 from 'utf8'; import { CellBackendService, CellIdentifier } from './cell_bd_svc'; import { SelectOptionCellDataPB, URLCellDataPB, DateCellDataPB } from '@/services/backend'; import { Err, None, Ok, Option, Some } from 'ts-results'; @@ -19,6 +18,7 @@ class CellDataLoader<T> { loadData = async () => { const result = await this.service.getCell(this.cellId); + if (result.ok) { return Ok(this.parser.parserData(result.val.data)); } else { @@ -48,6 +48,7 @@ class SelectOptionCellDataParser extends CellDataParser<SelectOptionCellDataPB> if (data.length === 0) { return None; } + return Some(SelectOptionCellDataPB.deserializeBinary(data)); } } @@ -57,6 +58,7 @@ class URLCellDataParser extends CellDataParser<URLCellDataPB> { if (data.length === 0) { return None; } + return Some(URLCellDataPB.deserializeBinary(data)); } } diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/data_persistence.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/data_persistence.ts index 9066b61150ae..ea8e9e2953c2 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/data_persistence.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/data_persistence.ts @@ -25,10 +25,12 @@ export class DateCellDataPersistence extends CellDataPersistence<CalendarData> { save(data: CalendarData): Promise<Result<void, FlowyError>> { const payload = DateChangesetPB.fromObject({ cell_id: _makeCellId(this.cellIdentifier) }); + payload.date = (data.date.getTime() / 1000) | 0; if (data.time !== undefined) { payload.time = data.time; } + payload.include_time = data.includeTime; return DatabaseEventUpdateDateCell(payload); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/select_option_bd_svc.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/select_option_bd_svc.ts index 068ae60797ce..5e3e86fadc69 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/select_option_bd_svc.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/select_option_bd_svc.ts @@ -40,6 +40,7 @@ export class SelectOptionCellBackendService { }); const result = await DatabaseEventCreateSelectOption(payload); + if (result.ok) { return await this._insertOption(result.val, params.isSelect || true); } else { @@ -47,7 +48,7 @@ export class SelectOptionCellBackendService { } }; - private _insertOption = (option: SelectOptionPB, isSelect: boolean) => { + private _insertOption = (option: SelectOptionPB, _: boolean) => { const payload = RepeatedSelectOptionPayload.fromObject({ view_id: this.cellIdentifier.viewId, field_id: this.cellIdentifier.fieldId, @@ -73,6 +74,7 @@ export class SelectOptionCellBackendService { field_id: this.cellIdentifier.fieldId, row_id: this.cellIdentifier.rowId, }); + payload.items.push(...options); return DatabaseEventDeleteSelectOption(payload); }; @@ -83,12 +85,14 @@ export class SelectOptionCellBackendService { selectOption = (optionIds: string[]) => { const payload = SelectOptionCellChangesetPB.fromObject({ cell_identifier: this._cellIdentifier() }); + payload.insert_option_ids.push(...optionIds); return DatabaseEventUpdateSelectOptionCell(payload); }; unselectOption = (optionIds: string[]) => { const payload = SelectOptionCellChangesetPB.fromObject({ cell_identifier: this._cellIdentifier() }); + payload.delete_option_ids.push(...optionIds); return DatabaseEventUpdateSelectOptionCell(payload); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_bd_svc.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_bd_svc.ts index 7a03c82beb5a..a38dc176c4e7 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_bd_svc.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_bd_svc.ts @@ -19,7 +19,6 @@ import { MoveGroupRowPayloadPB, MoveRowPayloadPB, RowIdPB, - DatabaseEventUpdateDatabaseSetting, DuplicateFieldPayloadPB, DatabaseEventDuplicateField, } from '@/services/backend/events/flowy-database2'; @@ -92,6 +91,7 @@ export class DatabaseBackendService { moveRow = async (fromRowId: string, toRowId: string) => { const payload = MoveRowPayloadPB.fromObject({ view_id: this.viewId, from_row_id: fromRowId, to_row_id: toRowId }); + return DatabaseEventMoveRow(payload); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_controller.ts index f4e94f4790b9..39d1db308a98 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_controller.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_controller.ts @@ -76,7 +76,6 @@ export class DatabaseController { // load database initial data await this.fieldController.loadFields(database.fields); - const loadGroupResult = await this.loadGroup(); this.databaseViewCache.initializeWithRows(database.rows); diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_bd_svc.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_bd_svc.ts index 3580aece0b8e..10c901067881 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_bd_svc.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_bd_svc.ts @@ -55,11 +55,13 @@ export class FieldBackendService { deleteField = () => { const payload = DeleteFieldPayloadPB.fromObject({ view_id: this.viewId, field_id: this.fieldId }); + return DatabaseEventDeleteField(payload); }; duplicateField = () => { const payload = DuplicateFieldPayloadPB.fromObject({ view_id: this.viewId, field_id: this.fieldId }); + return DatabaseEventDuplicateField(payload); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_controller.ts index 46902ddef4ec..2234c06dbd24 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_controller.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_controller.ts @@ -29,6 +29,7 @@ export class FieldController { loadFields = async (fieldIds: FieldIdPB[]) => { const result = await this.backendService.getFields(fieldIds); + if (result.ok) { this.numOfFieldsNotifier.fieldInfos = result.val.map((field) => new FieldInfo(field)); } else { @@ -47,6 +48,7 @@ export class FieldController { onFieldsChanged: (result) => { if (result.ok) { const changeset = result.val; + this._deleteFields(changeset.deleted_fields); this._insertFields(changeset.inserted_fields); this._updateFields(changeset.updated_fields); @@ -66,6 +68,7 @@ export class FieldController { const predicate = (element: FieldInfo): boolean => { return !deletedFieldIds.includes(element.field.id); }; + this.numOfFieldsNotifier.fieldInfos = [...this.fieldInfos].filter(predicate); }; @@ -73,9 +76,12 @@ export class FieldController { if (insertedFields.length === 0) { return; } + const newFieldInfos = [...this.fieldInfos]; + insertedFields.forEach((insertedField) => { const fieldInfo = new FieldInfo(insertedField.field); + if (newFieldInfos.length > insertedField.index) { newFieldInfos.splice(insertedField.index, 0, fieldInfo); } else { @@ -91,10 +97,12 @@ export class FieldController { } const newFieldInfos = [...this.fieldInfos]; + updatedFields.forEach((updatedField) => { const index = newFieldInfos.findIndex((fieldInfo) => { return fieldInfo.field.id === updatedField.id; }); + if (index !== -1) { newFieldInfos.splice(index, 1, new FieldInfo(updatedField)); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_observer.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_observer.ts index d6a3e307b272..162c5c972aaa 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_observer.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/field_observer.ts @@ -26,6 +26,7 @@ export class DatabaseFieldChangesetObserver { } else { this.notifier?.notify(result); } + return; default: break; @@ -64,6 +65,7 @@ export class DatabaseFieldObserver { } else { this._notifier?.notify(result); } + break; default: break; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/type_option/type_option_bd_svc.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/type_option/type_option_bd_svc.ts index 31da01b9d80c..240615a2e6b6 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/type_option/type_option_bd_svc.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/type_option/type_option_bd_svc.ts @@ -10,6 +10,7 @@ export class TypeOptionBackendService { createTypeOption = (fieldType: FieldType) => { const payload = CreateFieldPayloadPB.fromObject({ view_id: this.viewId, field_type: fieldType }); + return DatabaseEventCreateTypeOption(payload); }; @@ -19,6 +20,7 @@ export class TypeOptionBackendService { field_id: fieldId, field_type: fieldType, }); + return DatabaseEventGetTypeOption(payload); }; @@ -28,6 +30,7 @@ export class TypeOptionBackendService { field_id: fieldId, field_type: fieldType, }); + return DatabaseEventUpdateFieldType(payload); }; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/type_option/type_option_context.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/type_option/type_option_context.ts index 7e1f55bc3043..ce1b05251d3a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/type_option/type_option_context.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/type_option/type_option_context.ts @@ -22,6 +22,7 @@ abstract class TypeOptionSerde<T> { // RichText export function makeRichTextTypeOptionContext(controller: TypeOptionController): RichTextTypeOptionContext { const parser = new RichTextTypeOptionSerde(); + return new TypeOptionContext<string>(parser, controller); } @@ -40,6 +41,7 @@ class RichTextTypeOptionSerde extends TypeOptionSerde<string> { // Number export function makeNumberTypeOptionContext(controller: TypeOptionController): NumberTypeOptionContext { const parser = new NumberTypeOptionSerde(); + return new TypeOptionContext<NumberTypeOptionPB>(parser, controller); } @@ -58,6 +60,7 @@ class NumberTypeOptionSerde extends TypeOptionSerde<NumberTypeOptionPB> { // Checkbox export function makeCheckboxTypeOptionContext(controller: TypeOptionController): CheckboxTypeOptionContext { const parser = new CheckboxTypeOptionSerde(); + return new TypeOptionContext<CheckboxTypeOptionPB>(parser, controller); } @@ -76,6 +79,7 @@ class CheckboxTypeOptionSerde extends TypeOptionSerde<CheckboxTypeOptionPB> { // URL export function makeURLTypeOptionContext(controller: TypeOptionController): URLTypeOptionContext { const parser = new URLTypeOptionSerde(); + return new TypeOptionContext<URLTypeOptionPB>(parser, controller); } @@ -94,6 +98,7 @@ class URLTypeOptionSerde extends TypeOptionSerde<URLTypeOptionPB> { // Date export function makeDateTypeOptionContext(controller: TypeOptionController): DateTypeOptionContext { const parser = new DateTypeOptionSerde(); + return new TypeOptionContext<DateTypeOptionPB>(parser, controller); } @@ -112,6 +117,7 @@ class DateTypeOptionSerde extends TypeOptionSerde<DateTypeOptionPB> { // SingleSelect export function makeSingleSelectTypeOptionContext(controller: TypeOptionController): SingleSelectTypeOptionContext { const parser = new SingleSelectTypeOptionSerde(); + return new TypeOptionContext<SingleSelectTypeOptionPB>(parser, controller); } @@ -130,6 +136,7 @@ class SingleSelectTypeOptionSerde extends TypeOptionSerde<SingleSelectTypeOption // Multi-select export function makeMultiSelectTypeOptionContext(controller: TypeOptionController): MultiSelectTypeOptionContext { const parser = new MultiSelectTypeOptionSerde(); + return new TypeOptionContext<MultiSelectTypeOptionPB>(parser, controller); } @@ -148,6 +155,7 @@ class MultiSelectTypeOptionSerde extends TypeOptionSerde<MultiSelectTypeOptionPB // Checklist export function makeChecklistTypeOptionContext(controller: TypeOptionController): ChecklistTypeOptionContext { const parser = new ChecklistTypeOptionSerde(); + return new TypeOptionContext<ChecklistTypeOptionPB>(parser, controller); } @@ -184,8 +192,10 @@ export class TypeOptionContext<T> { getTypeOption = async (): Promise<Result<T, FlowyError>> => { const result = await this.controller.getTypeOption(); + if (result.ok) { const typeOption = this.parser.deserialize(result.val.type_option_data); + this.typeOption = Some(typeOption); return Ok(typeOption); } else { diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/type_option/type_option_controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/type_option/type_option_controller.ts index a13196a236d1..a315140d9485 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/type_option/type_option_controller.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/type_option/type_option_controller.ts @@ -48,18 +48,23 @@ export class TypeOptionController { throw Error('Unexpected empty type option data. Should call initialize first'); } } + return new FieldInfo(this.typeOptionData.val.field); }; switchToField = async (fieldType: FieldType) => { const result = await this.typeOptionBackendSvc.updateTypeOptionType(this.fieldId, fieldType); + if (result.ok) { const getResult = await this.typeOptionBackendSvc.getTypeOption(this.fieldId, fieldType); + if (getResult.ok) { this.updateTypeOptionData(getResult.val); } + return getResult; } + return result; }; @@ -114,6 +119,7 @@ export class TypeOptionController { if (this.fieldBackendSvc === undefined) { Log.error('Unexpected empty field backend service'); } + return this.fieldBackendSvc?.deleteField(); }; @@ -121,6 +127,7 @@ export class TypeOptionController { if (this.fieldBackendSvc === undefined) { Log.error('Unexpected empty field backend service'); } + return this.fieldBackendSvc?.duplicateField(); }; @@ -130,6 +137,7 @@ export class TypeOptionController { if (result.ok) { this.updateTypeOptionData(result.val); } + return result; }); }; @@ -139,6 +147,7 @@ export class TypeOptionController { if (result.ok) { this.updateTypeOptionData(result.val); } + return result; }); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/group/group_controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/group/group_controller.ts index 71e498bf9ac5..0a5c8de4afef 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/group/group_controller.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/group/group_controller.ts @@ -1,11 +1,4 @@ -import { - DatabaseNotification, - FlowyError, - GroupPB, - GroupRowsNotificationPB, - RowMetaPB, - RowPB, -} from '@/services/backend'; +import { DatabaseNotification, FlowyError, GroupPB, GroupRowsNotificationPB, RowMetaPB } from '@/services/backend'; import { ChangeNotifier } from '$app/utils/change_notifier'; import { None, Ok, Option, Result, Some } from 'ts-results'; import { DatabaseNotificationObserver } from '../notifications/observer'; @@ -48,6 +41,7 @@ export class DatabaseGroupController { if (this.group.rows.length < index) { return None; } + return Some(this.group.rows[index]); }; @@ -56,6 +50,7 @@ export class DatabaseGroupController { onRowsChanged: (result) => { if (result.ok) { const changeset = result.val; + // Delete changeset.deleted_rows.forEach((deletedRowId) => { this.group.rows = this.group.rows.filter((row) => row.id !== deletedRowId); @@ -65,6 +60,7 @@ export class DatabaseGroupController { // Insert changeset.inserted_rows.forEach((insertedRow) => { let index: number | undefined = insertedRow.index; + if (insertedRow.has_index && this.group.rows.length > insertedRow.index) { this.group.rows.splice(index, 0, insertedRow.row_meta); } else { @@ -82,6 +78,7 @@ export class DatabaseGroupController { // Update changeset.updated_rows.forEach((updatedRow) => { const index = this.group.rows.findIndex((row) => row.id === updatedRow.id); + if (index !== -1) { this.group.rows[index] = updatedRow; this.callbacks?.onUpdateRow(this.group.group_id, updatedRow); @@ -134,6 +131,7 @@ class GroupDataObserver { } else { this.notifier?.notify(result); } + return; default: break; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/group/group_observer.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/group/group_observer.ts index 46cfb1096197..796735114594 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/group/group_observer.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/group/group_observer.ts @@ -33,6 +33,7 @@ export class DatabaseGroupObserver { } else { this.groupByNotifier?.notify(result); } + break; case DatabaseNotification.DidUpdateNumOfGroups: if (result.ok) { @@ -40,6 +41,7 @@ export class DatabaseGroupObserver { } else { this.groupChangesetNotifier?.notify(result); } + break; default: break; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/notifications/observer.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/notifications/observer.ts index 04a819d88405..210258cde892 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/notifications/observer.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/notifications/observer.ts @@ -11,6 +11,7 @@ export class DatabaseNotificationObserver extends AFNotificationObserver<Databas callback: params.parserHandler, id: params.id, }); + super(parser); } } diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/notifications/parser.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/notifications/parser.ts index 264a6036bd09..caac06d1baf9 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/notifications/parser.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/notifications/parser.ts @@ -10,6 +10,7 @@ export class DatabaseNotificationParser extends NotificationParser<DatabaseNotif params.callback, (ty) => { const notification = DatabaseNotification[ty]; + if (isDatabaseNotification(notification)) { return DatabaseNotification[notification]; } else { diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/row/row_cache.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/row/row_cache.ts index 8cf7bc623048..a353fa6198a4 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/row/row_cache.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/row/row_cache.ts @@ -1,9 +1,7 @@ import { - RowPB, InsertedRowPB, UpdatedRowPB, RowIdPB, - OptionalRowPB, RowsChangePB, RowsVisibilityChangePB, ReorderSingleRowPB, @@ -13,7 +11,7 @@ import { ChangeNotifier } from '$app/utils/change_notifier'; import { FieldInfo } from '../field/field_controller'; import { CellCache, CellCacheKey } from '../cell/cell_cache'; import { CellIdentifier } from '../cell/cell_bd_svc'; -import { DatabaseEventGetRow, DatabaseEventGetRowMeta } from '@/services/backend/events/flowy-database2'; +import { DatabaseEventGetRowMeta } from '@/services/backend/events/flowy-database2'; import { None, Option, Some } from 'ts-results'; import { Log } from '$app/utils/log'; @@ -40,10 +38,12 @@ export class RowCache { loadCells = async (rowId: string): Promise<CellByFieldId> => { const opRow = this.rowList.getRow(rowId); + if (opRow.some) { return this._toCellMap(opRow.val.row.id, this.getFieldInfos()); } else { const rowResult = await this._loadRow(rowId); + if (rowResult.ok) { this._refreshRow(rowResult.val); return this._toCellMap(rowId, this.getFieldInfos()); @@ -101,6 +101,7 @@ export class RowCache { applyReorderSingleRow = (reorderRow: ReorderSingleRowPB) => { const rowInfo = this.rowList.getRow(reorderRow.row_id); + if (rowInfo !== undefined) { this.rowList.move({ rowId: reorderRow.row_id, fromIndex: reorderRow.old_index, toIndex: reorderRow.new_index }); this.notifier.withChange(RowChangedReason.ReorderSingleRow, reorderRow.row_id); @@ -109,24 +110,29 @@ export class RowCache { private _refreshRow = (updatedRow: RowMetaPB) => { const option = this.rowList.getRowWithIndex(updatedRow.id); + if (option.some) { const { rowInfo, index } = option.val; + this.rowList.remove(rowInfo.row.id); this.rowList.insert(index, rowInfo.copyWith({ row: updatedRow })); } else { const newRowInfo = new RowInfo(this.viewId, this.getFieldInfos(), updatedRow); + this.rowList.push(newRowInfo); } }; private _loadRow = (rowId: string) => { const payload = RowIdPB.fromObject({ view_id: this.viewId, row_id: rowId }); + return DatabaseEventGetRowMeta(payload); }; private _deleteRows = (rowIds: string[]) => { rowIds.forEach((rowId) => { const deletedRow = this.rowList.remove(rowId); + if (deletedRow !== undefined) { this.notifier.withChange(RowChangedReason.Delete, deletedRow.rowInfo.row.id); } @@ -137,6 +143,7 @@ export class RowCache { rows.forEach((insertedRow) => { const rowInfo = this._toRowInfo(insertedRow.row_meta); const insertedIndex = this.rowList.insert(insertedRow.index, rowInfo); + if (insertedIndex !== undefined) { this.notifier.withChange(RowChangedReason.Insert, insertedIndex.rowId); } @@ -149,9 +156,11 @@ export class RowCache { } const rowInfos: RowInfo[] = []; + updatedRows.forEach((updatedRow) => { updatedRow.field_ids.forEach((fieldId) => { const key = new CellCacheKey(fieldId, updatedRow.row_meta.id); + this.cellCache.remove(key); }); @@ -159,6 +168,7 @@ export class RowCache { }); const updatedIndexs = this.rowList.insertRows(rowInfos); + updatedIndexs.forEach((row) => { this.notifier.withChange(RowChangedReason.Update, row.rowId); }); @@ -167,6 +177,7 @@ export class RowCache { private _hideRows = (rowIds: string[]) => { rowIds.forEach((rowId) => { const deletedRow = this.rowList.remove(rowId); + if (deletedRow !== undefined) { this.notifier.withChange(RowChangedReason.Delete, deletedRow.rowInfo.row.id); } @@ -196,6 +207,7 @@ export class RowCache { fieldInfos.forEach((fieldInfo) => { const identifier = new CellIdentifier(this.viewId, rowId, fieldInfo.field.id, fieldInfo.field.field_type); + cellIdentifierByFieldId.set(fieldInfo.field.id, identifier); }); @@ -213,6 +225,7 @@ class RowList { getRow = (rowId: string): Option<RowInfo> => { const rowInfo = this._rowInfoByRowId.get(rowId); + if (rowInfo === undefined) { return None; } else { @@ -221,23 +234,29 @@ class RowList { }; getRowWithIndex = (rowId: string): Option<{ rowInfo: RowInfo; index: number }> => { const rowInfo = this._rowInfoByRowId.get(rowId); + if (rowInfo !== undefined) { const index = this._rowInfos.indexOf(rowInfo, 0); + return Some({ rowInfo: rowInfo, index: index }); } + return None; }; indexOfRow = (rowId: string): number => { const rowInfo = this._rowInfoByRowId.get(rowId); + if (rowInfo !== undefined) { return this._rowInfos.indexOf(rowInfo, 0); } + return -1; }; push = (rowInfo: RowInfo) => { const index = this.indexOfRow(rowInfo.row.id); + if (index !== -1) { this._rowInfos.splice(index, 1, rowInfo); } else { @@ -249,8 +268,10 @@ class RowList { remove = (rowId: string): DeletedRow | undefined => { const result = this.getRowWithIndex(rowId); + if (result.some) { const { rowInfo, index } = result.val; + this._rowInfoByRowId.delete(rowInfo.row.id); this._rowInfos.splice(index, 1); return new DeletedRow(index, rowInfo); @@ -263,13 +284,16 @@ class RowList { const rowId = newRowInfo.row.id; // Calibrate where to insert let insertedIndex = insertIndex; + if (this._rowInfos.length <= insertedIndex) { insertedIndex = this._rowInfos.length; } + const result = this.getRowWithIndex(rowId); if (result.some) { const { index } = result.val; + // remove the old row info this._rowInfos.splice(index, 1); // insert the new row info to the insertedIndex @@ -285,8 +309,10 @@ class RowList { insertRows = (rowInfos: RowInfo[]) => { const map = new Map<string, InsertedRow>(); + rowInfos.forEach((rowInfo) => { const index = this.indexOfRow(rowInfo.row.id); + if (index !== -1) { this._rowInfos.splice(index, 1, rowInfo); this._rowInfoByRowId.set(rowInfo.row.id, rowInfo); @@ -299,8 +325,10 @@ class RowList { move = (params: { rowId: string; fromIndex: number; toIndex: number }) => { const currentIndex = this.indexOfRow(params.rowId); + if (currentIndex !== -1 && currentIndex !== params.toIndex) { const rowInfo = this.remove(params.rowId)?.rowInfo; + if (rowInfo !== undefined) { this.insert(params.toIndex, rowInfo); } @@ -312,6 +340,7 @@ class RowList { this._rowInfos = []; rowIds.forEach((rowId) => { const rowInfo = this._rowInfoByRowId.get(rowId); + if (rowInfo !== undefined) { this._rowInfos.push(rowInfo); } @@ -324,6 +353,7 @@ class RowList { setFieldInfos = (fieldInfos: readonly FieldInfo[]) => { const newRowInfos: RowInfo[] = []; + this._rowInfos.forEach((rowInfo) => { newRowInfos.push(rowInfo.copyWith({ fieldInfos: fieldInfos })); }); @@ -371,6 +401,7 @@ export class RowChangeNotifier extends ChangeNotifier<RowChanged> { withChange = (reason: RowChangedReason, rowId?: string) => { const newChange = new RowChanged(reason, rowId); + if (this._currentChanged !== newChange) { this._currentChanged = newChange; this.notify(this._currentChanged); diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/database_view_cache.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/database_view_cache.ts index e099d3ba6bef..072dae5189a1 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/database_view_cache.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/database_view_cache.ts @@ -1,7 +1,7 @@ import { DatabaseViewRowsObserver } from './view_row_observer'; import { RowCache, RowInfo } from '../row/row_cache'; import { FieldController } from '../field/field_controller'; -import { RowMetaPB, RowPB } from '@/services/backend'; +import { RowMetaPB } from '@/services/backend'; export class DatabaseViewCache { private readonly rowsObserver: DatabaseViewRowsObserver; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/view_row_observer.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/view_row_observer.ts index 7bc66f9115aa..2b7a67e4f183 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/view_row_observer.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/view_row_observer.ts @@ -47,6 +47,7 @@ export class DatabaseViewRowsObserver { } else { this.rowsVisibilityNotifier.notify(result); } + break; case DatabaseNotification.DidUpdateViewRows: if (result.ok) { @@ -54,6 +55,7 @@ export class DatabaseViewRowsObserver { } else { this.rowsNotifier.notify(result); } + break; case DatabaseNotification.DidReorderRows: if (result.ok) { @@ -61,6 +63,7 @@ export class DatabaseViewRowsObserver { } else { this.reorderRowsNotifier.notify(result); } + break; case DatabaseNotification.DidReorderSingleRow: if (result.ok) { @@ -68,6 +71,7 @@ export class DatabaseViewRowsObserver { } else { this.reorderSingleRowNotifier.notify(result); } + break; default: break; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/notifications/observer.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/notifications/observer.ts index ee149e571899..0d7a3d97d1b8 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/notifications/observer.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/notifications/observer.ts @@ -11,6 +11,7 @@ export class DocumentNotificationObserver extends AFNotificationObserver<Documen callback: params.parserHandler, id: params.viewId, }); + super(parser); } } diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/notifications/parser.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/notifications/parser.ts index e29956803b32..8609148153c4 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/notifications/parser.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/document/notifications/parser.ts @@ -10,6 +10,7 @@ export class DocumentNotificationParser extends NotificationParser<DocumentNotif params.callback, (ty) => { const notification = DocumentNotification[ty]; + if (isDocumentNotification(notification)) { return DocumentNotification[notification]; } else { diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/user/user_bd_svc.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/user/user_bd_svc.ts index 38f5850f3f6d..7306fcfaea67 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/user/user_bd_svc.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/user/user_bd_svc.ts @@ -1,8 +1,6 @@ import { nanoid } from '@reduxjs/toolkit'; import { AppearanceSettingsPB, - AuthTypePB, - ThemeModePB, UserEventGetAppearanceSetting, UserEventGetUserProfile, UserEventGetUserSetting, @@ -13,7 +11,6 @@ import { UserEventUpdateUserProfile, } from '@/services/backend/events/flowy-user'; import { - BlockActionPB, CreateWorkspacePayloadPB, SignInPayloadPB, SignUpPayloadPB, @@ -72,6 +69,7 @@ export class UserBackendService { openWorkspace = (workspaceId: string) => { const payload = WorkspaceIdPB.fromObject({ value: workspaceId }); + return FolderEventOpenWorkspace(payload); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/page/page_controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/page/page_controller.ts index 5bdea28e3915..95d0a2a5b05b 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/page/page_controller.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/page/page_controller.ts @@ -11,8 +11,8 @@ export class PageController { // } - dispose = () => { - this.observer.unsubscribe(); + dispose = async () => { + await this.observer.unsubscribe(); }; createPage = async (params: { name: string; layout: ViewLayoutPB }): Promise<string> => { @@ -56,7 +56,6 @@ export class PageController { getPage = async (id?: string): Promise<Page> => { const result = await this.backendService.getPage(id || this.id); - if (result.ok) { return parserViewPBToPage(result.val); } @@ -76,8 +75,10 @@ export class PageController { const res = ViewPB.deserializeBinary(payload); const page = parserViewPBToPage(ViewPB.deserializeBinary(payload)); const childPages = res.child_views.map(parserViewPBToPage); + callbacks.onPageChanged?.(page, childPages); }; + await this.observer.subscribeView(this.id, { didUpdateView, }); diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/trash/controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/trash/controller.ts index 5b84faa37c6a..86563a0ab7a8 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/trash/controller.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/trash/controller.ts @@ -7,20 +7,20 @@ export class TrashController { private readonly backendService: TrashBackendService = new TrashBackendService(); - subscribe = (callbacks: { onTrashChanged?: (trash: TrashPB[]) => void }) => { + subscribe = async (callbacks: { onTrashChanged?: (trash: TrashPB[]) => void }) => { const didUpdateTrash = (payload: Uint8Array) => { const res = RepeatedTrashPB.deserializeBinary(payload); callbacks.onTrashChanged?.(res.items); }; - this.observer.subscribeTrash({ + await this.observer.subscribeTrash({ didUpdateTrash, }); }; - dispose = () => { - this.observer.unsubscribe(); + dispose = async () => { + await this.observer.unsubscribe(); }; getTrash = async () => { const res = await this.backendService.getTrash(); diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/workspace_controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/workspace_controller.ts index 9ff2856bfb66..56b7003c422e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/workspace_controller.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/workspace_controller.ts @@ -1,6 +1,6 @@ import { WorkspaceBackendService } from '$app/stores/effects/workspace/workspace_bd_svc'; import { WorkspaceObserver } from '$app/stores/effects/workspace/workspace_observer'; -import { CreateViewPayloadPB, RepeatedViewPB } from "@/services/backend"; +import { CreateViewPayloadPB, RepeatedViewPB } from '@/services/backend'; import { PageBackendService } from '$app/stores/effects/workspace/page/page_bd_svc'; import { Page, parserViewPBToPage } from '$app_reducers/pages/slice'; @@ -13,8 +13,8 @@ export class WorkspaceController { this.backendService = new WorkspaceBackendService(); } - dispose = () => { - this.observer.unsubscribe(); + dispose = async () => { + await this.observer.unsubscribe(); }; open = async () => { @@ -37,16 +37,15 @@ export class WorkspaceController { return Promise.reject(result.err); }; - subscribe = async (callbacks: { - onChildPagesChanged?: (childPages: Page[]) => void; - }) => { - + subscribe = async (callbacks: { onChildPagesChanged?: (childPages: Page[]) => void }) => { const didUpdateWorkspace = (payload: Uint8Array) => { const res = RepeatedViewPB.deserializeBinary(payload).items; + callbacks.onChildPagesChanged?.(res.map(parserViewPBToPage)); - } + }; + await this.observer.subscribeWorkspace(this.workspaceId, { - didUpdateWorkspace + didUpdateWorkspace, }); }; @@ -71,6 +70,4 @@ export class WorkspaceController { return []; }; - - } diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/workspace_manager_controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/workspace_manager_controller.ts index d717a0749562..67bfdd92eedf 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/workspace_manager_controller.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/workspace_manager_controller.ts @@ -1,5 +1,5 @@ import { WorkspaceBackendService } from './workspace_bd_svc'; -import { CreateWorkspacePayloadPB, RepeatedWorkspacePB } from '@/services/backend'; +import { CreateWorkspacePayloadPB } from '@/services/backend'; import { WorkspaceItem } from '$app_reducers/workspace/slice'; import { WorkspaceObserver } from '$app/stores/effects/workspace/workspace_observer'; @@ -51,6 +51,7 @@ export class WorkspaceManagerController { if (result.ok) { const workspace = result.val; + return { id: workspace.id, name: workspace.name, @@ -64,8 +65,8 @@ export class WorkspaceManagerController { await this.observer.unsubscribe(); }; - private didCreateWorkspace = (payload: Uint8Array) => { - const data = RepeatedWorkspacePB.deserializeBinary(payload); + private didCreateWorkspace = () => { + // const data = RepeatedWorkspacePB.deserializeBinary(payload); // onWorkspacesChanged(data.toObject().items); }; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/block-draggable/slice.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/block-draggable/slice.ts index f5c946209570..123225a64de2 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/block-draggable/slice.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/block-draggable/slice.ts @@ -81,17 +81,21 @@ export const blockDraggableSlice = createSlice({ state.draggingPosition = draggingPosition; state.dropContext = dropContext; - const moveDistance = Math.sqrt( - Math.pow(draggingPosition.x - state.startDraggingPosition!.x, 2) + - Math.pow(draggingPosition.y - state.startDraggingPosition!.y, 2) - ); + const { startDraggingPosition } = state; + + const moveDistance = startDraggingPosition + ? Math.sqrt( + Math.pow(draggingPosition.x - startDraggingPosition.x, 2) + + Math.pow(draggingPosition.y - startDraggingPosition.y, 2) + ) + : 0; state.dropId = dropId; state.insertType = insertType; state.dragShadowVisible = moveDistance > DRAG_DISTANCE_THRESHOLD; }, - endDrag: (state) => { + endDrag: () => { return initialState; }, }, diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/current-user/slice.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/current-user/slice.ts index f7a245ad60d2..267c3e68f130 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/current-user/slice.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/current-user/slice.ts @@ -1,5 +1,4 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { nanoid } from 'nanoid'; import { WorkspaceSettingPB } from '@/services/backend/models/flowy-folder2/workspace'; import { UserSetting } from '$app/interfaces'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/duplicate.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/duplicate.ts index cb6334aaf755..7a95846059fa 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/duplicate.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/duplicate.ts @@ -21,7 +21,7 @@ export const duplicateBelowNodeThunk = createAsyncThunk( if (!duplicateActions) return; await controller.applyActions(duplicateActions.actions); - dispatch( + await dispatch( setRectSelectionThunk({ docId, selection: [duplicateActions.newNodeId], diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/insert.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/insert.ts index 76cdca31ba20..5f19743be3d2 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/insert.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/insert.ts @@ -14,7 +14,7 @@ export const insertAfterNodeThunk = createAsyncThunk( id: string; controller: DocumentController; type: BlockType; - data?: BlockData<any>; + data?: BlockData; defaultDelta?: Delta; }, thunkAPI @@ -22,8 +22,7 @@ export const insertAfterNodeThunk = createAsyncThunk( const { controller, id, type, data, defaultDelta } = payload; const { getState } = thunkAPI; const state = getState() as RootState; - const docId = controller.documentId; - const documentState = state[DOCUMENT_NAME][docId]; + const documentState = state[DOCUMENT_NAME][controller.documentId]; const node = documentState.nodes[id]; if (!node) return; @@ -34,8 +33,9 @@ export const insertAfterNodeThunk = createAsyncThunk( const actions = []; let newNodeId; const deltaOperator = new BlockDeltaOperator(documentState, controller); + if (type === BlockType.DividerBlock) { - const newNode = newBlock<any>(type, parentId, data); + const newNode = newBlock(type, parentId, data); actions.push(controller.getInsertAction(newNode, node.id)); newNodeId = newNode.id; @@ -64,7 +64,7 @@ export const insertAfterNodeThunk = createAsyncThunk( }) ); } else { - const newNode = newBlock<any>(type, parentId, data); + const newNode = newBlock(type, parentId, data); actions.push(controller.getInsertAction(newNode, node.id)); newNodeId = newNode.id; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/update.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/update.ts index 72f68e1eddad..6e211cc72810 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/update.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/blocks/update.ts @@ -8,7 +8,7 @@ import { updatePageName } from '$app_reducers/pages/async_actions'; import { getDeltaText } from '$app/utils/document/delta'; import { BlockDeltaOperator } from '$app/utils/document/block_delta'; import { openMention, closeMention } from '$app_reducers/document/async-actions/mention'; -import {slashCommandActions} from "$app_reducers/document/slice"; +import { slashCommandActions } from '$app_reducers/document/slice'; const updateNodeDeltaAfterThunk = createAsyncThunk( 'document/updateNodeDeltaAfter', @@ -27,9 +27,11 @@ const updateNodeDeltaAfterThunk = createAsyncThunk( if (insertOps.length === 1) { const char = insertOps[0].insert; + if (char === '@' && (oldText.endsWith(' ') || oldText === '')) { - dispatch(openMention({ docId })); + await dispatch(openMention({ docId })); } + if (char === '/') { dispatch( slashCommandActions.openSlashCommand({ @@ -42,15 +44,13 @@ const updateNodeDeltaAfterThunk = createAsyncThunk( if (deleteOps.length === 1) { if (deleteText === '@') { - dispatch(closeMention({ docId })); + await dispatch(closeMention({ docId })); } + if (deleteText === '/') { - dispatch( - slashCommandActions.closeSlashCommand(docId) - ); + dispatch(slashCommandActions.closeSlashCommand(docId)); } } - } ); @@ -92,7 +92,7 @@ export const updateNodeDataThunk = createAsyncThunk< void, { id: string; - data: Partial<BlockData<any>>; + data: Partial<BlockData>; controller: DocumentController; } >('document/updateNodeDataExceptDelta', async (payload, thunkAPI) => { diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/copy_paste.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/copy_paste.ts index 1c7b7d10bfac..97392462ffc2 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/copy_paste.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/copy_paste.ts @@ -9,7 +9,7 @@ export const copyThunk = createAsyncThunk< controller: DocumentController; setClipboardData: (data: BlockCopyData) => void; } ->('document/copy', async (payload, thunkAPI) => { +>('document/copy', async () => { // TODO: Migrate to Rust implementation. }); @@ -29,6 +29,6 @@ export const pasteThunk = createAsyncThunk< data: BlockCopyData; controller: DocumentController; } ->('document/paste', async (payload, thunkAPI) => { +>('document/paste', async () => { // TODO: Migrate to Rust implementation. }); diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/keydown.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/keydown.ts index 343001d88e7f..449438b559ce 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/keydown.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/keydown.ts @@ -71,7 +71,7 @@ export const backspaceDeleteActionForBlockThunk = createAsyncThunk( length: 0, }; - dispatch( + await dispatch( setCursorRangeThunk({ docId, blockId: caret.id, @@ -115,7 +115,6 @@ export const enterActionForBlockThunk = createAsyncThunk( ); }); const isDocumentTitle = !node.parent; - let newLineId; const delta = deltaOperator.getDeltaWithBlockId(node.id); @@ -126,7 +125,7 @@ export const enterActionForBlockThunk = createAsyncThunk( return; } - newLineId = await deltaOperator.splitText( + const newLineId = await deltaOperator.splitText( { id: node.id, index: caret.index, @@ -138,7 +137,7 @@ export const enterActionForBlockThunk = createAsyncThunk( ); if (!newLineId) return; - dispatch( + await dispatch( setCursorRangeThunk({ docId, blockId: newLineId, diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/mention.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/mention.ts index 4ec94ea731df..f9379a1b7846 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/mention.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/mention.ts @@ -70,11 +70,15 @@ export const formatMention = createAsyncThunk( const nodeDelta = deltaOperator.getDeltaWithBlockId(blockId); if (!nodeDelta) return; - const diffDelta = new Delta().retain(index).delete(charLength).insert('@',{ mention: { type, [type]: value } }); + const diffDelta = new Delta() + .retain(index) + .delete(charLength) + .insert('@', { mention: { type, [type]: value } }); const applyTextDeltaAction = deltaOperator.getApplyDeltaAction(blockId, diffDelta); + if (!applyTextDeltaAction) return; await controller.applyActions([applyTextDeltaAction]); - dispatch( + await dispatch( setCursorRangeThunk({ docId, blockId, diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/menu.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/menu.ts index 7c75ea6a771a..1f1f50321c57 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/menu.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/menu.ts @@ -1,15 +1,10 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; -import { BlockData, BlockType } from '$app/interfaces/document'; +import { BlockType } from '$app/interfaces/document'; import { insertAfterNodeThunk } from '$app_reducers/document/async-actions/blocks'; import { DocumentController } from '$app/stores/effects/document/document_controller'; import { rangeActions, slashCommandActions } from '$app_reducers/document/slice'; -import { turnToBlockThunk } from '$app_reducers/document/async-actions/turn_to'; -import { blockConfig } from '$app/constants/document/config'; -import Delta, { Op } from 'quill-delta'; -import { getDeltaText } from '$app/utils/document/delta'; +import Delta from 'quill-delta'; import { RootState } from '$app/stores/store'; -import { DOCUMENT_NAME, RANGE_NAME } from '$app/constants/document/name'; -import { blockEditActions } from '$app_reducers/document/block_edit_slice'; import { BlockDeltaOperator } from '$app/utils/document/block_delta'; /** diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/range.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/range.ts index fb980d16cd95..14b8e1e00d7f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/range.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/range.ts @@ -156,7 +156,7 @@ export const deleteRangeAndInsertThunk = createAsyncThunk( ); if (!id) return; - dispatch( + await dispatch( setCursorRangeThunk({ docId, blockId: id, @@ -211,7 +211,7 @@ export const deleteRangeAndInsertEnterThunk = createAsyncThunk( ); if (!newLineId) return; - dispatch( + await dispatch( setCursorRangeThunk({ docId, blockId: newLineId, diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/temporary.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/temporary.ts index fbc33dc73929..7864c6638725 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/temporary.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/temporary.ts @@ -1,7 +1,6 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { RootState } from '$app/stores/store'; import { DOCUMENT_NAME, EQUATION_PLACEHOLDER, RANGE_NAME, TEMPORARY_NAME } from '$app/constants/document/name'; -import { getDeltaByRange, getDeltaText } from '$app/utils/document/delta'; import Delta from 'quill-delta'; import { TemporaryState, TemporaryType } from '$app/interfaces/document'; import { temporaryActions } from '$app_reducers/document/temporary_slice'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/turn_to.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/turn_to.ts index 18d368727b08..dd66cfd2abe3 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/turn_to.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/turn_to.ts @@ -20,7 +20,7 @@ import { DOCUMENT_NAME, RANGE_NAME } from '$app/constants/document/name'; */ export const turnToBlockThunk = createAsyncThunk( 'document/turnToBlock', - async (payload: { id: string; controller: DocumentController; type: BlockType; data: BlockData<any> }, thunkAPI) => { + async (payload: { id: string; controller: DocumentController; type: BlockType; data: BlockData }, thunkAPI) => { const { id, controller, type, data } = payload; const docId = controller.documentId; const { dispatch, getState } = thunkAPI; @@ -46,13 +46,13 @@ export const turnToBlockThunk = createAsyncThunk( if (type === BlockType.EquationBlock) { data.formula = deltaOperator.getDeltaText(delta); - const block = newBlock<any>(type, parent.id, data); + const block = newBlock(type, parent.id, data); insertActions.push(controller.getInsertAction(block, node.id)); caretId = block.id; caretIndex = 0; } else if (type === BlockType.DividerBlock) { - const block = newBlock<any>(type, parent.id, data); + const block = newBlock(type, parent.id, data); insertActions.push(controller.getInsertAction(block, node.id)); const nodeId = generateId(); @@ -97,7 +97,7 @@ export const turnToBlockThunk = createAsyncThunk( // submit actions await controller.applyActions([...insertActions, ...moveChildrenActions, deleteAction]); - dispatch( + await dispatch( setCursorRangeThunk({ docId, blockId: caretId, diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/async_actions.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/async_actions.ts index 08aed7199031..901a7a65ebe7 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/async_actions.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/async_actions.ts @@ -58,35 +58,17 @@ export const movePageThunk = createAsyncThunk( } ); -export const updatePageName = createAsyncThunk( - 'pages/updateName', - async ( - payload: { - id: string; - name: string; - }, - thunkAPI - ) => { - const controller = new PageController(payload.id); +export const updatePageName = createAsyncThunk('pages/updateName', async (payload: { id: string; name: string }) => { + const controller = new PageController(payload.id); - await controller.updatePage({ - id: payload.id, - name: payload.name, - }); - } -); + await controller.updatePage({ + id: payload.id, + name: payload.name, + }); +}); -export const updatePageIcon = createAsyncThunk( - 'pages/updateIcon', - async ( - payload: { - id: string; - icon?: PageIcon; - }, - thunkAPI - ) => { - const controller = new PageController(payload.id); +export const updatePageIcon = createAsyncThunk('pages/updateIcon', async (payload: { id: string; icon?: PageIcon }) => { + const controller = new PageController(payload.id); - await controller.updatePageIcon(payload.icon); - } -); + await controller.updatePageIcon(payload.icon); +}); diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts index 1aa0c4aa79f8..5d7d63e003fa 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts @@ -88,8 +88,10 @@ export const pagesSlice = createSlice({ expandPage(state, action: PayloadAction<string>) { const id = action.payload; + state.expandedIdMap[id] = true; const ids = Object.keys(state.expandedIdMap).filter(id => state.expandedIdMap[id]); + storeExpandedPageIds(ids); }, @@ -98,6 +100,7 @@ export const pagesSlice = createSlice({ state.expandedIdMap[id] = false; const ids = Object.keys(state.expandedIdMap).filter(id => state.expandedIdMap[id]); + storeExpandedPageIds(ids); }, }, diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/async_queue.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/async_queue.ts index a058032fed84..7e673506de0b 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/async_queue.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/async_queue.ts @@ -21,6 +21,10 @@ export class AsyncQueue<T = unknown> { const item = this.queue.shift(); + if (!item) { + return; + } + this.isProcessing = true; const executeFn = async (item: T) => { @@ -34,7 +38,7 @@ export class AsyncQueue<T = unknown> { } }; - executeFn(item!); + void executeFn(item); } private async processItem(item: T): Promise<void> { diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/change_notifier.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/change_notifier.ts index df81d11c4799..57d9f2a3704c 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/change_notifier.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/change_notifier.ts @@ -12,6 +12,7 @@ export class ChangeNotifier<T> { if (this.isUnsubscribe) { return null; } + return this.subject.asObservable(); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/document/action.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/document/action.ts index 8921e2b4d83a..31522469990a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/document/action.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/document/action.ts @@ -1,29 +1,14 @@ -import { - BlockType, - ControllerAction, - DocumentState, - NestedBlock, - RangeState, - RangeStatic, - SplitRelationship, -} from '$app/interfaces/document'; -import { getNextLineId, getPrevLineId, newBlock } from '$app/utils/document/block'; -import Delta from 'quill-delta'; -import { RootState } from '$app/stores/store'; +import { ControllerAction, DocumentState, RangeState, RangeStatic } from '$app/interfaces/document'; +import { getNextLineId, newBlock } from '$app/utils/document/block'; import { DocumentController } from '$app/stores/effects/document/document_controller'; -import { blockConfig } from '$app/constants/document/config'; import { caretInBottomEdgeByDelta, caretInTopEdgeByDelta, - getAfterExtentDeltaByRange, - getBeofreExtentDeltaByRange, - getDeltaText, getIndexRelativeEnter, getLastLineIndex, transformIndexToNextLine, transformIndexToPrevLine, } from '$app/utils/document/delta'; -import { DOCUMENT_NAME, RANGE_NAME } from '$app/constants/document/name'; import { BlockDeltaOperator } from '$app/utils/document/block_delta'; export function getMiddleIds(document: DocumentState, startId: string, endId: string) { @@ -80,7 +65,7 @@ export function getLeftCaretByRange(rangeState: RangeState) { } export function getRightCaretByRange(rangeState: RangeState) { - const { anchor, focus, ranges, caret } = rangeState; + const { anchor, focus, ranges } = rangeState; if (!anchor || !focus) return; const isForward = anchor.point.y < focus.point.y; @@ -180,7 +165,7 @@ export function getDuplicateActions( if (!node) return; // duplicate new node - const newNode = newBlock<any>(node.type, parentId, { + const newNode = newBlock(node.type, parentId, { ...node.data, }); diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/document/block_delta.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/document/block_delta.ts index cef85413d8fd..4b10d9c9f65e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/document/block_delta.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/document/block_delta.ts @@ -1,4 +1,4 @@ -import { BlockData, BlockType, DocumentState, NestedBlock, SplitRelationship } from '$app/interfaces/document'; +import { BlockData, BlockType, DocumentState, SplitRelationship } from '$app/interfaces/document'; import { generateId, getNextLineId, getPrevLineId } from '$app/utils/document/block'; import { DocumentController } from '$app/stores/effects/document/document_controller'; import Delta, { Op } from 'quill-delta'; @@ -92,7 +92,7 @@ export class BlockDeltaOperator { parentId: string; type: BlockType; prevId: string | null; - data?: BlockData<any>; + data?: BlockData; }) => { const externalId = generateId(); const block = { diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/document/format.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/document/format.ts index d156b6a06615..66fe95caaf85 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/document/format.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/document/format.ts @@ -24,5 +24,6 @@ export function parseFormat(e: KeyboardEvent | React.KeyboardEvent<HTMLDivElemen } else if (isHotkey(Keyboard.keys.FORMAT.CODE, e)) { return TextAction.Code; } + return null; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/document/node.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/document/node.ts index d50cb04b2b35..5d7dc8a565aa 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/document/node.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/document/node.ts @@ -25,8 +25,8 @@ export function findFirstTextNode(node: Node): Node | null { const children = node.childNodes; - for (let i = 0; i < children.length; i++) { - const textNode = findFirstTextNode(children[i]); + for (const child of children) { + const textNode = findFirstTextNode(child); if (textNode) { return textNode; diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/document/quill_editor.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/document/quill_editor.ts index e29c0fd72d02..7b9690b89454 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/document/quill_editor.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/document/quill_editor.ts @@ -46,6 +46,7 @@ export function adaptDeltaForQuill(inputOps: Op[], isOutput = false): Op[] { if (isOutput) { const newText = text.slice(0, -1); + if (newText !== '') { newOps[lastOpIndex] = { ...lastOp, insert: newText }; } else { diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/document/slate_editor.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/document/slate_editor.ts index 2f8cfac126cd..619dcf06f017 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/document/slate_editor.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/document/slate_editor.ts @@ -107,6 +107,7 @@ export function convertToSlateValue(delta: Delta): Descendant[] { export function convertToDelta(slateValue: Descendant[]) { const ops = (slateValue[0] as Element).children.map((child) => { const { text, ...attributes } = child as Text; + return { insert: text, attributes, diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/document/toolbar.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/document/toolbar.ts index 93546f33b239..fa8318ac4226 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/document/toolbar.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/document/toolbar.ts @@ -1,6 +1,7 @@ export function calcToolbarPosition(toolbarDom: HTMLDivElement, node: Element, container: HTMLDivElement) { const domSelection = window.getSelection(); let domRange; + if (domSelection?.rangeCount === 0) { return; } else { @@ -18,6 +19,7 @@ export function calcToolbarPosition(toolbarDom: HTMLDivElement, node: Element, c const rightBound = containerRect.right; const rightThreshold = 20; + if (left < leftBound) { left = leftBound; } else if (left + nodeRect.left + toolbarDom.offsetWidth > rightBound) { diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/draggable.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/draggable.ts index e0769899cadf..090ab871d7b9 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/draggable.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/draggable.ts @@ -1,8 +1,6 @@ import { BlockDraggableType, DragInsertType } from '$app_reducers/block-draggable/slice'; import { findParent } from '$app/utils/document/node'; import { nanoid } from 'nanoid'; -import { getBlock } from '$app/components/document/_shared/SubscribeNode.hooks'; -import { blockConfig } from '$app/constants/document/config'; export function getDraggableIdByPoint(target: HTMLElement | null) { let node = target; diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/region_grid.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/region_grid.ts index 3922c67329d1..75129c3bea7c 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/region_grid.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/region_grid.ts @@ -46,7 +46,7 @@ export class RegionGrid { this.grid.set(key, []); } - this.grid.get(key)!.push(block); + this.grid.get(key)?.push(block); } } @@ -58,6 +58,7 @@ export class RegionGrid { if (this.hasBlock(block.id)) { this.removeBlock(block); } + this.addBlock(block); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts index de291b980d9e..3f408738de70 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + export function debounce(fn: (...args: any[]) => void, delay: number) { let timeout: NodeJS.Timeout; const debounceFn = (...args: any[]) => { @@ -17,7 +19,7 @@ export function debounce(fn: (...args: any[]) => void, delay: number) { export function throttle<T extends (...args: any[]) => void = (...args: any[]) => void>( fn: T, delay: number, - immediate = true, + immediate = true ): T { let timeout: NodeJS.Timeout | null = null; @@ -156,7 +158,7 @@ export function chunkArray<T>(array: T[], chunkSize: number) { export function interval<T extends (...args: any[]) => any = (...args: any[]) => any>( fn: T, delay?: number, - options?: { immediate?: boolean }, + options?: { immediate?: boolean } ): T & { cancel: () => void } { const { immediate = true } = options || {}; let intervalId: NodeJS.Timer | null = null; diff --git a/frontend/appflowy_tauri/src/appflowy_app/views/DatabasePage.tsx b/frontend/appflowy_tauri/src/appflowy_app/views/DatabasePage.tsx index 61f86d22f04f..cb0dbd5ba060 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/views/DatabasePage.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/views/DatabasePage.tsx @@ -1,5 +1,25 @@ -import { Database } from '../components/database'; +import { useParams } from 'react-router-dom'; +import { ViewIdProvider } from '$app/hooks'; +import { Database, DatabaseTitle, VerticalScrollElementProvider } from '../components/database'; +import { useRef } from 'react'; export const DatabasePage = () => { - return <Database />; + const viewId = useParams().id; + + const ref = useRef<HTMLDivElement>(null); + + if (!viewId) { + return null; + } + + return ( + <div className="h-full overflow-y-auto" ref={ref}> + <VerticalScrollElementProvider value={ref}> + <ViewIdProvider value={viewId}> + <DatabaseTitle /> + <Database /> + </ViewIdProvider> + </VerticalScrollElementProvider> + </div> + ); }; From bc502c9c5b02558ff402b6e73f022258727d98fa Mon Sep 17 00:00:00 2001 From: Yijing Huang <hyj891204@gmail.com> Date: Fri, 3 Nov 2023 12:09:12 -0700 Subject: [PATCH 21/56] chore: improve share button text color (#3868) --- .../plugins/database_view/tar_bar/tab_bar_view.dart | 10 +++------- .../plugins/database_view/widgets/share_button.dart | 2 +- .../flowy_infra/lib/colorscheme/dandelion.dart | 2 +- .../packages/flowy_infra/lib/colorscheme/lavender.dart | 2 +- .../packages/flowy_infra/lib/colorscheme/lemonade.dart | 2 +- 5 files changed, 7 insertions(+), 11 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tab_bar_view.dart b/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tab_bar_view.dart index a32e706bf659..54d244dc2a94 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tab_bar_view.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tab_bar_view.dart @@ -216,13 +216,9 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder { @override Widget? get rightBarItem { - return Row( - children: [ - DatabaseShareButton( - key: ValueKey(notifier.view.id), - view: notifier.view, - ), - ], + return DatabaseShareButton( + key: ValueKey(notifier.view.id), + view: notifier.view, ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/share_button.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/share_button.dart index 28ccc7fa6975..a334838c5cc4 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/share_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/share_button.dart @@ -104,7 +104,7 @@ class DatabaseShareActionListState extends State<DatabaseShareActionList> { buildChild: (controller) { return RoundedTextButton( title: LocaleKeys.shareAction_buttonText.tr(), - textColor: Theme.of(context).colorScheme.onSurface, + textColor: Theme.of(context).colorScheme.onPrimary, onPressed: () => controller.show(), ); }, diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/dandelion.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/dandelion.dart index e577e098e521..684e876704fd 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/dandelion.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/dandelion.dart @@ -66,7 +66,7 @@ class DandelionColorScheme extends FlowyColorScheme { input: _white, hint: _lightShader3, primary: _lightDandelionYellow, - onPrimary: _white, + onPrimary: _lightShader1, // hover color in sidebar hoverBG1: _lightDandelionYellow, // tool bar hover color diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lavender.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lavender.dart index 23ba06198eda..894fc4c4b2ed 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lavender.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lavender.dart @@ -64,7 +64,7 @@ class LavenderColorScheme extends FlowyColorScheme { input: _white, hint: _lightShader3, primary: _lightMain1, - onPrimary: _white, + onPrimary: _lightShader1, hoverBG1: _lightBg2, hoverBG2: _lightHover, hoverBG3: _lightShader6, diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lemonade.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lemonade.dart index 7a1bd38f36d2..86a548059e1b 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lemonade.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lemonade.dart @@ -68,7 +68,7 @@ class LemonadeColorScheme extends FlowyColorScheme { input: _white, hint: _lightShader3, primary: _lightDandelionYellow, - onPrimary: _white, + onPrimary: _lightShader1, // hover color in sidebar hoverBG1: _lightDandelionYellow, // tool bar hover color From b35d6131d4d573d50a4891714eb25f78e06d6ee2 Mon Sep 17 00:00:00 2001 From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Date: Fri, 3 Nov 2023 21:30:24 +0100 Subject: [PATCH 22/56] feat: inline page reference (#3859) * feat: more methods of inserting page reference * test: add tests for inserting document reference * chore: remove unused import * chore: update editor ref * tests: fix test with an interim solution --- .../document_inline_page_reference_test.dart | 136 ++++++++++++++++++ .../document/document_test_runner.dart | 3 + .../document_with_inline_page_test.dart | 18 ++- .../document/edit_document_test.dart | 4 +- .../util/editor_test_operations.dart | 2 +- .../document/presentation/editor_page.dart | 17 +++ .../base/insert_page_command.dart | 78 +++++++--- .../base/link_to_page_widget.dart | 15 +- .../base/page_reference_commands.dart | 117 +++++++++++++++ .../referenced_database_menu_item.dart | 22 +++ .../document/presentation/editor_style.dart | 2 +- .../handlers/inline_page_reference.dart | 7 +- .../inline_actions/inline_actions_menu.dart | 4 + .../widgets/inline_actions_handler.dart | 39 +++-- .../widgets/inline_actions_menu_group.dart | 58 +++----- frontend/resources/translations/en.json | 6 +- 16 files changed, 428 insertions(+), 100 deletions(-) create mode 100644 frontend/appflowy_flutter/integration_test/document/document_inline_page_reference_test.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/page_reference_commands.dart diff --git a/frontend/appflowy_flutter/integration_test/document/document_inline_page_reference_test.dart b/frontend/appflowy_flutter/integration_test/document/document_inline_page_reference_test.dart new file mode 100644 index 000000000000..49d067f248a3 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/document/document_inline_page_reference_test.dart @@ -0,0 +1,136 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; +import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart'; +import 'package:flowy_infra/uuid.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../util/keyboard.dart'; +import '../util/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('insert inline document reference', () { + testWidgets('insert by slash menu', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + final name = await createDocumentToReference(tester); + + await tester.editor.tapLineOfEditorAt(0); + await tester.pumpAndSettle(); + + await triggerReferenceDocumentBySlashMenu(tester); + + // Search for prefix of document + await enterDocumentText(tester); + + // Select result + final optionFinder = find.descendant( + of: find.byType(LinkToPageMenu), + matching: find.text(name), + ); + + await tester.tap(optionFinder); + await tester.pumpAndSettle(); + + final mentionBlock = find.byType(MentionPageBlock); + expect(mentionBlock, findsOneWidget); + }); + + testWidgets('insert by `[[` character shortcut', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + final name = await createDocumentToReference(tester); + + await tester.editor.tapLineOfEditorAt(0); + await tester.pumpAndSettle(); + + await tester.ime.insertText('[['); + await tester.pumpAndSettle(); + + // Select result + await tester.editor.tapAtMenuItemWithName(name); + await tester.pumpAndSettle(); + + final mentionBlock = find.byType(MentionPageBlock); + expect(mentionBlock, findsOneWidget); + }); + + testWidgets('insert by `+` character shortcut', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + final name = await createDocumentToReference(tester); + + await tester.editor.tapLineOfEditorAt(0); + await tester.pumpAndSettle(); + + await tester.ime.insertText('+'); + await tester.pumpAndSettle(); + + // Select result + await tester.editor.tapAtMenuItemWithName(name); + await tester.pumpAndSettle(); + + final mentionBlock = find.byType(MentionPageBlock); + expect(mentionBlock, findsOneWidget); + }); + }); +} + +Future<String> createDocumentToReference(WidgetTester tester) async { + final name = 'document_${uuid()}'; + + await tester.createNewPageWithName( + name: name, + layout: ViewLayoutPB.Document, + openAfterCreated: false, + ); + + // This is a workaround since the openAfterCreated + // option does not work in createNewPageWithName method + await tester.tap(find.byType(SingleInnerViewItem).first); + await tester.pumpAndSettle(); + + return name; +} + +Future<void> triggerReferenceDocumentBySlashMenu(WidgetTester tester) async { + await tester.editor.showSlashMenu(); + await tester.pumpAndSettle(); + + // Search for referenced document action + await enterDocumentText(tester); + + // Select item + await FlowyTestKeyboard.simulateKeyDownEvent( + [ + LogicalKeyboardKey.enter, + ], + tester: tester, + ); + + await tester.pumpAndSettle(); +} + +Future<void> enterDocumentText(WidgetTester tester) async { + await FlowyTestKeyboard.simulateKeyDownEvent( + [ + LogicalKeyboardKey.keyD, + LogicalKeyboardKey.keyO, + LogicalKeyboardKey.keyC, + LogicalKeyboardKey.keyU, + LogicalKeyboardKey.keyM, + LogicalKeyboardKey.keyE, + LogicalKeyboardKey.keyN, + LogicalKeyboardKey.keyT, + ], + tester: tester, + ); + await tester.pumpAndSettle(); +} diff --git a/frontend/appflowy_flutter/integration_test/document/document_test_runner.dart b/frontend/appflowy_flutter/integration_test/document/document_test_runner.dart index 2e9ca77f5ebb..42462c26585a 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_test_runner.dart +++ b/frontend/appflowy_flutter/integration_test/document/document_test_runner.dart @@ -16,6 +16,8 @@ import 'document_with_inline_page_test.dart' as document_with_inline_page_test; import 'document_with_outline_block_test.dart' as document_with_outline_block; import 'document_with_toggle_list_test.dart' as document_with_toggle_list_test; import 'edit_document_test.dart' as document_edit_test; +import 'document_inline_page_reference_test.dart' + as document_inline_page_reference_test; void startTesting() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -35,4 +37,5 @@ void startTesting() { document_text_direction_test.main(); document_option_action_test.main(); document_with_image_block_test.main(); + document_inline_page_reference_test.main(); } diff --git a/frontend/appflowy_flutter/integration_test/document/document_with_inline_page_test.dart b/frontend/appflowy_flutter/integration_test/document/document_with_inline_page_test.dart index e071f3ee363a..f33ce370ff7e 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_with_inline_page_test.dart +++ b/frontend/appflowy_flutter/integration_test/document/document_with_inline_page_test.dart @@ -15,7 +15,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await insertingInlinePage(tester, ViewLayoutPB.Grid); + await insertInlinePage(tester, ViewLayoutPB.Grid); final mentionBlock = find.byType(MentionPageBlock); expect(mentionBlock, findsOneWidget); @@ -26,7 +26,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await insertingInlinePage(tester, ViewLayoutPB.Board); + await insertInlinePage(tester, ViewLayoutPB.Board); final mentionBlock = find.byType(MentionPageBlock); expect(mentionBlock, findsOneWidget); @@ -37,7 +37,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await insertingInlinePage(tester, ViewLayoutPB.Calendar); + await insertInlinePage(tester, ViewLayoutPB.Calendar); final mentionBlock = find.byType(MentionPageBlock); expect(mentionBlock, findsOneWidget); @@ -48,7 +48,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await insertingInlinePage(tester, ViewLayoutPB.Document); + await insertInlinePage(tester, ViewLayoutPB.Document); final mentionBlock = find.byType(MentionPageBlock); expect(mentionBlock, findsOneWidget); @@ -59,7 +59,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - final pageName = await insertingInlinePage(tester, ViewLayoutPB.Document); + final pageName = await insertInlinePage(tester, ViewLayoutPB.Document); // rename const newName = 'RenameToNewPageName'; @@ -78,7 +78,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - final pageName = await insertingInlinePage(tester, ViewLayoutPB.Grid); + final pageName = await insertInlinePage(tester, ViewLayoutPB.Grid); // rename await tester.hoverOnPageName( @@ -98,7 +98,7 @@ void main() { } /// Insert a referenced database of [layout] into the document -Future<String> insertingInlinePage( +Future<String> insertInlinePage( WidgetTester tester, ViewLayoutPB layout, ) async { @@ -110,15 +110,19 @@ Future<String> insertingInlinePage( layout: layout, openAfterCreated: false, ); + // create a new document await tester.createNewPageWithName( name: 'insert_a_inline_page_${layout.name}', layout: ViewLayoutPB.Document, ); + // tap the first line of the document await tester.editor.tapLineOfEditorAt(0); + // insert a inline page await tester.editor.showAtMenu(); await tester.editor.tapAtMenuItemWithName(name); + return name; } diff --git a/frontend/appflowy_flutter/integration_test/document/edit_document_test.dart b/frontend/appflowy_flutter/integration_test/document/edit_document_test.dart index b120b5e33272..280ff2457733 100644 --- a/frontend/appflowy_flutter/integration_test/document/edit_document_test.dart +++ b/frontend/appflowy_flutter/integration_test/document/edit_document_test.dart @@ -113,9 +113,9 @@ const _sample = r''' [] Type followed by bullet or num to create a list. -[x] Click `+ New Page` button at the bottom of your sidebar to add a new page. +[x] Click `New Page` button at the bottom of your sidebar to add a new page. -[] Click `+` next to any page title in the sidebar to quickly add a new subpage, `Document`, `Grid`, or `Kanban Board`. +[] Click the plus sign next to any page title in the sidebar to quickly add a new subpage, `Document`, `Grid`, or `Kanban Board`. --- * bulleted list 1 diff --git a/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart b/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart index 51e99ceddf9d..7236a4b94bbc 100644 --- a/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart +++ b/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart @@ -160,7 +160,7 @@ class EditorOperations { await tester.ime.insertCharacter('/'); } - /// trigger the slash command (selection menu) + /// trigger the mention (@) command Future<void> showAtMenu() async { await tester.ime.insertCharacter('@'); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index 5f63347c5ede..99f338bf807e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -1,6 +1,7 @@ import 'package:appflowy/plugins/document/application/doc_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_configuration.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/background_color/theme_background_color.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/base/page_reference_commands.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/i18n/editor_i18n.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/slash_menu_items.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; @@ -139,6 +140,21 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> { inlineActionsService, style: styleCustomizer.inlineActionsMenuStyleBuilder(), ), + + /// Inline page menu + /// - Using `[[` + pageReferenceShortcutBrackets( + context, + documentBloc.view.id, + styleCustomizer.inlineActionsMenuStyleBuilder(), + ), + + /// - Using `+` + pageReferenceShortcutPlusSign( + context, + documentBloc.view.id, + styleCustomizer.inlineActionsMenuStyleBuilder(), + ), ]; EditorStyleCustomizer get styleCustomizer => widget.styleCustomizer; @@ -322,6 +338,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> { referencedBoardMenuItem, inlineCalendarMenuItem(documentBloc), referencedCalendarMenuItem, + referencedDocumentMenuItem, calloutItem, outlineItem, mathEquationItem, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart index debf00f4e17c..fba3c8644ce0 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart @@ -1,5 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database_view/application/database_view_service.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; @@ -34,6 +35,7 @@ extension InsertDatabase on EditorState { Future<void> insertReferencePage( ViewPB childView, + ViewLayoutPB viewType, ) async { final selection = this.selection; if (selection == null || !selection.isCollapsed) { @@ -50,22 +52,63 @@ extension InsertDatabase on EditorState { ); } + late Transaction transaction; + if (viewType == ViewLayoutPB.Document) { + transaction = await _insertDocumentReference( + childView, + selection, + node, + ); + } else { + transaction = await _insertDatabaseReference( + childView, + selection.end.path, + ); + } + + await apply(transaction); + } + + Future<Transaction> _insertDocumentReference( + ViewPB view, + Selection selection, + Node node, + ) async { + return transaction + ..replaceText( + node, + selection.end.offset, + 0, + r'$', + attributes: { + MentionBlockKeys.mention: { + MentionBlockKeys.type: MentionType.page.name, + MentionBlockKeys.pageId: view.id, + } + }, + ); + } + + Future<Transaction> _insertDatabaseReference( + ViewPB view, + List<int> path, + ) async { // get the database id that the view is associated with - final databaseId = await DatabaseViewBackendService(viewId: childView.id) + final databaseId = await DatabaseViewBackendService(viewId: view.id) .getDatabaseId() .then((value) => value.swap().toOption().toNullable()); if (databaseId == null) { throw StateError( - 'The database associated with ${childView.id} could not be found while attempting to create a referenced ${childView.layout.name}.', + 'The database associated with ${view.id} could not be found while attempting to create a referenced ${view.layout.name}.', ); } - final prefix = _referencedDatabasePrefix(childView.layout); + final prefix = _referencedDatabasePrefix(view.layout); final ref = await ViewBackendService.createDatabaseLinkedView( - parentViewId: childView.id, - name: "$prefix ${childView.name}", - layoutType: childView.layout, + parentViewId: view.id, + name: "$prefix ${view.name}", + layoutType: view.layout, databaseId: databaseId, ).then((value) => value.swap().toOption().toNullable()); @@ -76,18 +119,17 @@ extension InsertDatabase on EditorState { ); } - final transaction = this.transaction; - transaction.insertNode( - selection.end.path, - Node( - type: _convertPageType(childView), - attributes: { - DatabaseBlockKeys.parentID: childView.id, - DatabaseBlockKeys.viewID: ref.id, - }, - ), - ); - await apply(transaction); + return transaction + ..insertNode( + path, + Node( + type: _convertPageType(view), + attributes: { + DatabaseBlockKeys.parentID: view.id, + DatabaseBlockKeys.viewID: ref.id, + }, + ), + ); } String _referencedDatabasePrefix(ViewLayoutPB layout) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart index ff5ae9984a94..0a87e2e189e3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart @@ -1,4 +1,3 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; @@ -42,7 +41,7 @@ void showLinkToPageMenu( hintText: pageType.toHintText(), onSelected: (appPB, viewPB) async { try { - await editorState.insertReferencePage(viewPB); + await editorState.insertReferencePage(viewPB, pageType); linkToPageMenuEntry.remove(); } on FlowyError catch (e) { Dialogs.show( @@ -188,6 +187,7 @@ class _LinkToPageMenuState extends State<LinkToPageMenu> { ) { int index = 0; return FutureBuilder<List<ViewPB>>( + future: items, builder: (context, snapshot) { if (snapshot.hasData && snapshot.connectionState == ConnectionState.done) { @@ -208,10 +208,7 @@ class _LinkToPageMenuState extends State<LinkToPageMenu> { children.add( FlowyButton( isSelected: index == _selectedIndex, - leftIcon: FlowySvg( - view.iconData, - color: Theme.of(context).iconTheme.color, - ), + leftIcon: view.defaultIcon(), text: FlowyText.regular(view.name), onTap: () => widget.onSelected(view, view), ), @@ -229,7 +226,6 @@ class _LinkToPageMenuState extends State<LinkToPageMenu> { return const Center(child: CircularProgressIndicator()); }, - future: items, ); } } @@ -239,13 +235,14 @@ extension on ViewLayoutPB { switch (this) { case ViewLayoutPB.Grid: return LocaleKeys.document_slashMenu_grid_selectAGridToLinkTo.tr(); - case ViewLayoutPB.Board: return LocaleKeys.document_slashMenu_board_selectABoardToLinkTo.tr(); - case ViewLayoutPB.Calendar: return LocaleKeys.document_slashMenu_calendar_selectACalendarToLinkTo .tr(); + case ViewLayoutPB.Document: + return LocaleKeys.document_slashMenu_document_selectADocumentToLinkTo + .tr(); default: throw Exception('Unknown layout type'); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/page_reference_commands.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/page_reference_commands.dart new file mode 100644 index 000000000000..0df49665a02d --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/page_reference_commands.dart @@ -0,0 +1,117 @@ +import 'package:appflowy/plugins/inline_actions/handlers/inline_page_reference.dart'; +import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart'; +import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; +import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; + +const _bracketChar = '['; +const _plusChar = '+'; + +CharacterShortcutEvent pageReferenceShortcutBrackets( + BuildContext context, + String viewId, + InlineActionsMenuStyle style, +) => + CharacterShortcutEvent( + key: 'show the inline page reference menu by [', + character: _bracketChar, + handler: (editorState) => inlinePageReferenceCommandHandler( + _bracketChar, + context, + viewId, + editorState, + style, + previousChar: _bracketChar, + ), + ); + +CharacterShortcutEvent pageReferenceShortcutPlusSign( + BuildContext context, + String viewId, + InlineActionsMenuStyle style, +) => + CharacterShortcutEvent( + key: 'show the inline page reference menu by +', + character: _plusChar, + handler: (editorState) => inlinePageReferenceCommandHandler( + _plusChar, + context, + viewId, + editorState, + style, + ), + ); + +InlineActionsMenuService? selectionMenuService; +Future<bool> inlinePageReferenceCommandHandler( + String character, + BuildContext context, + String currentViewId, + EditorState editorState, + InlineActionsMenuStyle style, { + String? previousChar, +}) async { + final selection = editorState.selection; + if (PlatformExtension.isMobile || selection == null) { + return false; + } + + if (!selection.isCollapsed) { + await editorState.deleteSelection(selection); + } + + // Check for previous character + if (previousChar != null) { + final node = editorState.getNodeAtPath(selection.end.path); + final delta = node?.delta; + if (node == null || delta == null || delta.isEmpty) { + return false; + } + + if (selection.end.offset > 0) { + final plain = delta.toPlainText(); + + final previousCharacter = plain[selection.end.offset - 1]; + if (previousCharacter != _bracketChar) { + return false; + } + } + } + + // ignore: use_build_context_synchronously + final service = InlineActionsService( + context: context, + handlers: [ + InlinePageReferenceService( + currentViewId: currentViewId, + ).inlinePageReferenceDelegate, + ], + ); + + await editorState.insertTextAtPosition(character, position: selection.start); + + final List<InlineActionsResult> initialResults = []; + for (final handler in service.handlers) { + final group = await handler(); + + if (group.results.isNotEmpty) { + initialResults.add(group); + } + } + + if (service.context != null) { + selectionMenuService = InlineActionsMenu( + context: service.context!, + editorState: editorState, + service: service, + initialResults: initialResults, + style: style, + startCharAmount: previousChar != null ? 2 : 1, + ); + + selectionMenuService?.show(); + } + + return true; +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/referenced_database_menu_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/referenced_database_menu_item.dart index 94d20d479c66..976a0186dd5e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/referenced_database_menu_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/referenced_database_menu_item.dart @@ -7,6 +7,28 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +// Document Reference + +SelectionMenuItem referencedDocumentMenuItem = SelectionMenuItem( + name: LocaleKeys.document_plugins_referencedDocument.tr(), + icon: (editorState, onSelected, style) => SelectableSvgWidget( + data: FlowySvgs.documents_s, + isSelected: onSelected, + style: style, + ), + keywords: ['page', 'notes', 'referenced page', 'referenced document'], + handler: (editorState, menuService, context) { + showLinkToPageMenu( + Overlay.of(context), + editorState, + menuService, + ViewLayoutPB.Document, + ); + }, +); + +// Database References + SelectionMenuItem referencedGridMenuItem = SelectionMenuItem( name: LocaleKeys.document_plugins_referencedGrid.tr(), icon: (editorState, onSelected, style) => SelectableSvgWidget( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart index d7f925d00f78..402e4a5c9cf4 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -179,7 +179,7 @@ class EditorStyleCustomizer { backgroundColor: theme.cardColor, groupTextColor: theme.colorScheme.onBackground.withOpacity(.8), menuItemTextColor: theme.colorScheme.onBackground, - menuItemSelectedColor: theme.hoverColor, + menuItemSelectedColor: theme.colorScheme.secondary, menuItemSelectedTextColor: theme.colorScheme.onSurface, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart index 3bfb4b2a0b83..4657f4d46f00 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart @@ -4,16 +4,20 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; class InlinePageReferenceService { - InlinePageReferenceService({required this.currentViewId}) { + InlinePageReferenceService({ + required this.currentViewId, + }) { init(); } final Completer _initCompleter = Completer<void>(); + final String currentViewId; late final ViewBackendService service; @@ -79,6 +83,7 @@ class InlinePageReferenceService { final pageSelectionMenuItem = InlineActionsMenuItem( keywords: [view.name.toLowerCase()], label: view.name, + icon: (onSelected) => view.defaultIcon(), onSelected: (context, editorState, menuService, replace) async { final selection = editorState.selection; if (selection == null || !selection.isCollapsed) { diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_menu.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_menu.dart index 1dffa7edd015..3db1fa29a90c 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_menu.dart @@ -18,6 +18,7 @@ class InlineActionsMenu extends InlineActionsMenuService { required this.service, required this.initialResults, required this.style, + this.startCharAmount = 1, }); final BuildContext context; @@ -28,6 +29,8 @@ class InlineActionsMenu extends InlineActionsMenuService { @override final InlineActionsMenuStyle style; + final int startCharAmount; + OverlayEntry? _menuEntry; bool selectionChangedByMenu = false; @@ -130,6 +133,7 @@ class InlineActionsMenu extends InlineActionsMenuService { onDismiss: dismiss, onSelectionUpdate: _onSelectionUpdate, style: style, + startCharAmount: startCharAmount, ), ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart index 9aa6f7ba0114..af58ea17cc47 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart @@ -1,5 +1,4 @@ import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/inline_actions/inline_actions_command.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart'; @@ -51,6 +50,7 @@ class InlineActionsHandler extends StatefulWidget { required this.onDismiss, required this.onSelectionUpdate, required this.style, + this.startCharAmount = 1, }); final InlineActionsService service; @@ -60,6 +60,7 @@ class InlineActionsHandler extends StatefulWidget { final VoidCallback onDismiss; final VoidCallback onSelectionUpdate; final InlineActionsMenuStyle style; + final int startCharAmount; @override State<InlineActionsHandler> createState() => _InlineActionsHandlerState(); @@ -99,10 +100,7 @@ class _InlineActionsHandlerState extends State<InlineActionsHandler> { _resetSelection(); newResults.sortByStartsWithKeyword(_search); - - setState(() { - results = newResults; - }); + setState(() => results = newResults); } void _resetSelection() { @@ -116,10 +114,9 @@ class _InlineActionsHandlerState extends State<InlineActionsHandler> { @override void initState() { super.initState(); - - WidgetsBinding.instance.addPostFrameCallback((_) { - _focusNode.requestFocus(); - }); + WidgetsBinding.instance.addPostFrameCallback( + (_) => _focusNode.requestFocus(), + ); startOffset = widget.editorState.selection?.endIndex ?? 0; } @@ -163,6 +160,8 @@ class _InlineActionsHandlerState extends State<InlineActionsHandler> { isGroupSelected: _selectedGroup == index, selectedIndex: _selectedIndex, onSelected: widget.onDismiss, + startOffset: startOffset - widget.startCharAmount, + endOffset: _search.length + widget.startCharAmount, ), ) .toList(), @@ -200,7 +199,10 @@ class _InlineActionsHandlerState extends State<InlineActionsHandler> { context, widget.editorState, widget.menuService, - (startOffset - 1, _search.length + 1), + ( + startOffset - widget.startCharAmount, + _search.length + widget.startCharAmount + ), ); widget.onDismiss(); @@ -212,7 +214,7 @@ class _InlineActionsHandlerState extends State<InlineActionsHandler> { } else if (event.logicalKey == LogicalKeyboardKey.backspace) { if (_search.isEmpty) { widget.onDismiss(); - widget.editorState.deleteBackward(); // Delete '@' + widget.editorState.deleteBackward(); } else { widget.onSelectionUpdate(); widget.editorState.deleteBackward(); @@ -282,16 +284,12 @@ class _InlineActionsHandlerState extends State<InlineActionsHandler> { return; } - /// Grab index of the first character in command (right after @) - final startIndex = - delta.toPlainText().lastIndexOf(inlineActionCharacter) + 1; - search = widget.editorState .getTextInSelection( selection.copyWith( - start: selection.start.copyWith(offset: startIndex), + start: selection.start.copyWith(offset: startOffset), end: selection.start - .copyWith(offset: startIndex + _search.length + 1), + .copyWith(offset: startOffset + _search.length + 1), ), ) .join(); @@ -331,8 +329,9 @@ class _InlineActionsHandlerState extends State<InlineActionsHandler> { return; } - search = delta - .toPlainText() - .substring(startOffset, startOffset - 1 + _search.length); + search = delta.toPlainText().substring( + startOffset, + startOffset - 1 + _search.length, + ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_menu_group.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_menu_group.dart index 1f568e8f3e36..498e91bb9e9b 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_menu_group.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_menu_group.dart @@ -2,6 +2,7 @@ import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:collection/collection.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; @@ -13,6 +14,8 @@ class InlineActionsGroup extends StatelessWidget { required this.menuService, required this.style, required this.onSelected, + required this.startOffset, + required this.endOffset, this.isGroupSelected = false, this.selectedIndex = 0, }); @@ -22,6 +25,8 @@ class InlineActionsGroup extends StatelessWidget { final InlineActionsMenuService menuService; final InlineActionsMenuStyle style; final VoidCallback onSelected; + final int startOffset; + final int endOffset; final bool isGroupSelected; final int selectedIndex; @@ -43,6 +48,8 @@ class InlineActionsGroup extends StatelessWidget { isSelected: isGroupSelected && index == selectedIndex, style: style, onSelected: onSelected, + startOffset: startOffset, + endOffset: endOffset, ), ), ], @@ -60,6 +67,8 @@ class InlineActionsWidget extends StatefulWidget { required this.isSelected, required this.style, required this.onSelected, + required this.startOffset, + required this.endOffset, }); final InlineActionsMenuItem item; @@ -68,57 +77,26 @@ class InlineActionsWidget extends StatefulWidget { final bool isSelected; final InlineActionsMenuStyle style; final VoidCallback onSelected; + final int startOffset; + final int endOffset; @override State<InlineActionsWidget> createState() => _InlineActionsWidgetState(); } class _InlineActionsWidgetState extends State<InlineActionsWidget> { - bool isHovering = false; - @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 2), child: SizedBox( width: 200, - child: widget.item.icon != null - ? TextButton.icon( - onPressed: _onPressed, - style: ButtonStyle( - alignment: Alignment.centerLeft, - backgroundColor: widget.isSelected - ? MaterialStateProperty.all( - widget.style.menuItemSelectedColor, - ) - : MaterialStateProperty.all(Colors.transparent), - ), - icon: widget.item.icon!.call(widget.isSelected || isHovering), - label: FlowyText.regular( - widget.item.label, - color: widget.isSelected - ? widget.style.menuItemSelectedTextColor - : widget.style.menuItemTextColor, - ), - ) - : TextButton( - onPressed: _onPressed, - style: ButtonStyle( - alignment: Alignment.centerLeft, - backgroundColor: widget.isSelected - ? MaterialStateProperty.all( - widget.style.menuItemSelectedColor, - ) - : MaterialStateProperty.all(Colors.transparent), - ), - onHover: (value) => setState(() => isHovering = value), - child: FlowyText.regular( - widget.item.label, - color: widget.isSelected - ? widget.style.menuItemSelectedTextColor - : widget.style.menuItemTextColor, - ), - ), + child: FlowyButton( + isSelected: widget.isSelected, + leftIcon: widget.item.icon?.call(widget.isSelected), + text: FlowyText.regular(widget.item.label), + onTap: _onPressed, + ), ), ); } @@ -129,7 +107,7 @@ class _InlineActionsWidgetState extends State<InlineActionsWidget> { context, widget.editorState, widget.menuService, - (0, 0), + (widget.startOffset, widget.endOffset), ); } } diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index fff0e8ec08a1..09ff958b53bf 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -579,6 +579,9 @@ "calendar": { "selectACalendarToLinkTo": "Select a Calendar to link to", "createANewCalendar": "Create a new Calendar" + }, + "document": { + "selectADocumentToLinkTo": "Select a Document to link to" } }, "selectionMenu": { @@ -589,6 +592,7 @@ "referencedBoard": "Referenced Board", "referencedGrid": "Referenced Grid", "referencedCalendar": "Referenced Calendar", + "referencedDocument": "Referenced Document", "autoGeneratorMenuItemName": "OpenAI Writer", "autoGeneratorTitleName": "OpenAI: Ask AI to write anything...", "autoGeneratorLearnMore": "Learn more", @@ -1030,4 +1034,4 @@ "noFavorite": "No favorite page", "noFavoriteHintText": "Swipe the page to the left to add it to your favorites" } -} \ No newline at end of file +} From 1025b6d55362e8bdd67f0fb0a3762564d2050e4b Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Sun, 5 Nov 2023 14:00:24 +0800 Subject: [PATCH 23/56] fix: af cloud sync auth (#3873) * feat: open workspace * chore: update env docs * fix: invalid user callback * fix: token invalid * chore: update * chore: update * chore: update * chore: fix test * chore: fix tauri build --- frontend/appflowy_flutter/dev.env | 26 ++- .../env/backend_env.dart} | 9 +- .../lib/startup/tasks/rust_sdk.dart | 7 +- .../lib/user/application/user_service.dart | 11 +- .../desktop_workspace_start_screen.dart | 1 - .../mobile_workspace_start_screen.dart | 2 - .../application/workspace/workspace_bloc.dart | 21 -- .../lib/appflowy_backend.dart | 7 +- frontend/appflowy_tauri/src-tauri/Cargo.lock | 76 ++++---- frontend/appflowy_tauri/src-tauri/Cargo.toml | 18 +- .../stores/effects/user/user_bd_svc.ts | 8 - .../effects/workspace/workspace_bd_svc.ts | 8 +- frontend/rust-lib/Cargo.lock | 80 ++++---- frontend/rust-lib/Cargo.toml | 18 +- .../event-integration/src/folder_event.rs | 9 - .../tests/document/af_cloud_test/edit_test.rs | 2 +- .../flowy-core/src/integrate/server.rs | 15 +- .../flowy-core/src/integrate/trait_impls.rs | 22 ++- .../rust-lib/flowy-core/src/integrate/user.rs | 28 ++- frontend/rust-lib/flowy-core/src/lib.rs | 1 - .../rust-lib/flowy-database2/src/manager.rs | 13 +- .../rust-lib/flowy-document-deps/src/cloud.rs | 3 +- .../rust-lib/flowy-document2/src/manager.rs | 31 ++- .../flowy-document2/tests/document/util.rs | 2 +- frontend/rust-lib/flowy-error/src/code.rs | 3 - frontend/rust-lib/flowy-error/src/errors.rs | 5 +- .../flowy-error/src/impl_from/cloud.rs | 37 ++-- .../rust-lib/flowy-folder-deps/src/cloud.rs | 15 ++ .../flowy-folder2/src/event_handler.rs | 35 +--- .../rust-lib/flowy-folder2/src/event_map.rs | 7 +- .../rust-lib/flowy-folder2/src/manager.rs | 184 ++++++++++-------- .../src/af_cloud/impls/document.rs | 2 +- .../flowy-server/src/af_cloud/impls/folder.rs | 33 +++- .../af_cloud/impls/user/cloud_service_impl.rs | 12 +- .../flowy-server/src/af_cloud/server.rs | 52 +++-- .../src/local_server/impls/document.rs | 3 +- .../src/local_server/impls/folder.rs | 29 +-- .../src/local_server/impls/user.rs | 8 +- .../flowy-server/src/local_server/server.rs | 1 - frontend/rust-lib/flowy-server/src/server.rs | 3 +- .../flowy-server/src/supabase/api/document.rs | 4 +- .../flowy-server/src/supabase/api/folder.rs | 9 + .../flowy-server/src/supabase/api/user.rs | 8 +- frontend/rust-lib/flowy-task/src/scheduler.rs | 17 +- .../rust-lib/flowy-user-deps/src/cloud.rs | 4 +- .../rust-lib/flowy-user-deps/src/entities.rs | 2 +- .../flowy-user/src/entities/user_profile.rs | 10 +- .../src/entities/workspace_member.rs | 7 + .../rust-lib/flowy-user/src/event_handler.rs | 17 +- frontend/rust-lib/flowy-user/src/event_map.rs | 12 +- frontend/rust-lib/flowy-user/src/manager.rs | 67 +++++-- .../src/migrations/document_empty_content.rs | 53 ++--- .../flowy-user/src/migrations/migration.rs | 7 - .../rust-lib/flowy-user/src/migrations/mod.rs | 1 + .../flowy-user/src/migrations/util.rs | 23 +++ .../migrations/workspace_and_favorite_v1.rs | 20 +- .../flowy-user/src/services/user_workspace.rs | 11 +- frontend/rust-lib/lib-log/Cargo.toml | 1 - frontend/rust-lib/lib-log/src/lib.rs | 16 +- 59 files changed, 658 insertions(+), 478 deletions(-) rename frontend/appflowy_flutter/{packages/appflowy_backend/lib/env_serde.dart => lib/env/backend_env.dart} (85%) create mode 100644 frontend/rust-lib/flowy-user/src/migrations/util.rs diff --git a/frontend/appflowy_flutter/dev.env b/frontend/appflowy_flutter/dev.env index b00d233a2fbb..109aa7c5cc5d 100644 --- a/frontend/appflowy_flutter/dev.env +++ b/frontend/appflowy_flutter/dev.env @@ -1,23 +1,33 @@ # Initial Setup + # 1. Copy the dev.env file to .env: -# cp dev.env .env -# 2. Alternatively, you can generate the .env file using the "Generate Env File" task in VSCode. +# cp dev.env .env +# Update the environment parameters as needed. + +# 2. Generate the env.dart from this .env file: +# You can use the "Generate Env File" task in VSCode. +# Alternatively, execute the following commands: +# cd appflowy_flutter +# dart run build_runner clean && dart run build_runner build --delete-conflicting-outputs + -# Configuring Cloud Type -# This configuration file is used to specify the cloud type and the necessary configurations for each cloud type. The available options are: +# Cloud Type Configuration +# Use this configuration file to specify the cloud type and its associated settings. The available cloud types are: # Local: 0 # Supabase: 1 # AppFlowy Cloud: 2 - +# By default, it's set to Local. CLOUD_TYPE=0 # Supabase Configuration -# If you're using Supabase (CLOUD_TYPE=1), you need to provide the following configurations: +# If using Supabase (CLOUD_TYPE=1), provide the following details: SUPABASE_URL=replace-with-your-supabase-url SUPABASE_ANON_KEY=replace-with-your-supabase-key # AppFlowy Cloud Configuration -# If you're using AppFlowy Cloud (CLOUD_TYPE=2), you need to provide the following configurations: +# If using AppFlowy Cloud (CLOUD_TYPE=2), provide the following details: APPFLOWY_CLOUD_BASE_URL=replace-with-your-appflowy-cloud-url APPFLOWY_CLOUD_WS_BASE_URL=replace-with-your-appflowy-cloud-ws-url -APPFLOWY_CLOUD_GOTRUE_URL=replace-with-your-appflowy-cloud-gotrue-url \ No newline at end of file +APPFLOWY_CLOUD_GOTRUE_URL=replace-with-your-appflowy-cloud-gotrue-url + + diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/lib/env_serde.dart b/frontend/appflowy_flutter/lib/env/backend_env.dart similarity index 85% rename from frontend/appflowy_flutter/packages/appflowy_backend/lib/env_serde.dart rename to frontend/appflowy_flutter/lib/env/backend_env.dart index 63000278d6db..b9355e66ea86 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/lib/env_serde.dart +++ b/frontend/appflowy_flutter/lib/env/backend_env.dart @@ -1,10 +1,7 @@ -import 'package:json_annotation/json_annotation.dart'; +// ignore_for_file: non_constant_identifier_names -// Run `dart run build_runner build` to generate the json serialization If the -// file `env_serde.g.dart` is existed, delete it first. -// -// the file `env_serde.g.dart` will be generated in the same directory. -part 'env_serde.g.dart'; +import 'package:json_annotation/json_annotation.dart'; +part 'backend_env.g.dart'; @JsonSerializable() class AppFlowyEnv { diff --git a/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart b/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart index 11ba1e4e510c..551e7c77dad9 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart @@ -1,8 +1,9 @@ +import 'dart:convert'; import 'dart:io'; +import 'package:appflowy/env/backend_env.dart'; import 'package:appflowy/env/env.dart'; import 'package:appflowy_backend/appflowy_backend.dart'; -import 'package:appflowy_backend/env_serde.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as path; @@ -23,8 +24,10 @@ class InitRustSDKTask extends LaunchTask { Future<void> initialize(LaunchContext context) async { final dir = directory ?? await appFlowyApplicationDataDirectory(); + // Pass the environment variables to the Rust SDK final env = getAppFlowyEnv(); - context.getIt<FlowySDK>().setEnv(env); + context.getIt<FlowySDK>().setEnv(jsonEncode(env.toJson())); + await context.getIt<FlowySDK>().init(dir); } diff --git a/frontend/appflowy_flutter/lib/user/application/user_service.dart b/frontend/appflowy_flutter/lib/user/application/user_service.dart index 9815af70dce6..b1125dd4af1c 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_service.dart @@ -100,14 +100,9 @@ class UserBackendService { return Future.value(left([])); } - Future<Either<WorkspacePB, FlowyError>> openWorkspace(String workspaceId) { - final request = WorkspaceIdPB.create()..value = workspaceId; - return FolderEventOpenWorkspace(request).send().then((result) { - return result.fold( - (workspace) => left(workspace), - (error) => right(error), - ); - }); + Future<Either<Unit, FlowyError>> openWorkspace(String workspaceId) { + final payload = UserWorkspaceIdPB.create()..workspaceId = workspaceId; + return UserEventOpenWorkspace(payload).send(); } Future<Either<WorkspacePB, FlowyError>> getCurrentWorkspace() { diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/desktop_workspace_start_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/desktop_workspace_start_screen.dart index e041c8e75da6..c50ec257851a 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/desktop_workspace_start_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/desktop_workspace_start_screen.dart @@ -101,6 +101,5 @@ Widget _renderCreateButton(BuildContext context) { // same method as in mobile void _popToWorkspace(BuildContext context, WorkspacePB workspace) { - context.read<WorkspaceBloc>().add(WorkspaceEvent.openWorkspace(workspace)); context.pop(workspace.id); } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/mobile_workspace_start_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/mobile_workspace_start_screen.dart index f587594a7989..d86b6c0fd03d 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/mobile_workspace_start_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/mobile_workspace_start_screen.dart @@ -6,7 +6,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; // TODO(yijing): needs refactor when multiple workspaces are supported @@ -139,6 +138,5 @@ class _MobileWorkspaceStartScreenState // same method as in desktop void _popToWorkspace(BuildContext context, WorkspacePB workspace) { - context.read<WorkspaceBloc>().add(WorkspaceEvent.openWorkspace(workspace)); context.pop(workspace.id); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart index 3562a906be41..ef44c02592ff 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart @@ -19,9 +19,6 @@ class WorkspaceBloc extends Bloc<WorkspaceEvent, WorkspaceState> { initial: (e) async { await _fetchWorkspaces(emit); }, - openWorkspace: (e) async { - await _openWorkspace(e.workspace, emit); - }, createWorkspace: (e) async { await _createWorkspace(e.name, e.desc, emit); }, @@ -57,22 +54,6 @@ class WorkspaceBloc extends Bloc<WorkspaceEvent, WorkspaceState> { ); } - Future<void> _openWorkspace( - WorkspacePB workspace, - Emitter<WorkspaceState> emit, - ) async { - final result = await userService.openWorkspace(workspace.id); - emit( - result.fold( - (workspaces) => state.copyWith(successOrFailure: left(unit)), - (error) { - Log.error(error); - return state.copyWith(successOrFailure: right(error)); - }, - ), - ); - } - Future<void> _createWorkspace( String name, String desc, @@ -98,8 +79,6 @@ class WorkspaceEvent with _$WorkspaceEvent { const factory WorkspaceEvent.initial() = Initial; const factory WorkspaceEvent.createWorkspace(String name, String desc) = CreateWorkspace; - const factory WorkspaceEvent.openWorkspace(WorkspacePB workspace) = - OpenWorkspace; const factory WorkspaceEvent.workspacesReveived( Either<List<WorkspacePB>, FlowyError> workspacesOrFail, ) = WorkspacesReceived; diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/lib/appflowy_backend.dart b/frontend/appflowy_flutter/packages/appflowy_backend/lib/appflowy_backend.dart index a31faedf9dae..91fb5c16ec3d 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/lib/appflowy_backend.dart +++ b/frontend/appflowy_flutter/packages/appflowy_backend/lib/appflowy_backend.dart @@ -1,11 +1,9 @@ export 'package:async/async.dart'; -import 'dart:convert'; import 'dart:io'; import 'dart:async'; import 'package:appflowy_backend/rust_stream.dart'; import 'package:flutter/services.dart'; import 'dart:ffi'; -import 'env_serde.dart'; import 'ffi.dart' as ffi; import 'package:ffi/ffi.dart'; @@ -37,8 +35,7 @@ class FlowySDK { ffi.init_sdk(sdkDir.path.toNativeUtf8()); } - void setEnv(AppFlowyEnv env) { - final jsonStr = jsonEncode(env.toJson()); - ffi.set_env(jsonStr.toNativeUtf8()); + void setEnv(String envStr) { + ffi.set_env(envStr.toNativeUtf8()); } } diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index adc7299a62ea..8a79aa6952ef 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -135,6 +135,21 @@ version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +[[package]] +name = "app-error" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c739029d99517282d2ec1593523a47b51a85231b#c739029d99517282d2ec1593523a47b51a85231b" +dependencies = [ + "anyhow", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "url", + "uuid", +] + [[package]] name = "appflowy_tauri" version = "0.0.0" @@ -445,7 +460,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" dependencies = [ "borsh-derive", - "hashbrown 0.12.3", + "hashbrown 0.13.2", ] [[package]] @@ -753,9 +768,10 @@ dependencies = [ [[package]] name = "client-api" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c87d0f05e988a02e9272a42722b304289be320e4#c87d0f05e988a02e9272a42722b304289be320e4" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c739029d99517282d2ec1593523a47b51a85231b#c739029d99517282d2ec1593523a47b51a85231b" dependencies = [ "anyhow", + "app-error", "bytes", "collab", "collab-entity", @@ -845,7 +861,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" dependencies = [ "anyhow", "async-trait", @@ -864,7 +880,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" dependencies = [ "anyhow", "async-trait", @@ -894,7 +910,7 @@ dependencies = [ [[package]] name = "collab-derive" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" dependencies = [ "proc-macro2", "quote", @@ -906,7 +922,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" dependencies = [ "anyhow", "collab", @@ -926,7 +942,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" dependencies = [ "anyhow", "bytes", @@ -940,7 +956,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" dependencies = [ "anyhow", "chrono", @@ -982,7 +998,7 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" dependencies = [ "async-trait", "bincode", @@ -1003,7 +1019,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" dependencies = [ "anyhow", "async-trait", @@ -1030,7 +1046,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" dependencies = [ "anyhow", "collab", @@ -1429,9 +1445,10 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c87d0f05e988a02e9272a42722b304289be320e4#c87d0f05e988a02e9272a42722b304289be320e4" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c739029d99517282d2ec1593523a47b51a85231b#c739029d99517282d2ec1593523a47b51a85231b" dependencies = [ "anyhow", + "app-error", "chrono", "collab-entity", "serde", @@ -2064,7 +2081,7 @@ dependencies = [ "nanoid", "parking_lot", "protobuf", - "scraper 0.18.0", + "scraper 0.18.1", "serde", "serde_json", "strum_macros 0.21.1", @@ -2778,7 +2795,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c87d0f05e988a02e9272a42722b304289be320e4#c87d0f05e988a02e9272a42722b304289be320e4" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c739029d99517282d2ec1593523a47b51a85231b#c739029d99517282d2ec1593523a47b51a85231b" dependencies = [ "anyhow", "futures-util", @@ -2794,9 +2811,10 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c87d0f05e988a02e9272a42722b304289be320e4#c87d0f05e988a02e9272a42722b304289be320e4" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c739029d99517282d2ec1593523a47b51a85231b#c739029d99517282d2ec1593523a47b51a85231b" dependencies = [ "anyhow", + "app-error", "jsonwebtoken", "lazy_static", "reqwest", @@ -3229,7 +3247,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c87d0f05e988a02e9272a42722b304289be320e4#c87d0f05e988a02e9272a42722b304289be320e4" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c739029d99517282d2ec1593523a47b51a85231b#c739029d99517282d2ec1593523a47b51a85231b" dependencies = [ "anyhow", "reqwest", @@ -3479,7 +3497,6 @@ dependencies = [ "tracing-appender", "tracing-bunyan-formatter", "tracing-core", - "tracing-log 0.2.0", "tracing-subscriber", ] @@ -4904,8 +4921,9 @@ dependencies = [ [[package]] name = "realtime-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c87d0f05e988a02e9272a42722b304289be320e4#c87d0f05e988a02e9272a42722b304289be320e4" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c739029d99517282d2ec1593523a47b51a85231b#c739029d99517282d2ec1593523a47b51a85231b" dependencies = [ + "anyhow", "bytes", "collab", "collab-entity", @@ -5358,9 +5376,9 @@ dependencies = [ [[package]] name = "scraper" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3693f9a0203d49a7ba8f38aa915316b3d535c1862d03dae7009cb71a3408b36a" +checksum = "585480e3719b311b78a573db1c9d9c4c1f8010c2dee4cc59c2efe58ea4dbc3e1" dependencies = [ "ahash 0.8.3", "cssparser 0.31.2", @@ -5642,9 +5660,10 @@ dependencies = [ [[package]] name = "shared_entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c87d0f05e988a02e9272a42722b304289be320e4#c87d0f05e988a02e9272a42722b304289be320e4" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c739029d99517282d2ec1593523a47b51a85231b#c739029d99517282d2ec1593523a47b51a85231b" dependencies = [ "anyhow", + "app-error", "collab-entity", "database-entity", "gotrue-entity", @@ -6689,7 +6708,7 @@ dependencies = [ "time", "tracing", "tracing-core", - "tracing-log 0.1.3", + "tracing-log", "tracing-subscriber", ] @@ -6714,17 +6733,6 @@ dependencies = [ "tracing-core", ] -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - [[package]] name = "tracing-serde" version = "0.1.3" @@ -6752,7 +6760,7 @@ dependencies = [ "thread_local", "tracing", "tracing-core", - "tracing-log 0.1.3", + "tracing-log", "tracing-serde", ] diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index b7a4e519580c..d53d248a3488 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -38,7 +38,7 @@ custom-protocol = ["tauri/custom-protocol"] # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c87d0f05e988a02e9272a42722b304289be320e4" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c739029d99517282d2ec1593523a47b51a85231b" } # Please use the following script to update collab. # Working directory: frontend # @@ -48,14 +48,14 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c87 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "01f92f7bb" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "01f92f7bb" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "01f92f7bb" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "01f92f7bb" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "01f92f7bb" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "01f92f7bb" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "01f92f7bb" } -collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "01f92f7bb" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bab20052" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bab20052" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bab20052" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bab20052" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bab20052" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bab20052" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bab20052" } +collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bab20052" } diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/user/user_bd_svc.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/user/user_bd_svc.ts index 7306fcfaea67..8dec16134cb2 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/user/user_bd_svc.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/user/user_bd_svc.ts @@ -15,13 +15,11 @@ import { SignInPayloadPB, SignUpPayloadPB, UpdateUserProfilePayloadPB, - WorkspaceIdPB, WorkspacePB, WorkspaceSettingPB, } from '@/services/backend'; import { FolderEventCreateWorkspace, - FolderEventOpenWorkspace, FolderEventGetCurrentWorkspaceSetting, FolderEventReadCurrentWorkspace, } from '@/services/backend/events/flowy-folder2'; @@ -67,12 +65,6 @@ export class UserBackendService { return FolderEventReadCurrentWorkspace(); }; - openWorkspace = (workspaceId: string) => { - const payload = WorkspaceIdPB.fromObject({ value: workspaceId }); - - return FolderEventOpenWorkspace(payload); - }; - createWorkspace = async (params: { name: string; desc: string }): Promise<WorkspacePB> => { const payload = CreateWorkspacePayloadPB.fromObject({ name: params.name, desc: params.desc }); const result = await FolderEventCreateWorkspace(payload); diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/workspace_bd_svc.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/workspace_bd_svc.ts index 5b57e534beff..1a5025412e4a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/workspace_bd_svc.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/workspace/workspace_bd_svc.ts @@ -1,12 +1,12 @@ import { FolderEventCreateWorkspace, CreateWorkspacePayloadPB, - FolderEventOpenWorkspace, FolderEventDeleteWorkspace, WorkspaceIdPB, FolderEventReadWorkspaceViews, FolderEventReadCurrentWorkspace, } from '@/services/backend/events/flowy-folder2'; +import { UserEventOpenWorkspace, UserWorkspaceIdPB } from '@/services/backend/events/flowy-user'; export class WorkspaceBackendService { constructor() { @@ -24,11 +24,11 @@ export class WorkspaceBackendService { }; openWorkspace = async (workspaceId: string) => { - const payload = new WorkspaceIdPB({ - value: workspaceId, + const payload = new UserWorkspaceIdPB({ + workspace_id: workspaceId, }); - return FolderEventOpenWorkspace(payload); + return UserEventOpenWorkspace(payload); }; deleteWorkspace = async (workspaceId: string) => { diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 188eb32f5bf1..97703309f95d 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -121,6 +121,21 @@ version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +[[package]] +name = "app-error" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c739029d99517282d2ec1593523a47b51a85231b#c739029d99517282d2ec1593523a47b51a85231b" +dependencies = [ + "anyhow", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "url", + "uuid", +] + [[package]] name = "arrayvec" version = "0.5.2" @@ -452,7 +467,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" dependencies = [ "borsh-derive", - "hashbrown 0.12.3", + "hashbrown 0.13.2", ] [[package]] @@ -651,9 +666,10 @@ dependencies = [ [[package]] name = "client-api" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c87d0f05e988a02e9272a42722b304289be320e4#c87d0f05e988a02e9272a42722b304289be320e4" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c739029d99517282d2ec1593523a47b51a85231b#c739029d99517282d2ec1593523a47b51a85231b" dependencies = [ "anyhow", + "app-error", "bytes", "collab", "collab-entity", @@ -712,7 +728,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" dependencies = [ "anyhow", "async-trait", @@ -731,7 +747,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" dependencies = [ "anyhow", "async-trait", @@ -761,7 +777,7 @@ dependencies = [ [[package]] name = "collab-derive" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" dependencies = [ "proc-macro2", "quote", @@ -773,7 +789,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" dependencies = [ "anyhow", "collab", @@ -793,7 +809,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" dependencies = [ "anyhow", "bytes", @@ -807,7 +823,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" dependencies = [ "anyhow", "chrono", @@ -849,7 +865,7 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" dependencies = [ "async-trait", "bincode", @@ -870,7 +886,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" dependencies = [ "anyhow", "async-trait", @@ -897,7 +913,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=01f92f7bb#01f92f7bb204a3aeed24b345d504dfd36d3d9fcb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" dependencies = [ "anyhow", "collab", @@ -1256,9 +1272,10 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c87d0f05e988a02e9272a42722b304289be320e4#c87d0f05e988a02e9272a42722b304289be320e4" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c739029d99517282d2ec1593523a47b51a85231b#c739029d99517282d2ec1593523a47b51a85231b" dependencies = [ "anyhow", + "app-error", "chrono", "collab-entity", "serde", @@ -1885,7 +1902,7 @@ dependencies = [ "nanoid", "parking_lot", "protobuf", - "scraper 0.18.0", + "scraper 0.18.1", "serde", "serde_json", "strum_macros 0.21.1", @@ -2437,7 +2454,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c87d0f05e988a02e9272a42722b304289be320e4#c87d0f05e988a02e9272a42722b304289be320e4" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c739029d99517282d2ec1593523a47b51a85231b#c739029d99517282d2ec1593523a47b51a85231b" dependencies = [ "anyhow", "futures-util", @@ -2453,9 +2470,10 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c87d0f05e988a02e9272a42722b304289be320e4#c87d0f05e988a02e9272a42722b304289be320e4" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c739029d99517282d2ec1593523a47b51a85231b#c739029d99517282d2ec1593523a47b51a85231b" dependencies = [ "anyhow", + "app-error", "jsonwebtoken", "lazy_static", "reqwest", @@ -2813,7 +2831,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c87d0f05e988a02e9272a42722b304289be320e4#c87d0f05e988a02e9272a42722b304289be320e4" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c739029d99517282d2ec1593523a47b51a85231b#c739029d99517282d2ec1593523a47b51a85231b" dependencies = [ "anyhow", "reqwest", @@ -2979,7 +2997,6 @@ dependencies = [ "tracing-appender", "tracing-bunyan-formatter", "tracing-core", - "tracing-log 0.2.0", "tracing-subscriber", ] @@ -4254,8 +4271,9 @@ dependencies = [ [[package]] name = "realtime-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c87d0f05e988a02e9272a42722b304289be320e4#c87d0f05e988a02e9272a42722b304289be320e4" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c739029d99517282d2ec1593523a47b51a85231b#c739029d99517282d2ec1593523a47b51a85231b" dependencies = [ + "anyhow", "bytes", "collab", "collab-entity", @@ -4705,9 +4723,9 @@ dependencies = [ [[package]] name = "scraper" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3693f9a0203d49a7ba8f38aa915316b3d535c1862d03dae7009cb71a3408b36a" +checksum = "585480e3719b311b78a573db1c9d9c4c1f8010c2dee4cc59c2efe58ea4dbc3e1" dependencies = [ "ahash 0.8.3", "cssparser", @@ -4810,9 +4828,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.105" +version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ "itoa", "ryu", @@ -4891,9 +4909,10 @@ dependencies = [ [[package]] name = "shared_entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c87d0f05e988a02e9272a42722b304289be320e4#c87d0f05e988a02e9272a42722b304289be320e4" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c739029d99517282d2ec1593523a47b51a85231b#c739029d99517282d2ec1593523a47b51a85231b" dependencies = [ "anyhow", + "app-error", "collab-entity", "database-entity", "gotrue-entity", @@ -5654,7 +5673,7 @@ dependencies = [ "time", "tracing", "tracing-core", - "tracing-log 0.1.3", + "tracing-log", "tracing-subscriber", ] @@ -5679,17 +5698,6 @@ dependencies = [ "tracing-core", ] -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - [[package]] name = "tracing-serde" version = "0.1.3" @@ -5717,7 +5725,7 @@ dependencies = [ "thread_local", "tracing", "tracing-core", - "tracing-log 0.1.3", + "tracing-log", "tracing-serde", ] diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 8d30996ea8a1..b7f1d825d686 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -82,7 +82,7 @@ incremental = false # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c87d0f05e988a02e9272a42722b304289be320e4" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c739029d99517282d2ec1593523a47b51a85231b" } # Please use the following script to update collab. # Working directory: frontend # @@ -92,11 +92,11 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c87 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "01f92f7bb" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "01f92f7bb" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "01f92f7bb" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "01f92f7bb" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "01f92f7bb" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "01f92f7bb" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "01f92f7bb" } -collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "01f92f7bb" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bab20052" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bab20052" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bab20052" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bab20052" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bab20052" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bab20052" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bab20052" } +collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bab20052" } diff --git a/frontend/rust-lib/event-integration/src/folder_event.rs b/frontend/rust-lib/event-integration/src/folder_event.rs index 0dcc8d296719..df06634bd898 100644 --- a/frontend/rust-lib/event-integration/src/folder_event.rs +++ b/frontend/rust-lib/event-integration/src/folder_event.rs @@ -144,19 +144,10 @@ pub struct ViewTest { pub workspace: WorkspacePB, pub child_view: ViewPB, } - impl ViewTest { #[allow(dead_code)] pub async fn new(sdk: &EventIntegrationTest, layout: ViewLayoutPB, data: Vec<u8>) -> Self { let workspace = sdk.folder_manager.get_current_workspace().await.unwrap(); - let payload = WorkspaceIdPB { - value: workspace.id.clone(), - }; - let _ = EventBuilder::new(sdk.clone()) - .event(OpenWorkspace) - .payload(payload) - .async_send() - .await; let payload = CreateViewPayloadPB { parent_view_id: workspace.id.clone(), diff --git a/frontend/rust-lib/event-integration/tests/document/af_cloud_test/edit_test.rs b/frontend/rust-lib/event-integration/tests/document/af_cloud_test/edit_test.rs index e5a08c8506e9..2c78a3c5ab53 100644 --- a/frontend/rust-lib/event-integration/tests/document/af_cloud_test/edit_test.rs +++ b/frontend/rust-lib/event-integration/tests/document/af_cloud_test/edit_test.rs @@ -22,7 +22,7 @@ async fn af_cloud_edit_document_test() { let rx = test .notification_sender .subscribe_with_condition::<DocumentSyncStatePB, _>(&document_id, |pb| pb.is_finish); - receive_with_timeout(rx, Duration::from_secs(15)) + receive_with_timeout(rx, Duration::from_secs(25)) .await .unwrap(); diff --git a/frontend/rust-lib/flowy-core/src/integrate/server.rs b/frontend/rust-lib/flowy-core/src/integrate/server.rs index 418890fce5fe..adc593243871 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/server.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/server.rs @@ -5,7 +5,6 @@ use std::sync::{Arc, Weak}; use parking_lot::RwLock; use serde_repr::*; -use collab_integrate::YrsDocAction; use flowy_error::{FlowyError, FlowyResult}; use flowy_server::af_cloud::AFCloudServer; use flowy_server::local_server::{LocalServer, LocalServerDB}; @@ -14,9 +13,7 @@ use flowy_server::{AppFlowyEncryption, AppFlowyServer, EncryptionImpl}; use flowy_server_config::af_cloud_config::AFCloudConfiguration; use flowy_server_config::supabase_config::SupabaseConfiguration; use flowy_sqlite::kv::StorePreferences; -use flowy_user::services::database::{ - get_user_profile, get_user_workspace, open_collab_db, open_user_db, -}; +use flowy_user::services::database::{get_user_profile, get_user_workspace, open_user_db}; use flowy_user_deps::cloud::UserCloudService; use flowy_user_deps::entities::*; @@ -195,14 +192,4 @@ impl LocalServerDB for LocalServerDBImpl { let user_workspace = get_user_workspace(&sqlite_db, uid)?; Ok(user_workspace) } - - fn get_collab_updates(&self, uid: i64, object_id: &str) -> Result<Vec<Vec<u8>>, FlowyError> { - let collab_db = open_collab_db(&self.storage_path, uid)?; - let read_txn = collab_db.read_txn(); - let updates = read_txn.get_all_updates(uid, object_id).map_err(|e| { - FlowyError::internal().with_context(format!("Failed to open collab db: {:?}", e)) - })?; - - Ok(updates) - } } diff --git a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs index de9eb2c22bc5..84f4ed6614a6 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs @@ -17,7 +17,9 @@ use flowy_database_deps::cloud::{ use flowy_document2::deps::DocumentData; use flowy_document_deps::cloud::{DocumentCloudService, DocumentSnapshot}; use flowy_error::FlowyError; -use flowy_folder_deps::cloud::{FolderCloudService, FolderData, FolderSnapshot, Workspace}; +use flowy_folder_deps::cloud::{ + FolderCloudService, FolderData, FolderSnapshot, Workspace, WorkspaceRecord, +}; use flowy_storage::{FileStorageService, StorageObject}; use flowy_user::event_map::UserCloudServiceProvider; use flowy_user_deps::cloud::UserCloudService; @@ -140,6 +142,17 @@ impl FolderCloudService for ServerProvider { FutureResult::new(async move { server?.folder_service().create_workspace(uid, &name).await }) } + fn open_workspace(&self, workspace_id: &str) -> FutureResult<(), Error> { + let workspace_id = workspace_id.to_string(); + let server = self.get_server(&self.get_server_type()); + FutureResult::new(async move { server?.folder_service().open_workspace(&workspace_id).await }) + } + + fn get_all_workspace(&self) -> FutureResult<Vec<WorkspaceRecord>, Error> { + let server = self.get_server(&self.get_server_type()); + FutureResult::new(async move { server?.folder_service().get_all_workspace().await }) + } + fn get_folder_data( &self, workspace_id: &str, @@ -245,7 +258,7 @@ impl DocumentCloudService for ServerProvider { &self, document_id: &str, workspace_id: &str, - ) -> FutureResult<Vec<Vec<u8>>, Error> { + ) -> FutureResult<Vec<Vec<u8>>, FlowyError> { let workspace_id = workspace_id.to_string(); let document_id = document_id.to_string(); let server = self.get_server(&self.get_server_type()); @@ -308,7 +321,7 @@ impl CollabStorageProvider for ServerProvider { to_fut(async move { let mut plugins: Vec<Arc<dyn CollabPlugin>> = vec![]; match server.collab_ws_channel(&collab_object.object_id).await { - Ok(Some((channel, ws_connect_state))) => { + Ok(Some((channel, ws_connect_state, is_connected))) => { let origin = CollabOrigin::Client(CollabClient::new( collab_object.uid, collab_object.device_id.clone(), @@ -316,7 +329,7 @@ impl CollabStorageProvider for ServerProvider { let sync_object = SyncObject::from(collab_object); let (sink, stream) = (channel.sink(), channel.stream()); let sink_config = SinkConfig::new() - .send_timeout(6) + .send_timeout(8) .with_strategy(SinkStrategy::FixInterval(Duration::from_secs(2))); let sync_plugin = SyncPlugin::new( origin, @@ -326,6 +339,7 @@ impl CollabStorageProvider for ServerProvider { sink_config, stream, Some(channel), + !is_connected, ws_connect_state, ); plugins.push(Arc::new(sync_plugin)); diff --git a/frontend/rust-lib/flowy-core/src/integrate/user.rs b/frontend/rust-lib/flowy-core/src/integrate/user.rs index 1cc8841e5013..10dbd33413b2 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/user.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/user.rs @@ -1,12 +1,13 @@ use std::sync::Arc; use anyhow::Context; +use tracing::event; use collab_integrate::collab_builder::AppFlowyCollabBuilder; use flowy_database2::DatabaseManager; use flowy_document2::manager::DocumentManager; use flowy_error::FlowyResult; -use flowy_folder2::manager::{FolderInitializeDataSource, FolderManager}; +use flowy_folder2::manager::{FolderInitDataSource, FolderManager}; use flowy_user::event_map::{UserCloudServiceProvider, UserStatusCallback}; use flowy_user_deps::cloud::UserCloudConfig; use flowy_user_deps::entities::{AuthType, UserProfile, UserWorkspace}; @@ -59,7 +60,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { .initialize( user_id, &user_workspace.id, - FolderInitializeDataSource::LocalDisk { + FolderInitDataSource::LocalDisk { create_if_not_exist: false, }, ) @@ -82,8 +83,9 @@ impl UserStatusCallback for UserStatusCallbackImpl { &self, user_id: i64, user_workspace: &UserWorkspace, - _device_id: &str, + device_id: &str, ) -> Fut<FlowyResult<()>> { + let device_id = device_id.to_owned(); let user_id = user_id.to_owned(); let user_workspace = user_workspace.clone(); let folder_manager = self.folder_manager.clone(); @@ -91,6 +93,13 @@ impl UserStatusCallback for UserStatusCallbackImpl { let document_manager = self.document_manager.clone(); to_fut(async move { + event!( + tracing::Level::TRACE, + "Notify did sign in: latest_workspace: {:?}, device_id: {}", + user_workspace, + device_id + ); + folder_manager .initialize_with_workspace_id(user_id, &user_workspace.id) .await?; @@ -113,8 +122,9 @@ impl UserStatusCallback for UserStatusCallbackImpl { is_new_user: bool, user_profile: &UserProfile, user_workspace: &UserWorkspace, - _device_id: &str, + device_id: &str, ) -> Fut<FlowyResult<()>> { + let device_id = device_id.to_owned(); let user_profile = user_profile.clone(); let folder_manager = self.folder_manager.clone(); let database_manager = self.database_manager.clone(); @@ -122,12 +132,20 @@ impl UserStatusCallback for UserStatusCallbackImpl { let document_manager = self.document_manager.clone(); to_fut(async move { + event!( + tracing::Level::TRACE, + "Notify did sign up: is new: {} user_workspace: {:?}, device_id: {}", + is_new_user, + user_workspace, + device_id + ); + folder_manager .initialize_with_new_user( user_profile.uid, &user_profile.token, is_new_user, - FolderInitializeDataSource::LocalDisk { + FolderInitDataSource::LocalDisk { create_if_not_exist: true, }, &user_workspace.id, diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index 4a7ade197299..41365b5a45a0 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -196,7 +196,6 @@ impl AppFlowyCore { let cloned_user_session = Arc::downgrade(&user_manager); if let Some(user_session) = cloned_user_session.upgrade() { - event!(tracing::Level::DEBUG, "init user session",); if let Err(err) = user_session .init(user_status_callback, collab_interact_impl) .await diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index ec5d1c28092f..633ca5e3146b 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -13,7 +13,7 @@ use collab_database::views::{CreateDatabaseParams, CreateViewParams, DatabaseLay use collab_entity::CollabType; use futures::executor::block_on; use tokio::sync::RwLock; -use tracing::{instrument, trace}; +use tracing::{event, instrument, trace}; use collab_integrate::collab_builder::AppFlowyCollabBuilder; use collab_integrate::{CollabPersistenceConfig, RocksCollabDB}; @@ -80,6 +80,11 @@ impl DatabaseManager { workspace_id: String, database_views_aggregate_id: String, ) -> FlowyResult<()> { + // Clear all existing tasks + self.task_scheduler.write().await.clear_task(); + // Release all existing editors + self.editors.write().await.clear(); + let collab_db = self.user.collab_db(uid)?; let collab_builder = UserDatabaseCollabServiceImpl { workspace_id: workspace_id.clone(), @@ -114,7 +119,11 @@ impl DatabaseManager { } // Construct the workspace database. - trace!("open workspace database: {}", &database_views_aggregate_id); + event!( + tracing::Level::INFO, + "open aggregate database views object: {}", + &database_views_aggregate_id + ); let collab = collab_builder.build_collab_with_config( uid, &database_views_aggregate_id, diff --git a/frontend/rust-lib/flowy-document-deps/src/cloud.rs b/frontend/rust-lib/flowy-document-deps/src/cloud.rs index 0a17113b7a66..59ee90206ea0 100644 --- a/frontend/rust-lib/flowy-document-deps/src/cloud.rs +++ b/frontend/rust-lib/flowy-document-deps/src/cloud.rs @@ -1,6 +1,7 @@ use anyhow::Error; pub use collab_document::blocks::DocumentData; +use flowy_error::FlowyError; use lib_infra::future::FutureResult; /// A trait for document cloud service. @@ -11,7 +12,7 @@ pub trait DocumentCloudService: Send + Sync + 'static { &self, document_id: &str, workspace_id: &str, - ) -> FutureResult<Vec<Vec<u8>>, Error>; + ) -> FutureResult<Vec<Vec<u8>>, FlowyError>; fn get_document_snapshots( &self, diff --git a/frontend/rust-lib/flowy-document2/src/manager.rs b/frontend/rust-lib/flowy-document2/src/manager.rs index ee8a9c9a9c49..8852ff50aebe 100644 --- a/frontend/rust-lib/flowy-document2/src/manager.rs +++ b/frontend/rust-lib/flowy-document2/src/manager.rs @@ -1,14 +1,14 @@ use std::sync::Weak; use std::{collections::HashMap, sync::Arc}; -use collab::core::collab::MutexCollab; +use collab::core::collab::{CollabRawData, MutexCollab}; use collab_document::blocks::DocumentData; use collab_document::document::Document; -use collab_document::document_data::default_document_data; +use collab_document::document_data::{default_document_collab_data, default_document_data}; use collab_document::YrsDocAction; use collab_entity::CollabType; use parking_lot::RwLock; -use tracing::instrument; +use tracing::{event, instrument}; use collab_integrate::collab_builder::AppFlowyCollabBuilder; use collab_integrate::RocksCollabDB; @@ -109,10 +109,29 @@ impl DocumentManager { let mut updates = vec![]; if !self.is_doc_exist(doc_id)? { // Try to get the document from the cloud service - updates = self + let result: Result<CollabRawData, FlowyError> = self .cloud_service - .get_document_updates(&self.user.workspace_id()?, doc_id) - .await?; + .get_document_updates(doc_id, &self.user.workspace_id()?) + .await; + + updates = match result { + Ok(data) => data, + Err(err) => { + if err.is_record_not_found() { + // The document's ID exists in the cloud, but its content does not. + // This occurs when user A's document hasn't finished syncing and user B tries to open it. + // As a result, a blank document is created for user B. + event!( + tracing::Level::INFO, + "can't find the document in the cloud, doc_id: {}", + doc_id + ); + default_document_collab_data(doc_id) + } else { + return Err(err); + } + }, + } } let uid = self.user.user_id()?; diff --git a/frontend/rust-lib/flowy-document2/tests/document/util.rs b/frontend/rust-lib/flowy-document2/tests/document/util.rs index 4ee4fcb109a3..6e29c6266e44 100644 --- a/frontend/rust-lib/flowy-document2/tests/document/util.rs +++ b/frontend/rust-lib/flowy-document2/tests/document/util.rs @@ -129,7 +129,7 @@ impl DocumentCloudService for LocalTestDocumentCloudServiceImpl { &self, _document_id: &str, _workspace_id: &str, - ) -> FutureResult<Vec<Vec<u8>>, Error> { + ) -> FutureResult<Vec<Vec<u8>>, FlowyError> { FutureResult::new(async move { Ok(vec![]) }) } diff --git a/frontend/rust-lib/flowy-error/src/code.rs b/frontend/rust-lib/flowy-error/src/code.rs index 25c5e9242b6f..5ff1f9741a6e 100644 --- a/frontend/rust-lib/flowy-error/src/code.rs +++ b/frontend/rust-lib/flowy-error/src/code.rs @@ -98,9 +98,6 @@ pub enum ErrorCode { #[error("user id is empty or whitespace")] UserIdInvalid = 30, - #[error("User not exist")] - UserNotExist = 31, - #[error("Text is too long")] TextTooLong = 32, diff --git a/frontend/rust-lib/flowy-error/src/errors.rs b/frontend/rust-lib/flowy-error/src/errors.rs index c96094b35b71..10fd44e8eb5f 100644 --- a/frontend/rust-lib/flowy-error/src/errors.rs +++ b/frontend/rust-lib/flowy-error/src/errors.rs @@ -55,6 +55,10 @@ impl FlowyError { self.code == ErrorCode::RecordNotFound } + pub fn is_unauthorized(&self) -> bool { + self.code == ErrorCode::UserUnauthorized || self.code == ErrorCode::RecordNotFound + } + static_flowy_error!(internal, ErrorCode::Internal); static_flowy_error!(record_not_found, ErrorCode::RecordNotFound); static_flowy_error!(workspace_name, ErrorCode::WorkspaceNameInvalid); @@ -87,7 +91,6 @@ impl FlowyError { ); static_flowy_error!(name_empty, ErrorCode::UserNameIsEmpty); static_flowy_error!(user_id, ErrorCode::UserIdInvalid); - static_flowy_error!(user_not_exist, ErrorCode::UserNotExist); static_flowy_error!(text_too_long, ErrorCode::TextTooLong); static_flowy_error!(invalid_data, ErrorCode::InvalidParams); static_flowy_error!(out_of_bounds, ErrorCode::OutOfBounds); diff --git a/frontend/rust-lib/flowy-error/src/impl_from/cloud.rs b/frontend/rust-lib/flowy-error/src/impl_from/cloud.rs index 97221c886fcc..6f15465a9cfc 100644 --- a/frontend/rust-lib/flowy-error/src/impl_from/cloud.rs +++ b/frontend/rust-lib/flowy-error/src/impl_from/cloud.rs @@ -1,26 +1,25 @@ -use client_api::error::AppError; +use client_api::error::{AppResponseError, ErrorCode as AppErrorCode}; use crate::{ErrorCode, FlowyError}; -impl From<AppError> for FlowyError { - fn from(error: AppError) -> Self { +impl From<AppResponseError> for FlowyError { + fn from(error: AppResponseError) -> Self { let code = match error.code { - client_api::error::ErrorCode::Ok => ErrorCode::Internal, - client_api::error::ErrorCode::Unhandled => ErrorCode::Internal, - client_api::error::ErrorCode::RecordNotFound => ErrorCode::RecordNotFound, - client_api::error::ErrorCode::RecordAlreadyExists => ErrorCode::RecordAlreadyExists, - client_api::error::ErrorCode::InvalidEmail => ErrorCode::EmailFormatInvalid, - client_api::error::ErrorCode::InvalidPassword => ErrorCode::PasswordFormatInvalid, - client_api::error::ErrorCode::OAuthError => ErrorCode::UserUnauthorized, - client_api::error::ErrorCode::MissingPayload => ErrorCode::MissingPayload, - client_api::error::ErrorCode::OpenError => ErrorCode::Internal, - client_api::error::ErrorCode::InvalidUrl => ErrorCode::InvalidURL, - client_api::error::ErrorCode::InvalidRequestParams => ErrorCode::InvalidParams, - client_api::error::ErrorCode::UrlMissingParameter => ErrorCode::InvalidParams, - client_api::error::ErrorCode::InvalidOAuthProvider => ErrorCode::InvalidAuthConfig, - client_api::error::ErrorCode::NotLoggedIn => ErrorCode::UserUnauthorized, - client_api::error::ErrorCode::NotEnoughPermissions => ErrorCode::NotEnoughPermissions, - client_api::error::ErrorCode::UserNameIsEmpty => ErrorCode::UserNameIsEmpty, + AppErrorCode::Ok => ErrorCode::Internal, + AppErrorCode::Unhandled => ErrorCode::Internal, + AppErrorCode::RecordNotFound => ErrorCode::RecordNotFound, + AppErrorCode::RecordAlreadyExists => ErrorCode::RecordAlreadyExists, + AppErrorCode::InvalidEmail => ErrorCode::EmailFormatInvalid, + AppErrorCode::InvalidPassword => ErrorCode::PasswordFormatInvalid, + AppErrorCode::OAuthError => ErrorCode::UserUnauthorized, + AppErrorCode::MissingPayload => ErrorCode::MissingPayload, + AppErrorCode::OpenError => ErrorCode::Internal, + AppErrorCode::InvalidUrl => ErrorCode::InvalidURL, + AppErrorCode::InvalidRequestParams => ErrorCode::InvalidParams, + AppErrorCode::UrlMissingParameter => ErrorCode::InvalidParams, + AppErrorCode::InvalidOAuthProvider => ErrorCode::InvalidAuthConfig, + AppErrorCode::NotLoggedIn => ErrorCode::UserUnauthorized, + AppErrorCode::NotEnoughPermissions => ErrorCode::NotEnoughPermissions, _ => ErrorCode::Internal, }; diff --git a/frontend/rust-lib/flowy-folder-deps/src/cloud.rs b/frontend/rust-lib/flowy-folder-deps/src/cloud.rs index 98ec0bf8c977..527051904354 100644 --- a/frontend/rust-lib/flowy-folder-deps/src/cloud.rs +++ b/frontend/rust-lib/flowy-folder-deps/src/cloud.rs @@ -6,8 +6,16 @@ use lib_infra::future::FutureResult; /// [FolderCloudService] represents the cloud service for folder. pub trait FolderCloudService: Send + Sync + 'static { + /// Creates a new workspace for the user. + /// Returns error if the cloud service doesn't support multiple workspaces fn create_workspace(&self, uid: i64, name: &str) -> FutureResult<Workspace, Error>; + fn open_workspace(&self, workspace_id: &str) -> FutureResult<(), Error>; + + /// Returns all workspaces of the user. + /// Returns vec![] if the cloud service doesn't support multiple workspaces + fn get_all_workspace(&self) -> FutureResult<Vec<WorkspaceRecord>, Error>; + fn get_folder_data( &self, workspace_id: &str, @@ -39,3 +47,10 @@ pub fn gen_workspace_id() -> Uuid { pub fn gen_view_id() -> Uuid { uuid::Uuid::new_v4() } + +#[derive(Debug)] +pub struct WorkspaceRecord { + pub id: String, + pub name: String, + pub created_at: i64, +} diff --git a/frontend/rust-lib/flowy-folder2/src/event_handler.rs b/frontend/rust-lib/flowy-folder2/src/event_handler.rs index 7a3676b547bf..d826c202a3ba 100644 --- a/frontend/rust-lib/flowy-folder2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-folder2/src/event_handler.rs @@ -38,6 +38,14 @@ pub(crate) async fn create_workspace_handler( }) } +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn get_all_workspace_handler( + _data: AFPluginData<CreateWorkspacePayloadPB>, + _folder: AFPluginState<Weak<FolderManager>>, +) -> DataResult<RepeatedWorkspacePB, FlowyError> { + todo!() +} + #[tracing::instrument(level = "debug", skip(folder), err)] pub(crate) async fn get_workspace_views_handler( folder: AFPluginState<Weak<FolderManager>>, @@ -48,32 +56,12 @@ pub(crate) async fn get_workspace_views_handler( data_result_ok(repeated_view) } -#[tracing::instrument(level = "debug", skip(data, folder), err)] -pub(crate) async fn open_workspace_handler( - data: AFPluginData<WorkspaceIdPB>, - folder: AFPluginState<Weak<FolderManager>>, -) -> DataResult<WorkspacePB, FlowyError> { - let folder = upgrade_folder(folder)?; - let workspace_id = data.into_inner().value; - if workspace_id.is_empty() { - Err(FlowyError::workspace_id().with_context("workspace id should not be empty")) - } else { - let workspace = folder.open_workspace(&workspace_id).await?; - let views = folder.get_workspace_views(&workspace_id).await?; - let workspace_pb: WorkspacePB = (workspace, views).into(); - data_result_ok(workspace_pb) - } -} - #[tracing::instrument(level = "debug", skip(folder), err)] pub(crate) async fn read_current_workspace_setting_handler( folder: AFPluginState<Weak<FolderManager>>, ) -> DataResult<WorkspaceSettingPB, FlowyError> { let folder = upgrade_folder(folder)?; - let setting = folder - .get_workspace_setting_pb() - .await - .ok_or(FlowyError::record_not_found())?; + let setting = folder.get_workspace_setting_pb().await?; data_result_ok(setting) } @@ -82,10 +70,7 @@ pub(crate) async fn read_current_workspace_handler( folder: AFPluginState<Weak<FolderManager>>, ) -> DataResult<WorkspacePB, FlowyError> { let folder = upgrade_folder(folder)?; - let workspace = folder - .get_workspace_pb() - .await - .ok_or(FlowyError::record_not_found())?; + let workspace = folder.get_workspace_pb().await?; data_result_ok(workspace) } diff --git a/frontend/rust-lib/flowy-folder2/src/event_map.rs b/frontend/rust-lib/flowy-folder2/src/event_map.rs index d26b2b6ef5a2..e6d145c533ef 100644 --- a/frontend/rust-lib/flowy-folder2/src/event_map.rs +++ b/frontend/rust-lib/flowy-folder2/src/event_map.rs @@ -12,9 +12,8 @@ pub fn init(folder: Weak<FolderManager>) -> AFPlugin { AFPlugin::new().name("Flowy-Folder").state(folder) // Workspace .event(FolderEvent::CreateWorkspace, create_workspace_handler) - .event(FolderEvent::GetCurrentWorkspaceSetting, read_current_workspace_setting_handler) + .event(FolderEvent::GetCurrentWorkspaceSetting, read_current_workspace_setting_handler) .event(FolderEvent::ReadCurrentWorkspace, read_current_workspace_handler) - .event(FolderEvent::OpenWorkspace, open_workspace_handler) .event(FolderEvent::ReadWorkspaceViews, get_workspace_views_handler) // View .event(FolderEvent::CreateView, create_view_handler) @@ -59,10 +58,6 @@ pub enum FolderEvent { #[event(input = "WorkspaceIdPB")] DeleteWorkspace = 3, - /// Open the workspace and mark it as the current workspace - #[event(input = "WorkspaceIdPB", output = "WorkspacePB")] - OpenWorkspace = 4, - /// Return a list of views of the current workspace. /// Only the first level of child views are included. #[event(input = "WorkspaceIdPB", output = "RepeatedViewPB")] diff --git a/frontend/rust-lib/flowy-folder2/src/manager.rs b/frontend/rust-lib/flowy-folder2/src/manager.rs index a710c4b5d1af..4e70300ed2ac 100644 --- a/frontend/rust-lib/flowy-folder2/src/manager.rs +++ b/frontend/rust-lib/flowy-folder2/src/manager.rs @@ -1,4 +1,5 @@ use std::collections::HashSet; +use std::fmt::{Display, Formatter}; use std::ops::Deref; use std::sync::{Arc, Weak}; @@ -124,68 +125,50 @@ impl FolderManager { Ok(views) } - /// Called immediately after the application launched fi the user already sign in/sign up. + /// Called immediately after the application launched if the user already sign in/sign up. #[tracing::instrument(level = "info", skip(self, initial_data), err)] pub async fn initialize( &self, uid: i64, workspace_id: &str, - initial_data: FolderInitializeDataSource, + initial_data: FolderInitDataSource, ) -> FlowyResult<()> { + // Update the workspace id + event!( + Level::INFO, + "Init current workspace: {} from: {}", + workspace_id, + initial_data + ); *self.workspace_id.write() = Some(workspace_id.to_string()); let workspace_id = workspace_id.to_string(); - if let Ok(collab_db) = self.user.collab_db(uid) { - let (view_tx, view_rx) = tokio::sync::broadcast::channel(100); - let (trash_tx, trash_rx) = tokio::sync::broadcast::channel(100); - let folder_notifier = FolderNotify { - view_change_tx: view_tx, - trash_change_tx: trash_tx, - }; - let folder = match initial_data { - FolderInitializeDataSource::LocalDisk { - create_if_not_exist, - } => { - let is_exist = is_exist_in_local_disk(&self.user, &workspace_id).unwrap_or(false); - if is_exist { - event!(Level::INFO, "Restore folder from local disk"); - let collab = self - .collab_for_folder(uid, &workspace_id, collab_db, vec![]) - .await?; - Folder::open(UserId::from(uid), collab, Some(folder_notifier))? - } else if create_if_not_exist { - event!(Level::INFO, "Create folder with default folder builder"); - let folder_data = - DefaultFolderBuilder::build(uid, workspace_id.to_string(), &self.operation_handlers) - .await; - let collab = self - .collab_for_folder(uid, &workspace_id, collab_db, vec![]) - .await?; - Folder::create( - UserId::from(uid), - collab, - Some(folder_notifier), - folder_data, - ) - } else { - return Err(FlowyError::new( - ErrorCode::RecordNotFound, - "Can't find any workspace data", - )); - } - }, - FolderInitializeDataSource::Cloud(raw_data) => { - event!(Level::INFO, "Restore folder from cloud service"); - if raw_data.is_empty() { - return Err(workspace_data_not_sync_error(uid, &workspace_id)); - } + // Get the collab db for the user with given user id. + let collab_db = self.user.collab_db(uid)?; + + let (view_tx, view_rx) = tokio::sync::broadcast::channel(100); + let (trash_tx, trash_rx) = tokio::sync::broadcast::channel(100); + let folder_notifier = FolderNotify { + view_change_tx: view_tx, + trash_change_tx: trash_tx, + }; + + let folder = match initial_data { + FolderInitDataSource::LocalDisk { + create_if_not_exist, + } => { + let is_exist = is_exist_in_local_disk(&self.user, &workspace_id).unwrap_or(false); + if is_exist { + event!(Level::INFO, "Restore folder from local disk"); let collab = self - .collab_for_folder(uid, &workspace_id, collab_db, raw_data) + .collab_for_folder(uid, &workspace_id, collab_db, vec![]) .await?; Folder::open(UserId::from(uid), collab, Some(folder_notifier))? - }, - FolderInitializeDataSource::FolderData(folder_data) => { - event!(Level::INFO, "Restore folder with passed-in folder data"); + } else if create_if_not_exist { + event!(Level::INFO, "Create folder with default folder builder"); + let folder_data = + DefaultFolderBuilder::build(uid, workspace_id.to_string(), &self.operation_handlers) + .await; let collab = self .collab_for_folder(uid, &workspace_id, collab_db, vec![]) .await?; @@ -195,24 +178,45 @@ impl FolderManager { Some(folder_notifier), folder_data, ) - }, - }; + } else { + return Err(FlowyError::new( + ErrorCode::RecordNotFound, + "Can't find any workspace data", + )); + } + }, + FolderInitDataSource::Cloud(raw_data) => { + event!(Level::INFO, "Restore folder from cloud service"); + if raw_data.is_empty() { + return Err(workspace_data_not_sync_error(uid, &workspace_id)); + } + let collab = self + .collab_for_folder(uid, &workspace_id, collab_db, raw_data) + .await?; + Folder::open(UserId::from(uid), collab, Some(folder_notifier))? + }, + FolderInitDataSource::FolderData(folder_data) => { + event!(Level::INFO, "Restore folder with passed-in folder data"); + let collab = self + .collab_for_folder(uid, &workspace_id, collab_db, vec![]) + .await?; + Folder::create( + UserId::from(uid), + collab, + Some(folder_notifier), + folder_data, + ) + }, + }; - tracing::debug!("Current workspace_id: {}", workspace_id); - let folder_state_rx = folder.subscribe_sync_state(); - *self.mutex_folder.lock() = Some(folder); - - let weak_mutex_folder = Arc::downgrade(&self.mutex_folder); - subscribe_folder_sync_state_changed( - workspace_id.clone(), - folder_state_rx, - &weak_mutex_folder, - ); - subscribe_folder_snapshot_state_changed(workspace_id, &weak_mutex_folder); - subscribe_folder_trash_changed(trash_rx, &weak_mutex_folder); - subscribe_folder_view_changed(view_rx, &weak_mutex_folder); - } + let folder_state_rx = folder.subscribe_sync_state(); + *self.mutex_folder.lock() = Some(folder); + let weak_mutex_folder = Arc::downgrade(&self.mutex_folder); + subscribe_folder_sync_state_changed(workspace_id.clone(), folder_state_rx, &weak_mutex_folder); + subscribe_folder_snapshot_state_changed(workspace_id, &weak_mutex_folder); + subscribe_folder_trash_changed(trash_rx, &weak_mutex_folder); + subscribe_folder_view_changed(view_rx, &weak_mutex_folder); Ok(()) } @@ -239,7 +243,7 @@ impl FolderManager { /// Initialize the folder with the given workspace id. /// Fetch the folder updates from the cloud service and initialize the folder. - #[tracing::instrument(level = "debug", skip(self, user_id), err)] + #[tracing::instrument(skip(self, user_id), err)] pub async fn initialize_with_workspace_id( &self, user_id: i64, @@ -250,7 +254,8 @@ impl FolderManager { .get_folder_updates(workspace_id, user_id) .await?; - info!( + event!( + Level::INFO, "Get folder updates via {}, number of updates: {}", self.cloud_service.service_name(), folder_updates.len() @@ -260,7 +265,7 @@ impl FolderManager { .initialize( user_id, workspace_id, - FolderInitializeDataSource::Cloud(folder_updates), + FolderInitDataSource::Cloud(folder_updates), ) .await?; Ok(()) @@ -268,18 +273,13 @@ impl FolderManager { /// Initialize the folder for the new user. /// Using the [DefaultFolderBuilder] to create the default workspace for the new user. - #[instrument( - name = "folder_initialize_with_new_user", - level = "debug", - skip_all, - err - )] + #[instrument(level = "info", skip_all, err)] pub async fn initialize_with_new_user( &self, user_id: i64, _token: &str, is_new: bool, - data_source: FolderInitializeDataSource, + data_source: FolderInitDataSource, workspace_id: &str, ) -> FlowyResult<()> { // Create the default workspace if the user is new @@ -306,7 +306,7 @@ impl FolderManager { .initialize( user_id, workspace_id, - FolderInitializeDataSource::Cloud(folder_updates), + FolderInitDataSource::Cloud(folder_updates), ) .await?; }, @@ -348,20 +348,24 @@ impl FolderManager { self.with_folder(|| None, |folder| folder.get_current_workspace()) } - pub async fn get_workspace_setting_pb(&self) -> Option<WorkspaceSettingPB> { - let workspace_id = self.get_current_workspace_id().await.ok()?; + pub async fn get_workspace_setting_pb(&self) -> FlowyResult<WorkspaceSettingPB> { + let workspace_id = self.get_current_workspace_id().await?; let latest_view = self.get_current_view().await; - Some(WorkspaceSettingPB { + Ok(WorkspaceSettingPB { workspace_id, latest_view, }) } - pub async fn get_workspace_pb(&self) -> Option<WorkspacePB> { + pub async fn get_workspace_pb(&self) -> FlowyResult<WorkspacePB> { let workspace_pb = { let guard = self.mutex_folder.lock(); - let folder = guard.as_ref()?; - let workspace = folder.get_current_workspace()?; + let folder = guard + .as_ref() + .ok_or(FlowyError::internal().with_context("folder is not initialized"))?; + let workspace = folder.get_current_workspace().ok_or( + FlowyError::record_not_found().with_context("Can't find the current workspace id "), + )?; let views = folder .views @@ -378,7 +382,7 @@ impl FolderManager { } }; - Some(workspace_pb) + Ok(workspace_pb) } async fn get_current_workspace_id(&self) -> FlowyResult<String> { @@ -1274,7 +1278,7 @@ impl Deref for MutexFolder { unsafe impl Sync for MutexFolder {} unsafe impl Send for MutexFolder {} -pub enum FolderInitializeDataSource { +pub enum FolderInitDataSource { /// It means using the data stored on local disk to initialize the folder LocalDisk { create_if_not_exist: bool }, /// If there is no data stored on local disk, we will use the data from the server to initialize the folder @@ -1283,6 +1287,16 @@ pub enum FolderInitializeDataSource { FolderData(FolderData), } +impl Display for FolderInitDataSource { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + FolderInitDataSource::LocalDisk { .. } => f.write_fmt(format_args!("LocalDisk")), + FolderInitDataSource::Cloud(_) => f.write_fmt(format_args!("Cloud")), + FolderInitDataSource::FolderData(_) => f.write_fmt(format_args!("Custom FolderData")), + } + } +} + fn is_exist_in_local_disk(user: &Arc<dyn FolderUser>, doc_id: &str) -> FlowyResult<bool> { let uid = user.user_id()?; if let Some(collab_db) = user.collab_db(uid)?.upgrade() { diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs index 2c46cebbaef2..2a55c945d9fa 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs @@ -20,7 +20,7 @@ where &self, document_id: &str, workspace_id: &str, - ) -> FutureResult<Vec<Vec<u8>>, Error> { + ) -> FutureResult<Vec<Vec<u8>>, FlowyError> { let workspace_id = workspace_id.to_string(); let try_get_client = self.0.try_get_client(); let document_id = document_id.to_string(); diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs index f2086f960878..71985ced42f6 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs @@ -4,7 +4,9 @@ use collab::core::origin::CollabOrigin; use collab_entity::CollabType; use flowy_error::FlowyError; -use flowy_folder_deps::cloud::{Folder, FolderCloudService, FolderData, FolderSnapshot, Workspace}; +use flowy_folder_deps::cloud::{ + Folder, FolderCloudService, FolderData, FolderSnapshot, Workspace, WorkspaceRecord, +}; use lib_infra::future::FutureResult; use crate::af_cloud::AFServer; @@ -19,6 +21,35 @@ where FutureResult::new(async move { Err(anyhow!("Not support yet")) }) } + fn open_workspace(&self, workspace_id: &str) -> FutureResult<(), Error> { + let workspace_id = workspace_id.to_string(); + let try_get_client = self.0.try_get_client(); + FutureResult::new(async move { + let client = try_get_client?; + let _ = client.open_workspace(&workspace_id).await?; + Ok(()) + }) + } + + fn get_all_workspace(&self) -> FutureResult<Vec<WorkspaceRecord>, Error> { + let try_get_client = self.0.try_get_client(); + FutureResult::new(async move { + let client = try_get_client?; + let records = client + .get_user_workspace_info() + .await? + .workspaces + .into_iter() + .map(|af_workspace| WorkspaceRecord { + id: af_workspace.workspace_id.to_string(), + name: af_workspace.workspace_name, + created_at: af_workspace.created_at.timestamp(), + }) + .collect::<Vec<_>>(); + Ok(records) + }) + } + fn get_folder_data( &self, workspace_id: &str, diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs index 256282e0ab97..ed29ee33b75a 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs @@ -113,7 +113,17 @@ where }) } - fn get_all_user_workspaces(&self, _uid: i64) -> FutureResult<Vec<UserWorkspace>, Error> { + fn open_workspace(&self, workspace_id: &str) -> FutureResult<UserWorkspace, FlowyError> { + let try_get_client = self.server.try_get_client(); + let workspace_id = workspace_id.to_string(); + FutureResult::new(async move { + let client = try_get_client?; + let af_workspace = client.open_workspace(&workspace_id).await?; + Ok(to_user_workspace(af_workspace)) + }) + } + + fn get_all_workspace(&self, _uid: i64) -> FutureResult<Vec<UserWorkspace>, Error> { let try_get_client = self.server.try_get_client(); FutureResult::new(async move { let workspaces = try_get_client?.get_workspaces().await?; diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs index 09542ee9c190..740ac3c8a58b 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs @@ -4,12 +4,12 @@ use std::sync::Arc; use anyhow::Error; use client_api::notify::{TokenState, TokenStateReceiver}; use client_api::ws::{ - BusinessID, WSClient, WSClientConfig, WSConnectStateReceiver, WebSocketChannel, + BusinessID, ConnectState, WSClient, WSClientConfig, WSConnectStateReceiver, WebSocketChannel, }; use client_api::Client; use tokio::sync::watch; use tokio_stream::wrappers::WatchStream; -use tracing::{error, info}; +use tracing::{error, event, info}; use flowy_database_deps::cloud::DatabaseCloudService; use flowy_document_deps::cloud::DocumentCloudService; @@ -145,7 +145,8 @@ impl AppFlowyServer for AFCloudServer { fn collab_ws_channel( &self, object_id: &str, - ) -> FutureResult<Option<(Arc<WebSocketChannel>, WSConnectStateReceiver)>, anyhow::Error> { + ) -> FutureResult<Option<(Arc<WebSocketChannel>, WSConnectStateReceiver, bool)>, anyhow::Error> + { if self.enable_sync.load(Ordering::SeqCst) { let object_id = object_id.to_string(); let weak_ws_client = Arc::downgrade(&self.ws_client); @@ -155,7 +156,7 @@ impl AppFlowyServer for AFCloudServer { Some(ws_client) => { let channel = ws_client.subscribe(BusinessID::CollabId, object_id).ok(); let connect_state_recv = ws_client.subscribe_connect_state(); - Ok(channel.map(|c| (c, connect_state_recv))) + Ok(channel.map(|c| (c, connect_state_recv, ws_client.is_connected()))) }, } }) @@ -190,24 +191,33 @@ fn spawn_ws_conn( if let Some(ws_client) = weak_ws_client.upgrade() { let mut state_recv = ws_client.subscribe_connect_state(); while let Ok(state) = state_recv.recv().await { - if !state.is_timeout() { - continue; - } - - // Try to reconnect if the connection is timed out. - if let (Some(api_client), Some(device_id)) = - (weak_api_client.upgrade(), weak_device_id.upgrade()) - { - if enable_sync.load(Ordering::SeqCst) { - info!("🟢websocket state: {:?}, reconnecting", state); - let device_id = device_id.read().clone(); - match api_client.ws_url(&device_id) { - Ok(ws_addr) => { - let _ = ws_client.connect(ws_addr).await; - }, - Err(err) => error!("Failed to get ws url: {}", err), + info!("[websocket] state: {:?}", state); + match state { + ConnectState::PingTimeout => { + // Try to reconnect if the connection is timed out. + if let (Some(api_client), Some(device_id)) = + (weak_api_client.upgrade(), weak_device_id.upgrade()) + { + if enable_sync.load(Ordering::SeqCst) { + let device_id = device_id.read().clone(); + match api_client.ws_url(&device_id) { + Ok(ws_addr) => { + event!(tracing::Level::INFO, "🟢reconnecting websocket"); + let _ = ws_client.connect(ws_addr).await; + }, + Err(err) => error!("Failed to get ws url: {}", err), + } + } } - } + }, + ConnectState::Unauthorized => { + if let Some(api_client) = weak_api_client.upgrade() { + if enable_sync.load(Ordering::SeqCst) { + let _ = api_client.refresh().await; + } + } + }, + _ => {}, } } } diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs index 6797270a0056..b392c9d28bb8 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs @@ -1,6 +1,7 @@ use anyhow::Error; use flowy_document_deps::cloud::*; +use flowy_error::FlowyError; use lib_infra::future::FutureResult; pub(crate) struct LocalServerDocumentCloudServiceImpl(); @@ -10,7 +11,7 @@ impl DocumentCloudService for LocalServerDocumentCloudServiceImpl { &self, _document_id: &str, _workspace_id: &str, - ) -> FutureResult<Vec<Vec<u8>>, Error> { + ) -> FutureResult<Vec<Vec<u8>>, FlowyError> { FutureResult::new(async move { Ok(vec![]) }) } diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs index 45c4efbc323a..7f84264150b1 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use anyhow::Error; use flowy_folder_deps::cloud::{ - gen_workspace_id, FolderCloudService, FolderData, FolderSnapshot, Workspace, + gen_workspace_id, FolderCloudService, FolderData, FolderSnapshot, Workspace, WorkspaceRecord, }; use lib_infra::future::FutureResult; use lib_infra::util::timestamp; @@ -11,6 +11,7 @@ use lib_infra::util::timestamp; use crate::local_server::LocalServerDB; pub(crate) struct LocalServerFolderCloudServiceImpl { + #[allow(dead_code)] pub db: Arc<dyn LocalServerDB>, } @@ -27,6 +28,14 @@ impl FolderCloudService for LocalServerFolderCloudServiceImpl { }) } + fn open_workspace(&self, _workspace_id: &str) -> FutureResult<(), Error> { + FutureResult::new(async { Ok(()) }) + } + + fn get_all_workspace(&self) -> FutureResult<Vec<WorkspaceRecord>, Error> { + FutureResult::new(async { Ok(vec![]) }) + } + fn get_folder_data( &self, _workspace_id: &str, @@ -43,18 +52,12 @@ impl FolderCloudService for LocalServerFolderCloudServiceImpl { FutureResult::new(async move { Ok(vec![]) }) } - fn get_folder_updates(&self, workspace_id: &str, uid: i64) -> FutureResult<Vec<Vec<u8>>, Error> { - let weak_db = Arc::downgrade(&self.db); - let workspace_id = workspace_id.to_string(); - FutureResult::new(async move { - match weak_db.upgrade() { - None => Ok(vec![]), - Some(db) => { - let updates = db.get_collab_updates(uid, &workspace_id)?; - Ok(updates) - }, - } - }) + fn get_folder_updates( + &self, + _workspace_id: &str, + _uid: i64, + ) -> FutureResult<Vec<Vec<u8>>, Error> { + FutureResult::new(async move { Ok(vec![]) }) } fn service_name(&self) -> String { diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs index fcdf519ace8e..fbb624739cd9 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs @@ -116,7 +116,13 @@ impl UserCloudService for LocalServerUserAuthServiceImpl { FutureResult::new(async { result }) } - fn get_all_user_workspaces(&self, _uid: i64) -> FutureResult<Vec<UserWorkspace>, Error> { + fn open_workspace(&self, _workspace_id: &str) -> FutureResult<UserWorkspace, FlowyError> { + FutureResult::new(async { + Err(FlowyError::not_support().with_context("local server doesn't support open workspace")) + }) + } + + fn get_all_workspace(&self, _uid: i64) -> FutureResult<Vec<UserWorkspace>, Error> { FutureResult::new(async { Ok(vec![]) }) } diff --git a/frontend/rust-lib/flowy-server/src/local_server/server.rs b/frontend/rust-lib/flowy-server/src/local_server/server.rs index af300a6e6986..8215dae2f5cf 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/server.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/server.rs @@ -23,7 +23,6 @@ use crate::AppFlowyServer; pub trait LocalServerDB: Send + Sync + 'static { fn get_user_profile(&self, uid: i64) -> Result<UserProfile, FlowyError>; fn get_user_workspace(&self, uid: i64) -> Result<Option<UserWorkspace>, FlowyError>; - fn get_collab_updates(&self, uid: i64, object_id: &str) -> Result<Vec<Vec<u8>>, FlowyError>; } pub struct LocalServer { diff --git a/frontend/rust-lib/flowy-server/src/server.rs b/frontend/rust-lib/flowy-server/src/server.rs index 9effd441af97..d6699b687b95 100644 --- a/frontend/rust-lib/flowy-server/src/server.rs +++ b/frontend/rust-lib/flowy-server/src/server.rs @@ -104,7 +104,8 @@ pub trait AppFlowyServer: Send + Sync + 'static { fn collab_ws_channel( &self, _object_id: &str, - ) -> FutureResult<Option<(Arc<WebSocketChannel>, WSConnectStateReceiver)>, anyhow::Error> { + ) -> FutureResult<Option<(Arc<WebSocketChannel>, WSConnectStateReceiver, bool)>, anyhow::Error> + { FutureResult::new(async { Ok(None) }) } diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/document.rs b/frontend/rust-lib/flowy-server/src/supabase/api/document.rs index baa140d9cc94..82ea76ca8fd9 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/document.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/document.rs @@ -32,7 +32,7 @@ where &self, document_id: &str, workspace_id: &str, - ) -> FutureResult<Vec<Vec<u8>>, Error> { + ) -> FutureResult<Vec<Vec<u8>>, FlowyError> { let try_get_postgrest = self.server.try_get_weak_postgrest(); let document_id = document_id.to_string(); let (tx, rx) = channel(); @@ -43,7 +43,7 @@ where let action = FetchObjectUpdateAction::new(document_id, CollabType::Document, postgrest); let updates = action.run_with_fix_interval(5, 10).await?; if updates.is_empty() { - return Err(FlowyError::collab_not_sync().into()); + return Err(FlowyError::collab_not_sync()); } Ok(updates) } diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs b/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs index 5a16416359a5..55c199512edc 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs @@ -9,6 +9,7 @@ use tokio::sync::oneshot::channel; use flowy_folder_deps::cloud::{ gen_workspace_id, Folder, FolderCloudService, FolderData, FolderSnapshot, Workspace, + WorkspaceRecord, }; use lib_dispatch::prelude::af_spawn; use lib_infra::future::FutureResult; @@ -69,6 +70,14 @@ where }) } + fn open_workspace(&self, _workspace_id: &str) -> FutureResult<(), Error> { + FutureResult::new(async { Ok(()) }) + } + + fn get_all_workspace(&self) -> FutureResult<Vec<WorkspaceRecord>, Error> { + FutureResult::new(async { Ok(vec![]) }) + } + fn get_folder_data( &self, workspace_id: &str, diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs index f915c2cbfb3a..e38c992a960c 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs @@ -226,7 +226,13 @@ where }) } - fn get_all_user_workspaces(&self, uid: i64) -> FutureResult<Vec<UserWorkspace>, Error> { + fn open_workspace(&self, _workspace_id: &str) -> FutureResult<UserWorkspace, FlowyError> { + FutureResult::new(async { + Err(FlowyError::not_support().with_context("supabase server doesn't support open workspace")) + }) + } + + fn get_all_workspace(&self, uid: i64) -> FutureResult<Vec<UserWorkspace>, Error> { let try_get_postgrest = self.server.try_get_postgrest(); FutureResult::new(async move { let postgrest = try_get_postgrest?; diff --git a/frontend/rust-lib/flowy-task/src/scheduler.rs b/frontend/rust-lib/flowy-task/src/scheduler.rs index 70d861a9cfba..c7e54094726d 100644 --- a/frontend/rust-lib/flowy-task/src/scheduler.rs +++ b/frontend/rust-lib/flowy-task/src/scheduler.rs @@ -1,17 +1,17 @@ -use crate::queue::TaskQueue; -use crate::store::TaskStore; -use crate::{Task, TaskContent, TaskId, TaskState}; -use anyhow::Error; - -use lib_infra::future::BoxResultFuture; - use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; +use anyhow::Error; use tokio::sync::{watch, RwLock}; use tokio::time::interval; +use lib_infra::future::BoxResultFuture; + +use crate::queue::TaskQueue; +use crate::store::TaskStore; +use crate::{Task, TaskContent, TaskId, TaskState}; + pub struct TaskDispatcher { queue: TaskQueue, store: TaskStore, @@ -122,6 +122,9 @@ impl TaskDispatcher { } } + pub fn clear_task(&mut self) { + self.store.clear(); + } pub fn next_task_id(&self) -> TaskId { self.store.next_task_id() } diff --git a/frontend/rust-lib/flowy-user-deps/src/cloud.rs b/frontend/rust-lib/flowy-user-deps/src/cloud.rs index 2b2b8635641d..e6056f578f1f 100644 --- a/frontend/rust-lib/flowy-user-deps/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-deps/src/cloud.rs @@ -93,8 +93,10 @@ pub trait UserCloudService: Send + Sync + 'static { /// return None if the user is not found fn get_user_profile(&self, credential: UserCredentials) -> FutureResult<UserProfile, FlowyError>; + fn open_workspace(&self, workspace_id: &str) -> FutureResult<UserWorkspace, FlowyError>; + /// Return the all the workspaces of the user - fn get_all_user_workspaces(&self, uid: i64) -> FutureResult<Vec<UserWorkspace>, Error>; + fn get_all_workspace(&self, uid: i64) -> FutureResult<Vec<UserWorkspace>, Error>; fn add_workspace_member( &self, diff --git a/frontend/rust-lib/flowy-user-deps/src/entities.rs b/frontend/rust-lib/flowy-user-deps/src/entities.rs index beda5f548c35..55bd733a4b7f 100644 --- a/frontend/rust-lib/flowy-user-deps/src/entities.rs +++ b/frontend/rust-lib/flowy-user-deps/src/entities.rs @@ -138,7 +138,7 @@ pub struct UserWorkspace { pub id: String, pub name: String, pub created_at: DateTime<Utc>, - /// The database storage id is used indexing all the database in current workspace. + /// The database storage id is used indexing all the database views in current workspace. #[serde(rename = "database_storage_id")] pub database_views_aggregate_id: String, } diff --git a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs index 2ee46aaaa2a6..eb5b105fa233 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs @@ -1,9 +1,12 @@ use std::convert::TryInto; +use validator::Validate; + use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_user_deps::entities::*; use crate::entities::parser::{UserEmail, UserIcon, UserName, UserOpenaiKey, UserPassword}; +use crate::entities::required_not_empty_str; use crate::entities::AuthTypePB; use crate::errors::ErrorCode; use crate::services::entities::HistoricalUser; @@ -217,10 +220,11 @@ impl From<Vec<UserWorkspace>> for RepeatedUserWorkspacePB { } } -#[derive(ProtoBuf, Default, Debug, Clone)] +#[derive(ProtoBuf, Default, Debug, Clone, Validate)] pub struct UserWorkspacePB { #[pb(index = 1)] - pub id: String, + #[validate(custom = "required_not_empty_str")] + pub workspace_id: String, #[pb(index = 2)] pub name: String, @@ -229,7 +233,7 @@ pub struct UserWorkspacePB { impl From<UserWorkspace> for UserWorkspacePB { fn from(value: UserWorkspace) -> Self { Self { - id: value.id, + workspace_id: value.id, name: value.name, } } diff --git a/frontend/rust-lib/flowy-user/src/entities/workspace_member.rs b/frontend/rust-lib/flowy-user/src/entities/workspace_member.rs index d94848c58841..a7a368f6e64b 100644 --- a/frontend/rust-lib/flowy-user/src/entities/workspace_member.rs +++ b/frontend/rust-lib/flowy-user/src/entities/workspace_member.rs @@ -103,3 +103,10 @@ impl From<Role> for AFRolePB { } } } + +#[derive(ProtoBuf, Default, Clone, Validate)] +pub struct UserWorkspaceIdPB { + #[pb(index = 1)] + #[validate(custom = "required_not_empty_str")] + pub workspace_id: String, +} diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index 35bc71bbdc2d..0e5819878661 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -2,7 +2,6 @@ use std::sync::Weak; use std::{convert::TryInto, sync::Arc}; use serde_json::Value; -use tracing::event; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_sqlite::kv::StorePreferences; @@ -105,17 +104,11 @@ pub async fn get_user_profile_handler( user_profile.email = "".to_string(); } - event!( - tracing::Level::DEBUG, - "Get user profile: {:?}", - user_profile - ); - data_result_ok(user_profile.into()) } #[tracing::instrument(level = "debug", skip(manager))] -pub async fn sign_out(manager: AFPluginState<Weak<UserManager>>) -> Result<(), FlowyError> { +pub async fn sign_out_handler(manager: AFPluginState<Weak<UserManager>>) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; manager.sign_out().await?; Ok(()) @@ -425,7 +418,7 @@ pub async fn get_cloud_config_handler( } #[tracing::instrument(level = "debug", skip(manager), err)] -pub async fn get_all_user_workspace_handler( +pub async fn get_all_workspace_handler( manager: AFPluginState<Weak<UserManager>>, ) -> DataResult<RepeatedUserWorkspacePB, FlowyError> { let manager = upgrade_manager(manager)?; @@ -436,12 +429,12 @@ pub async fn get_all_user_workspace_handler( #[tracing::instrument(level = "debug", skip(data, manager), err)] pub async fn open_workspace_handler( - data: AFPluginData<UserWorkspacePB>, + data: AFPluginData<UserWorkspaceIdPB>, manager: AFPluginState<Weak<UserManager>>, ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; - let params = data.into_inner(); - manager.open_workspace(¶ms.id).await?; + let params = data.validate()?.into_inner(); + manager.open_workspace(¶ms.workspace_id).await?; Ok(()) } diff --git a/frontend/rust-lib/flowy-user/src/event_map.rs b/frontend/rust-lib/flowy-user/src/event_map.rs index 07953d232dab..518004b8c522 100644 --- a/frontend/rust-lib/flowy-user/src/event_map.rs +++ b/frontend/rust-lib/flowy-user/src/event_map.rs @@ -29,7 +29,7 @@ pub fn init(user_session: Weak<UserManager>) -> AFPlugin { .event(UserEvent::SignUp, sign_up) .event(UserEvent::InitUser, init_user_handler) .event(UserEvent::GetUserProfile, get_user_profile_handler) - .event(UserEvent::SignOut, sign_out) + .event(UserEvent::SignOut, sign_out_handler) .event(UserEvent::UpdateUserProfile, update_user_profile_handler) .event(UserEvent::SetAppearanceSetting, set_appearance_setting) .event(UserEvent::GetAppearanceSetting, get_appearance_setting) @@ -41,7 +41,7 @@ pub fn init(user_session: Weak<UserManager>) -> AFPlugin { .event(UserEvent::OauthSignIn, oauth_handler) .event(UserEvent::GetSignInURL, get_sign_in_url_handler) .event(UserEvent::GetOauthURLWithProvider, sign_in_with_provider_handler) - .event(UserEvent::GetAllUserWorkspaces, get_all_user_workspace_handler) + .event(UserEvent::GetAllWorkspace, get_all_workspace_handler) .event(UserEvent::OpenWorkspace, open_workspace_handler) .event(UserEvent::UpdateNetworkState, update_network_state_handler) .event(UserEvent::GetHistoricalUsers, get_historical_users_handler) @@ -60,7 +60,7 @@ pub fn init(user_session: Weak<UserManager>) -> AFPlugin { .event(UserEvent::AddWorkspaceMember, add_workspace_member_handler) .event(UserEvent::RemoveWorkspaceMember, delete_workspace_member_handler) .event(UserEvent::GetWorkspaceMember, get_workspace_member_handler) - .event(UserEvent::UpdateWorkspaceMember, update_workspace_member_handler,) + .event(UserEvent::UpdateWorkspaceMember, update_workspace_member_handler) } #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] @@ -129,10 +129,10 @@ pub enum UserEvent { CheckEncryptionSign = 16, /// Return the all the workspaces of the user - #[event()] - GetAllUserWorkspaces = 20, + #[event(output = "RepeatedUserWorkspacePB")] + GetAllWorkspace = 17, - #[event(input = "UserWorkspacePB")] + #[event(input = "UserWorkspaceIdPB")] OpenWorkspace = 21, #[event(input = "NetworkStatePB")] diff --git a/frontend/rust-lib/flowy-user/src/manager.rs b/frontend/rust-lib/flowy-user/src/manager.rs index 6adf0e23c0d6..15a6fadf6a77 100644 --- a/frontend/rust-lib/flowy-user/src/manager.rs +++ b/frontend/rust-lib/flowy-user/src/manager.rs @@ -1,4 +1,5 @@ use std::string::ToString; +use std::sync::atomic::{AtomicI64, Ordering}; use std::sync::{Arc, Weak}; use collab_user::core::MutexUserAwareness; @@ -55,7 +56,7 @@ impl UserSessionConfig { } pub struct UserManager { - database: UserDB, + database: Arc<UserDB>, session_config: UserSessionConfig, pub(crate) cloud_services: Arc<dyn UserCloudServiceProvider>, pub(crate) store_preferences: Arc<StorePreferences>, @@ -64,7 +65,8 @@ pub struct UserManager { pub(crate) collab_builder: Weak<AppFlowyCollabBuilder>, pub(crate) collab_interact: RwLock<Arc<dyn CollabInteract>>, resumable_sign_up: Mutex<Option<ResumableSignUp>>, - current_session: parking_lot::RwLock<Option<Session>>, + current_session: Arc<parking_lot::RwLock<Option<Session>>>, + refresh_user_profile_since: AtomicI64, } impl UserManager { @@ -74,10 +76,11 @@ impl UserManager { store_preferences: Arc<StorePreferences>, collab_builder: Weak<AppFlowyCollabBuilder>, ) -> Arc<Self> { - let database = UserDB::new(&session_config.root_dir); + let database = Arc::new(UserDB::new(&session_config.root_dir)); let user_status_callback: RwLock<Arc<dyn UserStatusCallback>> = RwLock::new(Arc::new(DefaultUserStatusCallback)); + let refresh_user_profile_since = AtomicI64::new(0); let user_manager = Arc::new(Self { database, session_config, @@ -89,6 +92,7 @@ impl UserManager { collab_interact: RwLock::new(Arc::new(DefaultCollabInteract)), resumable_sign_up: Default::default(), current_session: Default::default(), + refresh_user_profile_since, }); let weak_user_manager = Arc::downgrade(&user_manager); @@ -120,13 +124,28 @@ impl UserManager { /// a local data migration for the user. After ensuring the user's data is migrated and up-to-date, /// the function will set up the collaboration configuration and initialize the user's awareness. Upon successful /// completion, a user status callback is invoked to signify that the initialization process is complete. + #[instrument(level = "debug", skip_all, err)] pub async fn init<C: UserStatusCallback + 'static, I: CollabInteract>( &self, user_status_callback: C, collab_interact: I, ) -> Result<(), FlowyError> { + let user_status_callback = Arc::new(user_status_callback); + *self.user_status_callback.write().await = user_status_callback.clone(); + *self.collab_interact.write().await = Arc::new(collab_interact); + if let Ok(session) = self.get_session() { let user = self.get_user_profile(session.user_id).await?; + + event!( + tracing::Level::INFO, + "init user session: {}:{}", + user.uid, + user.email + ); + + // Set the token if the current cloud service using token to authenticate + // Currently, only the AppFlowy cloud using token to init the client api. if let Err(err) = self.cloud_services.set_token(&user.token) { error!("Set token failed: {}", err); } @@ -134,10 +153,13 @@ impl UserManager { // Subscribe the token state let weak_pool = Arc::downgrade(&self.db_pool(user.uid)?); if let Some(mut token_state_rx) = self.cloud_services.subscribe_token_state() { + event!(tracing::Level::DEBUG, "Listen token state change"); af_spawn(async move { while let Some(token_state) = token_state_rx.next().await { + debug!("Token state changed: {:?}", token_state); match token_state { UserTokenState::Refresh { token } => { + // Only save the token if the token is different from the current token if token != user.token { if let Some(pool) = weak_pool.upgrade() { // Save the new token @@ -147,19 +169,14 @@ impl UserManager { } } }, - UserTokenState::Invalid => { - send_auth_state_notification(AuthStateChangedPB { - state: AuthStatePB::InvalidAuth, - message: "Token is invalid".to_string(), - }) - .send(); - }, + UserTokenState::Invalid => {}, } } }); } // Do the user data migration if needed + event!(tracing::Level::INFO, "Prepare user data migration"); match ( self.database.get_collab_db(session.user_id), self.database.get_pool(session.user_id), @@ -202,8 +219,6 @@ impl UserManager { error!("Failed to call did_init callback: {:?}", e); } } - *self.user_status_callback.write().await = Arc::new(user_status_callback); - *self.collab_interact.write().await = Arc::new(collab_interact); Ok(()) } @@ -380,6 +395,7 @@ impl UserManager { self .save_auth_data(&response, auth_type, &new_session) .await?; + self .user_status_callback .read() @@ -445,14 +461,28 @@ impl UserManager { pub async fn get_user_profile(&self, uid: i64) -> Result<UserProfile, FlowyError> { let user: UserProfile = user_table::dsl::user_table .filter(user_table::id.eq(&uid.to_string())) - .first::<UserTable>(&*(self.db_connection(uid)?))? + .first::<UserTable>(&*(self.db_connection(uid)?)) + .map_err(|err| { + FlowyError::record_not_found().with_context(format!( + "Can't find the user profile for user id: {}, error: {:?}", + uid, err + )) + })? .into(); Ok(user) } - #[tracing::instrument(level = "info", skip_all)] + #[tracing::instrument(level = "info", skip_all, err)] pub async fn refresh_user_profile(&self, old_user_profile: &UserProfile) -> FlowyResult<()> { + let now = chrono::Utc::now().timestamp(); + + // Add debounce to avoid too many requests + if now - self.refresh_user_profile_since.load(Ordering::SeqCst) < 5 { + return Ok(()); + } + + self.refresh_user_profile_since.store(now, Ordering::SeqCst); let uid = old_user_profile.uid; let result: Result<UserProfile, FlowyError> = self .cloud_services @@ -494,12 +524,12 @@ impl UserManager { }, Err(err) => { // If the user is not found, notify the frontend to logout - if err.is_record_not_found() { + if err.is_unauthorized() { event!( - tracing::Level::INFO, - "User is not found on the server when refreshing profile" + tracing::Level::ERROR, + "User is unauthorized, sign out the user" ); - + self.sign_out().await?; send_auth_state_notification(AuthStateChangedPB { state: AuthStatePB::InvalidAuth, message: "User is not found on the server".to_string(), @@ -641,6 +671,7 @@ impl UserManager { Ok(url) } + #[instrument(level = "info", skip_all, err)] async fn save_auth_data( &self, response: &impl UserAuthResponse, diff --git a/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs b/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs index d5945f1c22a5..b317ff2c4e93 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs @@ -5,11 +5,13 @@ use collab::core::origin::{CollabClient, CollabOrigin}; use collab_document::document::Document; use collab_document::document_data::default_document_data; use collab_folder::Folder; +use tracing::{event, instrument}; use collab_integrate::{RocksCollabDB, YrsDocAction}; use flowy_error::{internal_error, FlowyResult}; use crate::migrations::migration::UserDataMigration; +use crate::migrations::util::load_collab; use crate::services::entities::Session; /// Migrate the first level documents of the workspace by inserting documents @@ -20,39 +22,42 @@ impl UserDataMigration for HistoricalEmptyDocumentMigration { "historical_empty_document" } + #[instrument(name = "HistoricalEmptyDocumentMigration", skip_all, err)] fn run(&self, session: &Session, collab_db: &Arc<RocksCollabDB>) -> FlowyResult<()> { let write_txn = collab_db.write_txn(); - if let Ok(updates) = write_txn.get_all_updates(session.user_id, &session.user_workspace.id) { - let origin = CollabOrigin::Client(CollabClient::new(session.user_id, "phantom")); - // Deserialize the folder from the raw data - let folder = Folder::from_collab_raw_data( - session.user_id, - origin.clone(), - updates, - &session.user_workspace.id, - vec![], - )?; - - // Migration the first level documents of the workspace + let origin = CollabOrigin::Client(CollabClient::new(session.user_id, "phantom")); + // Deserialize the folder from the raw data + if let Ok(folder_collab) = load_collab(session.user_id, &write_txn, &session.user_workspace.id) + { + let folder = Folder::open(session.user_id, folder_collab, None)?; + + // Migration the first level documents of the workspace. The first level documents do not have + // any updates. So when calling load_collab, it will return error. let migration_views = folder.get_workspace_views(&session.user_workspace.id); for view in migration_views { - // Read all updates of the view - if let Ok(view_updates) = write_txn.get_all_updates(session.user_id, &view.id) { - if Document::from_updates(origin.clone(), view_updates, &view.id, vec![]).is_err() { - // Create a document with default data - let document_data = default_document_data(); - let collab = Arc::new(MutexCollab::new(origin.clone(), &view.id, vec![])); - if let Ok(document) = Document::create_with_data(collab.clone(), document_data) { - // Remove all old updates and then insert the new update - let (doc_state, sv) = document.get_collab().encode_as_update_v1(); - write_txn - .flush_doc_with(session.user_id, &view.id, &doc_state, &sv) - .map_err(internal_error)?; + if load_collab(session.user_id, &write_txn, &view.id).is_err() { + // Create a document with default data + let document_data = default_document_data(); + let collab = Arc::new(MutexCollab::new(origin.clone(), &view.id, vec![])); + if let Ok(document) = Document::create_with_data(collab.clone(), document_data) { + // Remove all old updates and then insert the new update + let (doc_state, sv) = document.get_collab().encode_as_update_v1(); + if let Err(err) = write_txn.flush_doc_with(session.user_id, &view.id, &doc_state, &sv) { + event!( + tracing::Level::ERROR, + "Failed to migrate document {}, error: {}", + view.id, + err + ); + } else { + event!(tracing::Level::INFO, "Did migrate document {}", view.id); } } } } } + + event!(tracing::Level::INFO, "Save all migrated documents"); write_txn.commit_transaction().map_err(internal_error)?; Ok(()) } diff --git a/frontend/rust-lib/flowy-user/src/migrations/migration.rs b/frontend/rust-lib/flowy-user/src/migrations/migration.rs index c156c4b99464..e874c39946a0 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/migration.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/migration.rs @@ -2,7 +2,6 @@ use std::sync::Arc; use chrono::NaiveDateTime; use diesel::{RunQueryDsl, SqliteConnection}; -use tracing::event; use collab_integrate::RocksCollabDB; use flowy_error::FlowyResult; @@ -55,12 +54,6 @@ impl UserLocalDataMigration { { let migration_name = migration.name().to_string(); if !duplicated_names.contains(&migration_name) { - event!( - tracing::Level::INFO, - "Running migration {}", - migration.name() - ); - migration.run(&self.session, &self.collab_db)?; applied_migrations.push(migration.name().to_string()); save_record(&conn, &migration_name); diff --git a/frontend/rust-lib/flowy-user/src/migrations/mod.rs b/frontend/rust-lib/flowy-user/src/migrations/mod.rs index be26507ec17e..4989032da651 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/mod.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/mod.rs @@ -3,4 +3,5 @@ pub use define::*; mod define; pub mod document_empty_content; pub mod migration; +mod util; pub mod workspace_and_favorite_v1; diff --git a/frontend/rust-lib/flowy-user/src/migrations/util.rs b/frontend/rust-lib/flowy-user/src/migrations/util.rs new file mode 100644 index 000000000000..92f2c5ee4faa --- /dev/null +++ b/frontend/rust-lib/flowy-user/src/migrations/util.rs @@ -0,0 +1,23 @@ +use std::sync::Arc; + +use collab::core::collab::MutexCollab; +use collab::preclude::Collab; + +use collab_integrate::{PersistenceError, YrsDocAction}; +use flowy_error::{internal_error, FlowyResult}; + +pub fn load_collab<'a, R>( + uid: i64, + collab_r_txn: &R, + object_id: &str, +) -> FlowyResult<Arc<MutexCollab>> +where + R: YrsDocAction<'a>, + PersistenceError: From<R::Error>, +{ + let collab = Collab::new(uid, object_id, "phantom", vec![]); + collab + .with_origin_transact_mut(|txn| collab_r_txn.load_doc_with_txn(uid, &object_id, txn)) + .map_err(internal_error)?; + Ok(Arc::new(MutexCollab::from_collab(collab))) +} diff --git a/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs b/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs index 768b38f20de0..bcd1aa87202a 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs @@ -1,12 +1,13 @@ use std::sync::Arc; -use collab::core::origin::{CollabClient, CollabOrigin}; use collab_folder::Folder; +use tracing::instrument; use collab_integrate::{RocksCollabDB, YrsDocAction}; use flowy_error::{internal_error, FlowyResult}; use crate::migrations::migration::UserDataMigration; +use crate::migrations::util::load_collab; use crate::services::entities::Session; /// 1. Migrate the workspace: { favorite: [view_id] } to { favorite: { uid: [view_id] } } @@ -19,19 +20,11 @@ impl UserDataMigration for FavoriteV1AndWorkspaceArrayMigration { "workspace_favorite_v1_and_workspace_array_migration" } + #[instrument(name = "FavoriteV1AndWorkspaceArrayMigration", skip_all, err)] fn run(&self, session: &Session, collab_db: &Arc<RocksCollabDB>) -> FlowyResult<()> { let write_txn = collab_db.write_txn(); - if let Ok(updates) = write_txn.get_all_updates(session.user_id, &session.user_workspace.id) { - let origin = CollabOrigin::Client(CollabClient::new(session.user_id, "phantom")); - // Deserialize the folder from the raw data - let folder = Folder::from_collab_raw_data( - session.user_id, - origin, - updates, - &session.user_workspace.id, - vec![], - )?; - + if let Ok(collab) = load_collab(session.user_id, &write_txn, &session.user_workspace.id) { + let folder = Folder::open(session.user_id, collab, None)?; folder.migrate_workspace_to_view(); let favorite_view_ids = folder @@ -48,8 +41,9 @@ impl UserDataMigration for FavoriteV1AndWorkspaceArrayMigration { write_txn .flush_doc_with(session.user_id, &session.user_workspace.id, &doc_state, &sv) .map_err(internal_error)?; - write_txn.commit_transaction().map_err(internal_error)?; } + + write_txn.commit_transaction().map_err(internal_error)?; Ok(()) } } diff --git a/frontend/rust-lib/flowy-user/src/services/user_workspace.rs b/frontend/rust-lib/flowy-user/src/services/user_workspace.rs index 4112c5438e19..32840bd5fd1e 100644 --- a/frontend/rust-lib/flowy-user/src/services/user_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/services/user_workspace.rs @@ -2,6 +2,7 @@ use std::convert::TryFrom; use std::sync::Arc; use collab_entity::{CollabObject, CollabType}; +use tracing::{error, instrument}; use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::schema::user_workspace_table; @@ -15,8 +16,14 @@ use crate::notification::{send_notification, UserNotification}; use crate::services::user_workspace_sql::UserWorkspaceTable; impl UserManager { + #[instrument(skip(self), err)] pub async fn open_workspace(&self, workspace_id: &str) -> FlowyResult<()> { let uid = self.user_id()?; + let _ = self + .cloud_services + .get_user_service()? + .open_workspace(workspace_id) + .await; if let Some(user_workspace) = self.get_user_workspace(uid, workspace_id) { if let Err(err) = self .user_status_callback @@ -25,7 +32,7 @@ impl UserManager { .open_workspace(uid, &user_workspace) .await { - tracing::error!("Open workspace failed: {:?}", err); + error!("Open workspace failed: {:?}", err); } } Ok(()) @@ -101,7 +108,7 @@ impl UserManager { if let Ok(service) = self.cloud_services.get_user_service() { if let Ok(pool) = self.db_pool(uid) { af_spawn(async move { - if let Ok(new_user_workspaces) = service.get_all_user_workspaces(uid).await { + if let Ok(new_user_workspaces) = service.get_all_workspace(uid).await { let _ = save_user_workspaces(uid, pool, &new_user_workspaces); let repeated_workspace_pbs = RepeatedUserWorkspacePB::from(new_user_workspaces); send_notification(&uid.to_string(), UserNotification::DidUpdateUserWorkspaces) diff --git a/frontend/rust-lib/lib-log/Cargo.toml b/frontend/rust-lib/lib-log/Cargo.toml index 483a4f403e21..9dc46e9121c6 100644 --- a/frontend/rust-lib/lib-log/Cargo.toml +++ b/frontend/rust-lib/lib-log/Cargo.toml @@ -7,7 +7,6 @@ edition = "2018" [dependencies] -tracing-log = { version = "0.2"} tracing-subscriber = { version = "0.3.17", features = ["registry", "env-filter", "ansi", "json"] } tracing-bunyan-formatter = "0.3.9" tracing-appender = "0.2.2" diff --git a/frontend/rust-lib/lib-log/src/lib.rs b/frontend/rust-lib/lib-log/src/lib.rs index 69271ebec1dd..323fbb056c6a 100644 --- a/frontend/rust-lib/lib-log/src/lib.rs +++ b/frontend/rust-lib/lib-log/src/lib.rs @@ -1,9 +1,11 @@ use std::sync::RwLock; +use chrono::Local; use lazy_static::lazy_static; use tracing::subscriber::set_global_default; use tracing_appender::{non_blocking::WorkerGuard, rolling::RollingFileAppender}; use tracing_bunyan_formatter::JsonStorageLayer; +use tracing_subscriber::fmt::format::Writer; use tracing_subscriber::{layer::SubscriberExt, EnvFilter}; use crate::layer::FlowyFormattingLayer; @@ -43,12 +45,12 @@ impl Builder { let (non_blocking, guard) = tracing_appender::non_blocking(self.file_appender); let subscriber = tracing_subscriber::fmt() + .with_timer(CustomTime) .with_ansi(true) - .with_target(true) + .with_target(false) .with_max_level(tracing::Level::TRACE) .with_thread_ids(false) - .with_file(false) - .with_writer(std::io::stderr) + .with_writer(std::io::stdout) .pretty() .with_env_filter(env_filter) .finish() @@ -56,7 +58,15 @@ impl Builder { .with(FlowyFormattingLayer::new(non_blocking)); set_global_default(subscriber).map_err(|e| format!("{:?}", e))?; + *LOG_GUARD.write().unwrap() = Some(guard); Ok(()) } } + +struct CustomTime; +impl tracing_subscriber::fmt::time::FormatTime for CustomTime { + fn format_time(&self, w: &mut Writer<'_>) -> std::fmt::Result { + write!(w, "{}", Local::now().format("%Y-%m-%d %H:%M:%S")) + } +} From 3434d447a7492079be5b881f2231c24556c58002 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Mon, 6 Nov 2023 00:47:20 +0800 Subject: [PATCH 24/56] chore: update collab rev (#3874) --- frontend/appflowy_tauri/src-tauri/Cargo.lock | 35 ++++++------ frontend/appflowy_tauri/src-tauri/Cargo.toml | 18 +++--- frontend/rust-lib/Cargo.lock | 37 ++++++------ frontend/rust-lib/Cargo.toml | 18 +++--- .../rust-lib/flowy-document2/src/manager.rs | 2 +- .../src/af_cloud/impls/database.rs | 13 ++++- .../src/af_cloud/impls/document.rs | 13 +++-- .../flowy-server/src/af_cloud/impls/folder.rs | 23 +++++--- .../flowy-server/src/supabase/api/user.rs | 6 +- .../src/anon_user_upgrade/sync_new_user.rs | 57 ++++++++++--------- .../src/migrations/document_empty_content.rs | 9 ++- .../migrations/workspace_and_favorite_v1.rs | 9 ++- 12 files changed, 138 insertions(+), 102 deletions(-) diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 8a79aa6952ef..1c1c96466cbf 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -138,7 +138,7 @@ checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c739029d99517282d2ec1593523a47b51a85231b#c739029d99517282d2ec1593523a47b51a85231b" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7752ab68a91ff742fae595c1b45babdc17927fea#7752ab68a91ff742fae595c1b45babdc17927fea" dependencies = [ "anyhow", "reqwest", @@ -768,7 +768,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c739029d99517282d2ec1593523a47b51a85231b#c739029d99517282d2ec1593523a47b51a85231b" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7752ab68a91ff742fae595c1b45babdc17927fea#7752ab68a91ff742fae595c1b45babdc17927fea" dependencies = [ "anyhow", "app-error", @@ -861,10 +861,11 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" dependencies = [ "anyhow", "async-trait", + "bincode", "bytes", "lib0", "parking_lot", @@ -880,7 +881,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" dependencies = [ "anyhow", "async-trait", @@ -910,7 +911,7 @@ dependencies = [ [[package]] name = "collab-derive" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" dependencies = [ "proc-macro2", "quote", @@ -922,7 +923,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" dependencies = [ "anyhow", "collab", @@ -942,7 +943,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" dependencies = [ "anyhow", "bytes", @@ -956,7 +957,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" dependencies = [ "anyhow", "chrono", @@ -998,7 +999,7 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" dependencies = [ "async-trait", "bincode", @@ -1019,7 +1020,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" dependencies = [ "anyhow", "async-trait", @@ -1046,7 +1047,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" dependencies = [ "anyhow", "collab", @@ -1445,7 +1446,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c739029d99517282d2ec1593523a47b51a85231b#c739029d99517282d2ec1593523a47b51a85231b" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7752ab68a91ff742fae595c1b45babdc17927fea#7752ab68a91ff742fae595c1b45babdc17927fea" dependencies = [ "anyhow", "app-error", @@ -2795,7 +2796,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c739029d99517282d2ec1593523a47b51a85231b#c739029d99517282d2ec1593523a47b51a85231b" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7752ab68a91ff742fae595c1b45babdc17927fea#7752ab68a91ff742fae595c1b45babdc17927fea" dependencies = [ "anyhow", "futures-util", @@ -2811,7 +2812,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c739029d99517282d2ec1593523a47b51a85231b#c739029d99517282d2ec1593523a47b51a85231b" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7752ab68a91ff742fae595c1b45babdc17927fea#7752ab68a91ff742fae595c1b45babdc17927fea" dependencies = [ "anyhow", "app-error", @@ -3247,7 +3248,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c739029d99517282d2ec1593523a47b51a85231b#c739029d99517282d2ec1593523a47b51a85231b" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7752ab68a91ff742fae595c1b45babdc17927fea#7752ab68a91ff742fae595c1b45babdc17927fea" dependencies = [ "anyhow", "reqwest", @@ -4921,7 +4922,7 @@ dependencies = [ [[package]] name = "realtime-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c739029d99517282d2ec1593523a47b51a85231b#c739029d99517282d2ec1593523a47b51a85231b" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7752ab68a91ff742fae595c1b45babdc17927fea#7752ab68a91ff742fae595c1b45babdc17927fea" dependencies = [ "anyhow", "bytes", @@ -5660,7 +5661,7 @@ dependencies = [ [[package]] name = "shared_entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c739029d99517282d2ec1593523a47b51a85231b#c739029d99517282d2ec1593523a47b51a85231b" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7752ab68a91ff742fae595c1b45babdc17927fea#7752ab68a91ff742fae595c1b45babdc17927fea" dependencies = [ "anyhow", "app-error", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index d53d248a3488..2f6f8e7ff05d 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -38,7 +38,7 @@ custom-protocol = ["tauri/custom-protocol"] # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c739029d99517282d2ec1593523a47b51a85231b" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "7752ab68a91ff742fae595c1b45babdc17927fea" } # Please use the following script to update collab. # Working directory: frontend # @@ -48,14 +48,14 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c73 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bab20052" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bab20052" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bab20052" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bab20052" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bab20052" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bab20052" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bab20052" } -collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bab20052" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "faf17b54" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "faf17b54" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "faf17b54" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "faf17b54" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "faf17b54" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "faf17b54" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "faf17b54" } +collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "faf17b54" } diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 97703309f95d..bf77c2651639 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -124,7 +124,7 @@ checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c739029d99517282d2ec1593523a47b51a85231b#c739029d99517282d2ec1593523a47b51a85231b" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7752ab68a91ff742fae595c1b45babdc17927fea#7752ab68a91ff742fae595c1b45babdc17927fea" dependencies = [ "anyhow", "reqwest", @@ -467,7 +467,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" dependencies = [ "borsh-derive", - "hashbrown 0.13.2", + "hashbrown 0.12.3", ] [[package]] @@ -666,7 +666,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c739029d99517282d2ec1593523a47b51a85231b#c739029d99517282d2ec1593523a47b51a85231b" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7752ab68a91ff742fae595c1b45babdc17927fea#7752ab68a91ff742fae595c1b45babdc17927fea" dependencies = [ "anyhow", "app-error", @@ -728,10 +728,11 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" dependencies = [ "anyhow", "async-trait", + "bincode", "bytes", "lib0", "parking_lot", @@ -747,7 +748,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" dependencies = [ "anyhow", "async-trait", @@ -777,7 +778,7 @@ dependencies = [ [[package]] name = "collab-derive" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" dependencies = [ "proc-macro2", "quote", @@ -789,7 +790,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" dependencies = [ "anyhow", "collab", @@ -809,7 +810,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" dependencies = [ "anyhow", "bytes", @@ -823,7 +824,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" dependencies = [ "anyhow", "chrono", @@ -865,7 +866,7 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" dependencies = [ "async-trait", "bincode", @@ -886,7 +887,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" dependencies = [ "anyhow", "async-trait", @@ -913,7 +914,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bab20052#bab200529ef0306194fa8618cc8708878b01ce04" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" dependencies = [ "anyhow", "collab", @@ -1272,7 +1273,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c739029d99517282d2ec1593523a47b51a85231b#c739029d99517282d2ec1593523a47b51a85231b" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7752ab68a91ff742fae595c1b45babdc17927fea#7752ab68a91ff742fae595c1b45babdc17927fea" dependencies = [ "anyhow", "app-error", @@ -2454,7 +2455,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c739029d99517282d2ec1593523a47b51a85231b#c739029d99517282d2ec1593523a47b51a85231b" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7752ab68a91ff742fae595c1b45babdc17927fea#7752ab68a91ff742fae595c1b45babdc17927fea" dependencies = [ "anyhow", "futures-util", @@ -2470,7 +2471,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c739029d99517282d2ec1593523a47b51a85231b#c739029d99517282d2ec1593523a47b51a85231b" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7752ab68a91ff742fae595c1b45babdc17927fea#7752ab68a91ff742fae595c1b45babdc17927fea" dependencies = [ "anyhow", "app-error", @@ -2831,7 +2832,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c739029d99517282d2ec1593523a47b51a85231b#c739029d99517282d2ec1593523a47b51a85231b" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7752ab68a91ff742fae595c1b45babdc17927fea#7752ab68a91ff742fae595c1b45babdc17927fea" dependencies = [ "anyhow", "reqwest", @@ -4271,7 +4272,7 @@ dependencies = [ [[package]] name = "realtime-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c739029d99517282d2ec1593523a47b51a85231b#c739029d99517282d2ec1593523a47b51a85231b" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7752ab68a91ff742fae595c1b45babdc17927fea#7752ab68a91ff742fae595c1b45babdc17927fea" dependencies = [ "anyhow", "bytes", @@ -4909,7 +4910,7 @@ dependencies = [ [[package]] name = "shared_entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c739029d99517282d2ec1593523a47b51a85231b#c739029d99517282d2ec1593523a47b51a85231b" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7752ab68a91ff742fae595c1b45babdc17927fea#7752ab68a91ff742fae595c1b45babdc17927fea" dependencies = [ "anyhow", "app-error", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index b7f1d825d686..ffcc51d1862a 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -82,7 +82,7 @@ incremental = false # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c739029d99517282d2ec1593523a47b51a85231b" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "7752ab68a91ff742fae595c1b45babdc17927fea" } # Please use the following script to update collab. # Working directory: frontend # @@ -92,11 +92,11 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c73 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bab20052" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bab20052" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bab20052" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bab20052" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bab20052" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bab20052" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bab20052" } -collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bab20052" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "faf17b54" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "faf17b54" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "faf17b54" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "faf17b54" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "faf17b54" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "faf17b54" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "faf17b54" } +collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "faf17b54" } diff --git a/frontend/rust-lib/flowy-document2/src/manager.rs b/frontend/rust-lib/flowy-document2/src/manager.rs index 8852ff50aebe..1a2a8782360b 100644 --- a/frontend/rust-lib/flowy-document2/src/manager.rs +++ b/frontend/rust-lib/flowy-document2/src/manager.rs @@ -126,7 +126,7 @@ impl DocumentManager { "can't find the document in the cloud, doc_id: {}", doc_id ); - default_document_collab_data(doc_id) + vec![default_document_collab_data(doc_id).doc_state.to_vec()] } else { return Err(err); } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs index e5ec9038b45e..1a794f212f8d 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs @@ -2,6 +2,7 @@ use anyhow::Error; use client_api::entity::QueryCollabResult::{Failed, Success}; use client_api::entity::{BatchQueryCollab, BatchQueryCollabParams, QueryCollabParams}; use client_api::error::ErrorCode::RecordNotFound; +use collab::core::collab_plugin::EncodedCollabV1; use collab_entity::CollabType; use tracing::error; @@ -34,7 +35,7 @@ where collab_type, }; match try_get_client?.get_collab(params).await { - Ok(data) => Ok(vec![data]), + Ok(data) => Ok(vec![data.doc_state.to_vec()]), Err(err) => { if err.code == RecordNotFound { Ok(vec![]) @@ -71,7 +72,15 @@ where .0 .into_iter() .flat_map(|(object_id, result)| match result { - Success { blob } => Some((object_id, vec![blob])), + Success { encode_collab_v1 } => { + match EncodedCollabV1::decode_from_bytes(&encode_collab_v1) { + Ok(encode) => Some((object_id, vec![encode.doc_state.to_vec()])), + Err(err) => { + error!("Failed to decode collab: {}", err); + None + }, + } + }, Failed { error } => { error!("Failed to get {} update: {}", object_id, error); None diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs index 2a55c945d9fa..3541b11b4bc1 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs @@ -33,7 +33,9 @@ where let data = try_get_client? .get_collab(params) .await - .map_err(FlowyError::from)?; + .map_err(FlowyError::from)? + .doc_state + .to_vec(); Ok(vec![data]) }) } @@ -61,11 +63,14 @@ where object_id: document_id.clone(), collab_type: CollabType::Document, }; - let updates = vec![try_get_client? + let doc_state = try_get_client? .get_collab(params) .await - .map_err(FlowyError::from)?]; - let document = Document::from_updates(CollabOrigin::Empty, updates, &document_id, vec![])?; + .map_err(FlowyError::from)? + .doc_state + .to_vec(); + let document = + Document::from_updates(CollabOrigin::Empty, vec![doc_state], &document_id, vec![])?; Ok(document.get_document_data().ok()) }) } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs index 71985ced42f6..f7268c0e532f 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs @@ -64,12 +64,19 @@ where workspace_id: workspace_id.clone(), collab_type: CollabType::Folder, }; - let updates = vec![try_get_client? + let doc_state = try_get_client? .get_collab(params) .await - .map_err(FlowyError::from)?]; - let folder = - Folder::from_collab_raw_data(uid, CollabOrigin::Empty, updates, &workspace_id, vec![])?; + .map_err(FlowyError::from)? + .doc_state + .to_vec(); + let folder = Folder::from_collab_raw_data( + uid, + CollabOrigin::Empty, + vec![doc_state], + &workspace_id, + vec![], + )?; Ok(folder.get_folder_data()) }) } @@ -91,11 +98,13 @@ where workspace_id, collab_type: CollabType::Folder, }; - let update = try_get_client? + let doc_state = try_get_client? .get_collab(params) .await - .map_err(FlowyError::from)?; - Ok(vec![update]) + .map_err(FlowyError::from)? + .doc_state + .to_vec(); + Ok(vec![doc_state]) }) } diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs index e38c992a960c..a76e980c4620 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs @@ -283,7 +283,7 @@ where let try_get_postgrest = self.server.try_get_weak_postgrest(); let (tx, rx) = channel(); - let init_update = empty_workspace_update(&collab_object); + let init_update = default_workspace_doc_state(&collab_object); af_spawn(async move { tx.send( async move { @@ -607,7 +607,7 @@ impl RealtimeEventHandler for RealtimeCollabUpdateHandler { } } -fn empty_workspace_update(collab_object: &CollabObject) -> Vec<u8> { +fn default_workspace_doc_state(collab_object: &CollabObject) -> Vec<u8> { let workspace_id = collab_object.object_id.clone(); let collab = Arc::new(MutexCollab::new( CollabOrigin::Empty, @@ -616,7 +616,7 @@ fn empty_workspace_update(collab_object: &CollabObject) -> Vec<u8> { )); let workspace = Workspace::new(workspace_id, "My workspace".to_string()); let folder = Folder::create(collab_object.uid, collab, None, FolderData::new(workspace)); - folder.encode_as_update_v1().0 + folder.encode_collab_v1().doc_state.to_vec() } fn oauth_params_from_box_any(any: BoxAny) -> Result<SupabaseOAuthParams, Error> { diff --git a/frontend/rust-lib/flowy-user/src/anon_user_upgrade/sync_new_user.rs b/frontend/rust-lib/flowy-user/src/anon_user_upgrade/sync_new_user.rs index 3afdab4c51c6..f3aee336101c 100644 --- a/frontend/rust-lib/flowy-user/src/anon_user_upgrade/sync_new_user.rs +++ b/frontend/rust-lib/flowy-user/src/anon_user_upgrade/sync_new_user.rs @@ -101,25 +101,26 @@ fn sync_views( match view.layout { ViewLayout::Document => { - let update = get_collab_init_update(uid, &collab_object, &collab_db)?; + let doc_state = get_collab_doc_state(uid, &collab_object, &collab_db)?; tracing::info!( "sync object: {} with update: {}", collab_object, - update.len() + doc_state.len() ); user_service - .create_collab_object(&collab_object, update) + .create_collab_object(&collab_object, doc_state) .await?; }, ViewLayout::Grid | ViewLayout::Board | ViewLayout::Calendar => { - let (database_update, row_ids) = get_database_init_update(uid, &collab_object, &collab_db)?; + let (database_doc_state, row_ids) = + get_database_doc_state(uid, &collab_object, &collab_db)?; tracing::info!( "sync object: {} with update: {}", collab_object, - database_update.len() + database_doc_state.len() ); user_service - .create_collab_object(&collab_object, database_update) + .create_collab_object(&collab_object, database_doc_state) .await?; // sync database's row @@ -134,16 +135,16 @@ fn sync_views( workspace_id.to_string(), device_id.clone(), ); - let database_row_update = - get_collab_init_update(uid, &database_row_collab_object, &collab_db)?; + let database_row_doc_state = + get_collab_doc_state(uid, &database_row_collab_object, &collab_db)?; tracing::info!( "sync object: {} with update: {}", database_row_collab_object, - database_row_update.len() + database_row_doc_state.len() ); let _ = user_service - .create_collab_object(&database_row_collab_object, database_row_update) + .create_collab_object(&database_row_collab_object, database_row_doc_state) .await; let database_row_document = CollabObject::new( @@ -154,16 +155,16 @@ fn sync_views( device_id.to_string(), ); // sync document in the row if exist - if let Ok(document_update) = - get_collab_init_update(uid, &database_row_document, &collab_db) + if let Ok(document_doc_state) = + get_collab_doc_state(uid, &database_row_document, &collab_db) { tracing::info!( "sync database row document: {} with update: {}", database_row_document, - document_update.len() + document_doc_state.len() ); let _ = user_service - .create_collab_object(&database_row_document, document_update) + .create_collab_object(&database_row_document, document_doc_state) .await; } } @@ -197,7 +198,7 @@ fn sync_views( }) } -fn get_collab_init_update( +fn get_collab_doc_state( uid: i64, collab_object: &CollabObject, collab_db: &Arc<RocksCollabDB>, @@ -208,15 +209,15 @@ fn get_collab_init_update( .read_txn() .load_doc_with_txn(uid, &collab_object.object_id, txn) })?; - let update = collab.encode_as_update_v1().0; - if update.is_empty() { + let doc_state = collab.encode_collab_v1().doc_state; + if doc_state.is_empty() { return Err(PersistenceError::UnexpectedEmptyUpdates); } - Ok(update) + Ok(doc_state.to_vec()) } -fn get_database_init_update( +fn get_database_doc_state( uid: i64, collab_object: &CollabObject, collab_db: &Arc<RocksCollabDB>, @@ -229,12 +230,12 @@ fn get_database_init_update( })?; let row_ids = get_database_row_ids(&collab).unwrap_or_default(); - let update = collab.encode_as_update_v1().0; - if update.is_empty() { + let doc_state = collab.encode_collab_v1().doc_state; + if doc_state.is_empty() { return Err(PersistenceError::UnexpectedEmptyUpdates); } - Ok((update, row_ids)) + Ok((doc_state.to_vec(), row_ids)) } async fn sync_folder( @@ -252,14 +253,14 @@ async fn sync_folder( .read_txn() .load_doc_with_txn(uid, workspace_id, txn) })?; - let update = collab.encode_as_update_v1().0; + let doc_state = collab.encode_collab_v1().doc_state; ( MutexFolder::new(Folder::open( uid, Arc::new(MutexCollab::from_collab(collab)), None, )?), - update, + doc_state, ) }; @@ -276,7 +277,7 @@ async fn sync_folder( update.len() ); if let Err(err) = user_service - .create_collab_object(&collab_object, update) + .create_collab_object(&collab_object, update.to_vec()) .await { tracing::error!("🔴sync folder failed: {:?}", err); @@ -313,14 +314,14 @@ async fn sync_database_views( .map(|_| { ( get_database_with_views(&collab), - collab.encode_as_update_v1().0, + collab.encode_collab_v1().doc_state, ) }) }; - if let Ok((records, update)) = result { + if let Ok((records, doc_state)) = result { let _ = user_service - .create_collab_object(&collab_object, update) + .create_collab_object(&collab_object, doc_state.to_vec()) .await; records.into_iter().map(Arc::new).collect() } else { diff --git a/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs b/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs index b317ff2c4e93..27d05869d890 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs @@ -41,8 +41,13 @@ impl UserDataMigration for HistoricalEmptyDocumentMigration { let collab = Arc::new(MutexCollab::new(origin.clone(), &view.id, vec![])); if let Ok(document) = Document::create_with_data(collab.clone(), document_data) { // Remove all old updates and then insert the new update - let (doc_state, sv) = document.get_collab().encode_as_update_v1(); - if let Err(err) = write_txn.flush_doc_with(session.user_id, &view.id, &doc_state, &sv) { + let encode = document.get_collab().encode_collab_v1(); + if let Err(err) = write_txn.flush_doc_with( + session.user_id, + &view.id, + &encode.doc_state, + &encode.state_vector, + ) { event!( tracing::Level::ERROR, "Failed to migrate document {}, error: {}", diff --git a/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs b/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs index bcd1aa87202a..8429e07b8520 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs @@ -37,9 +37,14 @@ impl UserDataMigration for FavoriteV1AndWorkspaceArrayMigration { folder.add_favorites(favorite_view_ids); } - let (doc_state, sv) = folder.encode_as_update_v1(); + let encode = folder.encode_collab_v1(); write_txn - .flush_doc_with(session.user_id, &session.user_workspace.id, &doc_state, &sv) + .flush_doc_with( + session.user_id, + &session.user_workspace.id, + &encode.doc_state, + &encode.state_vector, + ) .map_err(internal_error)?; } From 85af82acbe3bc65e4d5b6df024434a2d8a6ab427 Mon Sep 17 00:00:00 2001 From: Affif Mukhlashin <affif.bluemeda@gmail.com> Date: Mon, 6 Nov 2023 10:05:42 +0700 Subject: [PATCH 25/56] chore: update translations for Indonesian (#3864) Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io> --- frontend/resources/translations/ar-SA.json | 20 +-- frontend/resources/translations/ca-ES.json | 20 +-- frontend/resources/translations/de-DE.json | 22 +-- frontend/resources/translations/es-VE.json | 20 +-- frontend/resources/translations/eu-ES.json | 20 +-- frontend/resources/translations/fa.json | 20 +-- frontend/resources/translations/fr-CA.json | 20 +-- frontend/resources/translations/fr-FR.json | 20 +-- frontend/resources/translations/hu-HU.json | 20 +-- frontend/resources/translations/id-ID.json | 178 ++++++++++++++++++++- frontend/resources/translations/it-IT.json | 20 +-- frontend/resources/translations/ja-JP.json | 20 +-- frontend/resources/translations/ko-KR.json | 20 +-- frontend/resources/translations/pl-PL.json | 20 +-- frontend/resources/translations/pt-BR.json | 18 +-- frontend/resources/translations/pt-PT.json | 20 +-- frontend/resources/translations/ru-RU.json | 20 +-- frontend/resources/translations/sv.json | 20 +-- frontend/resources/translations/tr-TR.json | 20 +-- frontend/resources/translations/vi-VN.json | 4 +- frontend/resources/translations/zh-CN.json | 20 +-- frontend/resources/translations/zh-TW.json | 20 +-- 22 files changed, 374 insertions(+), 208 deletions(-) diff --git a/frontend/resources/translations/ar-SA.json b/frontend/resources/translations/ar-SA.json index ceb98e4fde45..0196f6176995 100644 --- a/frontend/resources/translations/ar-SA.json +++ b/frontend/resources/translations/ar-SA.json @@ -159,9 +159,7 @@ "editContact": "تحرير جهة الاتصال" }, "button": { - "OK": "نعم", - "Done": "منتهي", - "Cancel": "إلغاء", + "done": "منتهي", "signIn": "تسجيل الدخول", "signOut": "خروج", "complete": "مكتمل", @@ -177,8 +175,10 @@ "edit": "يحرر", "delete": "يمسح", "duplicate": "ينسخ", - "done": "منتهي", - "putback": "ضعها بالخلف" + "putback": "ضعها بالخلف", + "OK": "نعم", + "Done": "منتهي", + "Cancel": "إلغاء" }, "label": { "welcome": "مرحباً!", @@ -547,11 +547,11 @@ } }, "board": { + "menuName": "سبورة", + "referencedBoardPrefix": "نظرا ل", "column": { "create_new_card": "جديد" - }, - "menuName": "سبورة", - "referencedBoardPrefix": "نظرا ل" + } }, "calendar": { "menuName": "تقويم", @@ -568,9 +568,9 @@ "firstDayOfWeek": "اليوم الأول من الأسبوع", "layoutDateField": "تقويم التخطيط بواسطة", "noDateTitle": "بدون تاريخ", - "noDateHint": "ستظهر الأحداث غير المجدولة هنا", "clickToAdd": "انقر للإضافة إلى التقويم", - "name": "تخطيط التقويم" + "name": "تخطيط التقويم", + "noDateHint": "ستظهر الأحداث غير المجدولة هنا" }, "referencedCalendarPrefix": "نظرا ل" }, diff --git a/frontend/resources/translations/ca-ES.json b/frontend/resources/translations/ca-ES.json index 1b26bf233970..29be400e8f42 100644 --- a/frontend/resources/translations/ca-ES.json +++ b/frontend/resources/translations/ca-ES.json @@ -159,9 +159,7 @@ "editContact": "Editar un contacte" }, "button": { - "OK": "OK", - "Done": "Fet", - "Cancel": "Cancel·lar", + "done": "Fet", "signIn": "Iniciar sessió", "signOut": "Tancar sessió", "complete": "Completar", @@ -177,8 +175,10 @@ "edit": "Edita", "delete": "Suprimeix", "duplicate": "Duplicat", - "done": "Fet", - "putback": "Posar enrere" + "putback": "Posar enrere", + "OK": "OK", + "Done": "Fet", + "Cancel": "Cancel·lar" }, "label": { "welcome": "Benvingut!", @@ -546,11 +546,11 @@ } }, "board": { + "menuName": "Pissarra", + "referencedBoardPrefix": "Vista de", "column": { "create_new_card": "Nou" - }, - "menuName": "Pissarra", - "referencedBoardPrefix": "Vista de" + } }, "calendar": { "menuName": "Calendari", @@ -567,9 +567,9 @@ "firstDayOfWeek": "Comença la setmana", "layoutDateField": "Disseny del calendari per", "noDateTitle": "Sense data", - "noDateHint": "Els esdeveniments no programats es mostraran aquí", "clickToAdd": "Feu clic per afegir al calendari", - "name": "Disseny del calendari" + "name": "Disseny del calendari", + "noDateHint": "Els esdeveniments no programats es mostraran aquí" }, "referencedCalendarPrefix": "Vista de" }, diff --git a/frontend/resources/translations/de-DE.json b/frontend/resources/translations/de-DE.json index 4fe04a27368c..2a39ae0bad0e 100644 --- a/frontend/resources/translations/de-DE.json +++ b/frontend/resources/translations/de-DE.json @@ -189,9 +189,7 @@ "editContact": "Kontakt bearbeiten" }, "button": { - "OK": "OK", - "Done": "Erledigt", - "Cancel": "Abbrechen", + "done": "Erledigt", "signIn": "Anmelden", "signOut": "Abmelden", "complete": "Fertig", @@ -207,8 +205,10 @@ "edit": "Bearbeiten", "delete": "Löschen", "duplicate": "Duplikat", - "done": "Erledigt", - "putback": "Zurück geben" + "putback": "Zurück geben", + "OK": "OK", + "Done": "Erledigt", + "Cancel": "Abbrechen" }, "label": { "welcome": "Willkommen!", @@ -591,11 +591,11 @@ } }, "board": { + "menuName": "Planke", + "referencedBoardPrefix": "Sicht von", "column": { "create_new_card": "Neu" - }, - "menuName": "Planke", - "referencedBoardPrefix": "Sicht von" + } }, "calendar": { "menuName": "Kalender", @@ -612,9 +612,9 @@ "firstDayOfWeek": "Beginnen Sie die Woche am", "layoutDateField": "Layoutkalender von", "noDateTitle": "Kein Datum", - "noDateHint": "Außerplanmäßige Ereignisse werden hier angezeigt", "clickToAdd": "Klicken Sie, um es zum Kalender hinzuzufügen", - "name": "Kalenderlayout" + "name": "Kalenderlayout", + "noDateHint": "Außerplanmäßige Ereignisse werden hier angezeigt" }, "referencedCalendarPrefix": "Sicht von" }, @@ -640,4 +640,4 @@ "deleteContentTitle": "Möchten Sie den {pageType} wirklich löschen?", "deleteContentCaption": "Wenn Sie diesen {pageType} löschen, können Sie ihn aus dem Papierkorb wiederherstellen." } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/es-VE.json b/frontend/resources/translations/es-VE.json index 5620e986d154..33b7e1da8c08 100644 --- a/frontend/resources/translations/es-VE.json +++ b/frontend/resources/translations/es-VE.json @@ -159,9 +159,7 @@ "editContact": "Editar Contacto" }, "button": { - "OK": "OK", - "Done": "Hecho", - "Cancel": "Cancelar", + "done": "Hecho", "signIn": "Ingresar", "signOut": "Salir", "complete": "Completar", @@ -177,8 +175,10 @@ "edit": "Editar", "delete": "Borrar", "duplicate": "Duplicar", - "done": "Hecho", - "putback": "Volver" + "putback": "Volver", + "OK": "OK", + "Done": "Hecho", + "Cancel": "Cancelar" }, "label": { "welcome": "¡Bienvenido!", @@ -547,11 +547,11 @@ } }, "board": { + "menuName": "Junta", + "referencedBoardPrefix": "Vista de", "column": { "create_new_card": "Nuevo" - }, - "menuName": "Junta", - "referencedBoardPrefix": "Vista de" + } }, "calendar": { "menuName": "Calendario", @@ -568,9 +568,9 @@ "firstDayOfWeek": "Empieza la semana en", "layoutDateField": "Diseño de calendario por", "noDateTitle": "Sin cita", - "noDateHint": "Los eventos no programados se mostrarán aquí", "clickToAdd": "Haga clic para agregar al calendario", - "name": "Diseño de calendario" + "name": "Diseño de calendario", + "noDateHint": "Los eventos no programados se mostrarán aquí" }, "referencedCalendarPrefix": "Vista de" }, diff --git a/frontend/resources/translations/eu-ES.json b/frontend/resources/translations/eu-ES.json index 993d30062524..ac96d60104ad 100644 --- a/frontend/resources/translations/eu-ES.json +++ b/frontend/resources/translations/eu-ES.json @@ -159,9 +159,7 @@ "editContact": "Kontaktua editatu" }, "button": { - "OK": "OK", - "Done": "Eginda", - "Cancel": "Ezteztatu", + "done": "Eginda", "signIn": "Saioa hasi", "signOut": "Saioa itxi", "complete": "Burututa", @@ -177,8 +175,10 @@ "edit": "Editatu", "delete": "Ezabatu", "duplicate": "Bikoiztu", - "done": "Eginda", - "putback": "Jarri Atzera" + "putback": "Jarri Atzera", + "OK": "OK", + "Done": "Eginda", + "Cancel": "Ezteztatu" }, "label": { "welcome": "Ongi etorri!", @@ -552,11 +552,11 @@ } }, "board": { + "menuName": "Kontseilua", + "referencedBoardPrefix": "-ren ikuspegia", "column": { "create_new_card": "Berria" - }, - "menuName": "Kontseilua", - "referencedBoardPrefix": "-ren ikuspegia" + } }, "calendar": { "menuName": "Egutegia", @@ -573,9 +573,9 @@ "firstDayOfWeek": "Hasi astea", "layoutDateField": "Egutegiaren diseinua arabera", "noDateTitle": "Datarik ez", - "noDateHint": "Programatu gabeko gertaerak hemen agertuko dira", "clickToAdd": "Egin klik egutegian gehitzeko", - "name": "Egutegiaren diseinua" + "name": "Egutegiaren diseinua", + "noDateHint": "Programatu gabeko gertaerak hemen agertuko dira" }, "referencedCalendarPrefix": "-ren ikuspegia" }, diff --git a/frontend/resources/translations/fa.json b/frontend/resources/translations/fa.json index 6eebfc47689c..77e71f5e80f0 100644 --- a/frontend/resources/translations/fa.json +++ b/frontend/resources/translations/fa.json @@ -175,9 +175,7 @@ "editContact": "ویرایش مخاطب" }, "button": { - "OK": "باشه", - "Done": "انجام شد", - "Cancel": "لغو", + "done": "انجام شد", "signIn": "ورود", "signOut": "خروج", "complete": "کامل شد", @@ -193,8 +191,10 @@ "edit": "ویرایش", "delete": "حذف کردن", "duplicate": "تکرار کردن", - "done": "انجام شد", - "putback": "بازگشت" + "putback": "بازگشت", + "OK": "باشه", + "Done": "انجام شد", + "Cancel": "لغو" }, "label": { "welcome": "خوش آمدید!", @@ -593,11 +593,11 @@ } }, "board": { + "menuName": "بورد", + "referencedBoardPrefix": "نمای", "column": { "create_new_card": "ایجاد" - }, - "menuName": "بورد", - "referencedBoardPrefix": "نمای" + } }, "calendar": { "menuName": "تقویم", @@ -614,9 +614,9 @@ "firstDayOfWeek": "شروع هفته در", "layoutDateField": "طرح‌بندی تقویم با", "noDateTitle": "بدون تاریخ", - "noDateHint": "رویدادهای برنامه‌ریزی نشده در اینجا نشان داده می‌شوند", "clickToAdd": "برای افزودن به تقویم کلیک کنید", - "name": "طرح‌بندی تقویم" + "name": "طرح‌بندی تقویم", + "noDateHint": "رویدادهای برنامه‌ریزی نشده در اینجا نشان داده می‌شوند" }, "referencedCalendarPrefix": "نمای" }, diff --git a/frontend/resources/translations/fr-CA.json b/frontend/resources/translations/fr-CA.json index 5b9af705e108..08c1af4a87a4 100644 --- a/frontend/resources/translations/fr-CA.json +++ b/frontend/resources/translations/fr-CA.json @@ -159,9 +159,7 @@ "editContact": "Modifier le contact" }, "button": { - "OK": "OK", - "Done": "Fait", - "Cancel": "Annuler", + "done": "Fait", "signIn": "Se connecter", "signOut": "Se déconnecter", "complete": "Achevé", @@ -177,8 +175,10 @@ "edit": "Modifier", "delete": "Supprimer", "duplicate": "Dupliquer", - "done": "Fait", - "putback": "Remettre" + "putback": "Remettre", + "OK": "OK", + "Done": "Fait", + "Cancel": "Annuler" }, "label": { "welcome": "Bienvenue!", @@ -546,11 +546,11 @@ } }, "board": { + "menuName": "Conseil", + "referencedBoardPrefix": "Vue", "column": { "create_new_card": "Nouveau" - }, - "menuName": "Conseil", - "referencedBoardPrefix": "Vue" + } }, "calendar": { "menuName": "Calendrier", @@ -567,9 +567,9 @@ "firstDayOfWeek": "Commencer la semaine le", "layoutDateField": "Calendrier de mise en page par", "noDateTitle": "Pas de date", - "noDateHint": "Les événements non planifiés s'afficheront ici", "clickToAdd": "Cliquez pour ajouter au calendrier", - "name": "Disposition du calendrier" + "name": "Disposition du calendrier", + "noDateHint": "Les événements non planifiés s'afficheront ici" }, "referencedCalendarPrefix": "Vue" }, diff --git a/frontend/resources/translations/fr-FR.json b/frontend/resources/translations/fr-FR.json index cf77a54bd03e..298ec4393ef7 100644 --- a/frontend/resources/translations/fr-FR.json +++ b/frontend/resources/translations/fr-FR.json @@ -184,9 +184,7 @@ "editContact": "Modifier le contact" }, "button": { - "OK": "OK", - "Done": "Fait", - "Cancel": "Annuler", + "done": "Fait", "signIn": "Se connecter", "signOut": "Se déconnecter", "complete": "Achevé", @@ -202,12 +200,14 @@ "edit": "Modifier", "delete": "Supprimer", "duplicate": "Dupliquer", - "done": "Fait", "putback": "Remettre", "share": "Partager", "removeFromFavorites": "Retirer des favoris", "addToFavorites": "Ajouter aux favoris", - "rename": "Renommer" + "rename": "Renommer", + "OK": "OK", + "Done": "Fait", + "Cancel": "Annuler" }, "label": { "welcome": "Bienvenue !", @@ -630,11 +630,11 @@ } }, "board": { + "menuName": "Conseil", + "referencedBoardPrefix": "Vue", "column": { "create_new_card": "Nouveau" - }, - "menuName": "Conseil", - "referencedBoardPrefix": "Vue" + } }, "calendar": { "menuName": "Calendrier", @@ -651,9 +651,9 @@ "firstDayOfWeek": "Commencer la semaine le", "layoutDateField": "Calendrier de mise en page par", "noDateTitle": "Pas de date", - "noDateHint": "Les événements non planifiés s'afficheront ici", "clickToAdd": "Cliquez pour ajouter au calendrier", - "name": "Disposition du calendrier" + "name": "Disposition du calendrier", + "noDateHint": "Les événements non planifiés s'afficheront ici" }, "referencedCalendarPrefix": "Vue" }, diff --git a/frontend/resources/translations/hu-HU.json b/frontend/resources/translations/hu-HU.json index 6c897a580435..8f24a8e102a9 100644 --- a/frontend/resources/translations/hu-HU.json +++ b/frontend/resources/translations/hu-HU.json @@ -159,9 +159,7 @@ "editContact": "Kontakt Szerkesztése" }, "button": { - "OK": "OK", - "Done": "Kész", - "Cancel": "Mégse", + "done": "Kész", "signIn": "Bejelentkezés", "signOut": "Kijelentkezés", "complete": "Kész", @@ -177,8 +175,10 @@ "edit": "Szerkesztés", "delete": "Töröl", "duplicate": "Másolat", - "done": "Kész", - "putback": "Visszatesz" + "putback": "Visszatesz", + "OK": "OK", + "Done": "Kész", + "Cancel": "Mégse" }, "label": { "welcome": "Üdvözlünk!", @@ -546,11 +546,11 @@ } }, "board": { + "menuName": "Tábla", + "referencedBoardPrefix": "Nézet", "column": { "create_new_card": "Új" - }, - "menuName": "Tábla", - "referencedBoardPrefix": "Nézet" + } }, "calendar": { "menuName": "Naptár", @@ -567,9 +567,9 @@ "firstDayOfWeek": "Kezdje a hetet", "layoutDateField": "Elrendezés naptár által", "noDateTitle": "Nincs dátum", - "noDateHint": "A nem tervezett események itt jelennek meg", "clickToAdd": "Kattintson a naptárhoz való hozzáadáshoz", - "name": "Naptár elrendezés" + "name": "Naptár elrendezés", + "noDateHint": "A nem tervezett események itt jelennek meg" }, "referencedCalendarPrefix": "Nézet" }, diff --git a/frontend/resources/translations/id-ID.json b/frontend/resources/translations/id-ID.json index e47f2e7c753b..7c80d99407c9 100644 --- a/frontend/resources/translations/id-ID.json +++ b/frontend/resources/translations/id-ID.json @@ -13,6 +13,7 @@ "addAboveCmd": "Alt+klik", "addAboveMacCmd": "Opsi+klik", "addAboveTooltip": "untuk menambahkan di atas", + "dragTooltip": "Seret untuk pindahkan", "openMenuTooltip": "Klik untuk membuka menu" }, "signUp": { @@ -116,6 +117,11 @@ "confirmRestoreAll": { "title": "Apakah Anda yakin akan memulihkan semua laman di Sampah?", "caption": "Tindakan ini tidak bisa dibatalkan." + }, + "mobile": { + "actions": "Tindakan Sampah", + "empty": "Tempat Sampah Kosong", + "emptyDescription": "Anda tidak memiliki file yang dihapus" } }, "deletePagePrompt": { @@ -142,6 +148,7 @@ "defaultNewPageName": "Tanpa Judul", "renameDialog": "Ganti nama" }, + "noPagesInside": "Tidak ada halaman di dalamnya", "toolbar": { "undo": "Undo", "redo": "Redo", @@ -193,9 +200,9 @@ "editContact": "Ubah Kontak" }, "button": { - "OK": "Ya", - "Done": "Selesai", - "Cancel": "Batal", + "ok": "OKE", + "done": "Selesai", + "cancel": "Batal", "signIn": "Masuk", "signOut": "Keluar", "complete": "Selesai", @@ -211,8 +218,16 @@ "edit": "Sunting", "delete": "Menghapus", "duplicate": "Duplikat", - "done": "Selesai", - "putback": "Taruh kembali" + "putback": "Taruh kembali", + "update": "Perbarui", + "share": "Bagikan", + "removeFromFavorites": "Hapus dari favorit", + "addToFavorites": "Tambahkan ke Favorit", + "rename": "Ganti nama", + "helpCenter": "Pusat Bantuan", + "OK": "Ya", + "Done": "Selesai", + "Cancel": "Batal" }, "label": { "welcome": "Selamat datang!", @@ -374,6 +389,17 @@ "resetToDefault": "Mengatur ulang ke keybinding default", "couldNotLoadErrorMsg": "Tidak dapat memuat pintasan, Coba lagi", "couldNotSaveErrorMsg": "Tidak dapat menyimpan pintasan, Coba lagi" + }, + "mobile": { + "personalInfo": "Informasi pribadi", + "username": "Nama Pengguna", + "usernameEmptyError": "Nama pengguna tidak boleh kosong", + "about": "Tentang", + "pushNotifications": "Pemberitahuan Dorong", + "support": "Dukungan", + "joinDiscord": "Bergabunglah dengan kami di Discord", + "privacyPolicy": "Kebijakan Privasi", + "userAgreement": "Perjanjian Pengguna" } }, "grid": { @@ -531,7 +557,9 @@ "checklist": { "taskHint": "Deskripsi tugas", "addNew": "Tambahkan item", - "submitNewTask": "Buat" + "submitNewTask": "Buat", + "hideComplete": "Sembunyikan tugas yang sudah selesai", + "showComplete": "Tampilkan semua tugas" }, "menuName": "Grid", "referencedGridPrefix": "Pemandangan dari" @@ -724,9 +752,16 @@ }, "board": { "column": { + "createNewCard": "Baru", + "renameGroupTooltip": "Tekan untuk mengganti nama grup", "create_new_card": "Baru" }, "menuName": "Papan", + "showUngrouped": "Tampilkan item yang tidak dikelompokkan", + "ungroupedButtonText": "Tidak dikelompokkan", + "ungroupedButtonTooltip": "Berisi kartu yang tidak termasuk dalam grup mana pun", + "ungroupedItemsTitle": "Klik untuk menambahkan ke papan", + "groupBy": "Kelompokkan berdasarkan", "referencedBoardPrefix": "Pemandangan dari" }, "calendar": { @@ -817,6 +852,9 @@ "shortKeyword": "mengingatkan" } }, + "datePicker": { + "dateTimeFormatTooltip": "Ubah format tanggal dan waktu di pengaturan" + }, "relativeDates": { "yesterday": "Kemarin", "today": "Hari ini", @@ -825,10 +863,17 @@ }, "notificationHub": { "title": "Notifikasi", + "emptyTitle": "Semua sudah ketahuan!", + "emptyBody": "Tidak ada pemberitahuan atau tindakan yang tertunda. Nikmati ketenangannya.", "tabs": { "inbox": "Kotak masuk", "upcoming": "Mendatang" }, + "actions": { + "markAllRead": "tandai semua telah dibaca", + "showAll": "Semua", + "showUnreads": "Belum dibaca" + }, "filters": { "ascending": "Ascending", "descending": "Descending", @@ -854,5 +899,126 @@ "replaceAll": "Timpa semua", "noResult": "Tidak ada hasil", "caseSensitive": "Case sensitive" + }, + "error": { + "weAreSorry": "Kami meminta maaf", + "loadingViewError": "Kami mengalami masalah saat memuat tampilan ini. Silakan periksa koneksi internet Anda, segarkan aplikasi, dan jangan ragu untuk menghubungi tim jika masalah terus berlanjut." + }, + "editor": { + "bold": "Tebal", + "bulletedList": "Daftar Berpoin", + "checkbox": "Kotak centang", + "embedCode": "Sematkan Kode", + "heading1": "H1", + "heading2": "H2", + "heading3": "H3", + "highlight": "Sorotan", + "color": "Warna", + "image": "Gambar", + "italic": "Miring", + "link": "Tautan", + "numberedList": "Daftar Bernomor", + "quote": "Kutipan", + "strikethrough": "Dicoret", + "text": "Teks", + "underline": "Garis Bawah", + "fontColorDefault": "Bawaan", + "fontColorGray": "Abu-abu", + "fontColorBrown": "Cokelat", + "fontColorOrange": "Oranye", + "fontColorYellow": "Kuning", + "fontColorGreen": "Hijau", + "fontColorBlue": "Biru", + "fontColorPurple": "Ungu", + "fontColorPink": "Merah Jambu", + "fontColorRed": "Merah", + "backgroundColorDefault": "Latar belakang bawaan", + "backgroundColorGray": "Latar belakang abu-abu", + "backgroundColorBrown": "Latar belakang coklat", + "backgroundColorOrange": "Latar belakang oranye", + "backgroundColorYellow": "Latar belakang kuning", + "backgroundColorGreen": "Latar belakang hijau", + "backgroundColorBlue": "Latar belakang biru", + "backgroundColorPurple": "Latar belakang ungu", + "backgroundColorPink": "Latar belakang merah muda", + "backgroundColorRed": "Latar belakang merah", + "done": "Selesai", + "cancel": "Batalkan", + "tint1": "Warna 1", + "tint2": "Warna 2", + "tint3": "Warna 3", + "tint4": "Warna 4", + "tint5": "Warna 5", + "tint6": "Warna 6", + "tint7": "Warna 7", + "tint8": "Warna 8", + "tint9": "Warna 9", + "lightLightTint1": "Ungu", + "lightLightTint2": "Merah Jambu", + "lightLightTint3": "Merah Jambu Muda", + "lightLightTint4": "Oranye", + "lightLightTint5": "Kuning", + "lightLightTint6": "Hijau Jeruk Nipis", + "lightLightTint7": "Hijau", + "lightLightTint8": "Biru Air", + "lightLightTint9": "Biru", + "urlHint": "URL", + "mobileHeading1": "Judul 1", + "mobileHeading2": "Judul 2", + "mobileHeading3": "Judul 3", + "textColor": "Warna Teks", + "backgroundColor": "Warna Latar Belakang", + "addYourLink": "Tambahkan tautan Anda", + "openLink": "Buka tautan", + "copyLink": "Salin tautan", + "removeLink": "Hapus tautan", + "editLink": "Sunting tautan", + "linkText": "Teks", + "linkTextHint": "Silakan masukkan teks", + "linkAddressHint": "Silakan masukkan URL", + "highlightColor": "Sorot warna", + "clearHighlightColor": "Hapus warna sorotan", + "customColor": "Warna khusus", + "hexValue": "Nilai hex", + "opacity": "Kegelapan", + "resetToDefaultColor": "Atur ulang ke warna default", + "ltr": "LTR", + "rtl": "RTL", + "auto": "Otomatis", + "cut": "Potong", + "copy": "Salin", + "paste": "Tempel", + "find": "Temukan", + "previousMatch": "Padanan sebelumnya", + "nextMatch": "Padanan selanjutnya", + "closeFind": "Tutup", + "replace": "Ganti", + "replaceAll": "Ganti semua", + "regex": "Regex", + "caseSensitive": "Huruf besar kecil dibedakan", + "uploadImage": "Unggah Gambar", + "urlImage": "Gambar URL", + "incorrectLink": "Tautan Salah", + "upload": "Unggah", + "chooseImage": "Pilih gambar", + "loading": "Memuat", + "imageLoadFailed": "Tidak dapat memuat gambar", + "divider": "Pembagi", + "table": "Tabel", + "colAddBefore": "Tambahkan sebelumnya", + "rowAddBefore": "Tambahkan sebelumnya", + "colAddAfter": "Tambahkan setelahnya", + "rowAddAfter": "Tambahkan setelahnya", + "colRemove": "Hapus", + "rowRemove": "Hapus", + "colDuplicate": "Duplikat", + "rowDuplicate": "Duplikat", + "colClear": "Hapus Konten", + "rowClear": "Hapus Konten", + "slashPlaceHolder": "Masukkan / untuk menyisipkan blok, atau mulai mengetik" + }, + "favorite": { + "noFavorite": "Tidak ada halaman favorit", + "noFavoriteHintText": "Geser halaman ke kiri untuk menambahkannya ke favorit Anda" } } \ No newline at end of file diff --git a/frontend/resources/translations/it-IT.json b/frontend/resources/translations/it-IT.json index e7214164c835..d797240b17f4 100644 --- a/frontend/resources/translations/it-IT.json +++ b/frontend/resources/translations/it-IT.json @@ -160,9 +160,7 @@ "editContact": "Modifica Contatti" }, "button": { - "OK": "OK", - "Done": "Fatto", - "Cancel": "Annulla", + "done": "Fatto", "signIn": "Accedi", "signOut": "Esci", "complete": "Completa", @@ -178,8 +176,10 @@ "edit": "Modificare", "delete": "Eliminare", "duplicate": "Duplicare", - "done": "Fatto", - "putback": "Rimettere a posto" + "putback": "Rimettere a posto", + "OK": "OK", + "Done": "Fatto", + "Cancel": "Annulla" }, "label": { "welcome": "Benvenuto!", @@ -558,11 +558,11 @@ } }, "board": { + "menuName": "Asse", + "referencedBoardPrefix": "Vista di", "column": { "create_new_card": "Nuovo" - }, - "menuName": "Asse", - "referencedBoardPrefix": "Vista di" + } }, "calendar": { "menuName": "Calendario", @@ -579,9 +579,9 @@ "firstDayOfWeek": "Inizia la settimana", "layoutDateField": "Layout calendario per", "noDateTitle": "Nessuna data", - "noDateHint": "Gli eventi non programmati verranno visualizzati qui", "clickToAdd": "Fare clic per aggiungere al calendario", - "name": "Disposizione del calendario" + "name": "Disposizione del calendario", + "noDateHint": "Gli eventi non programmati verranno visualizzati qui" }, "referencedCalendarPrefix": "Vista di" }, diff --git a/frontend/resources/translations/ja-JP.json b/frontend/resources/translations/ja-JP.json index baba757890fb..9237dad5e33f 100644 --- a/frontend/resources/translations/ja-JP.json +++ b/frontend/resources/translations/ja-JP.json @@ -159,9 +159,7 @@ "editContact": "連絡先を編集する" }, "button": { - "OK": "OK", - "Done": "終わり", - "Cancel": "キャンセル", + "done": "終わり", "signIn": "サインイン", "signOut": "サインアウト", "complete": "完了", @@ -177,8 +175,10 @@ "edit": "編集", "delete": "消去", "duplicate": "複製", - "done": "終わり", - "putback": "戻す" + "putback": "戻す", + "OK": "OK", + "Done": "終わり", + "Cancel": "キャンセル" }, "label": { "welcome": "ようこそ!", @@ -547,11 +547,11 @@ } }, "board": { + "menuName": "ボード", + "referencedBoardPrefix": "のビュー", "column": { "create_new_card": "新しい" - }, - "menuName": "ボード", - "referencedBoardPrefix": "のビュー" + } }, "calendar": { "menuName": "カレンダー", @@ -568,9 +568,9 @@ "firstDayOfWeek": "週の開始日", "layoutDateField": "レイアウトカレンダー", "noDateTitle": "日付なし", - "noDateHint": "予定外のイベントがここに表示されます", "clickToAdd": "クリックしてカレンダーに追加します", - "name": "カレンダーのレイアウト" + "name": "カレンダーのレイアウト", + "noDateHint": "予定外のイベントがここに表示されます" }, "referencedCalendarPrefix": "のビュー" }, diff --git a/frontend/resources/translations/ko-KR.json b/frontend/resources/translations/ko-KR.json index 0d62c5de4020..98bd96640be4 100644 --- a/frontend/resources/translations/ko-KR.json +++ b/frontend/resources/translations/ko-KR.json @@ -159,9 +159,7 @@ "editContact": "연락처 편집" }, "button": { - "OK": "확인", - "Done": "완료", - "Cancel": "취소", + "done": "완료", "signIn": "로그인", "signOut": "로그아웃", "complete": "완료", @@ -177,8 +175,10 @@ "edit": "편집하다", "delete": "삭제", "duplicate": "복제하다", - "done": "완료", - "putback": "다시 집어 넣어" + "putback": "다시 집어 넣어", + "OK": "확인", + "Done": "완료", + "Cancel": "취소" }, "label": { "welcome": "환영합니다!", @@ -549,11 +549,11 @@ } }, "board": { + "menuName": "판자", + "referencedBoardPrefix": "관점", "column": { "create_new_card": "추가" - }, - "menuName": "판자", - "referencedBoardPrefix": "관점" + } }, "calendar": { "menuName": "달력", @@ -570,9 +570,9 @@ "firstDayOfWeek": "주 시작", "layoutDateField": "레이아웃 캘린더", "noDateTitle": "날짜 없음", - "noDateHint": "예약되지 않은 일정이 여기에 표시됩니다.", "clickToAdd": "캘린더에 추가하려면 클릭하세요.", - "name": "달력 레이아웃" + "name": "달력 레이아웃", + "noDateHint": "예약되지 않은 일정이 여기에 표시됩니다." }, "referencedCalendarPrefix": "관점" }, diff --git a/frontend/resources/translations/pl-PL.json b/frontend/resources/translations/pl-PL.json index 426c1287acbb..c0489cd08d4b 100644 --- a/frontend/resources/translations/pl-PL.json +++ b/frontend/resources/translations/pl-PL.json @@ -189,9 +189,7 @@ "editContact": "Edytuj Kontakt" }, "button": { - "OK": "OK", - "Done": "Zrobione", - "Cancel": "Anuluj", + "done": "Zrobione", "signIn": "Zaloguj", "signOut": "Wyloguj", "complete": "Zakończono", @@ -207,8 +205,10 @@ "edit": "Edytuj", "delete": "Usuń", "duplicate": "Duplikuj", - "done": "Zrobione", - "putback": "Odłóż z powrotem" + "putback": "Odłóż z powrotem", + "OK": "OK", + "Done": "Zrobione", + "Cancel": "Anuluj" }, "label": { "welcome": "Witaj!", @@ -656,11 +656,11 @@ } }, "board": { + "menuName": "Tablica", + "referencedBoardPrefix": "Widok", "column": { "create_new_card": "Nowy" - }, - "menuName": "Tablica", - "referencedBoardPrefix": "Widok" + } }, "calendar": { "menuName": "Kalendarz", @@ -678,9 +678,9 @@ "firstDayOfWeek": "Rozpocznij tydzień", "layoutDateField": "Układ kalendarza wg", "noDateTitle": "Brak daty", - "noDateHint": "Tutaj pojawią się nieplanowane wydarzenia", "clickToAdd": "Kliknij, aby dodać do kalendarza", - "name": "Układ kalendarza" + "name": "Układ kalendarza", + "noDateHint": "Tutaj pojawią się nieplanowane wydarzenia" }, "referencedCalendarPrefix": "Widok" }, diff --git a/frontend/resources/translations/pt-BR.json b/frontend/resources/translations/pt-BR.json index facd2ea5b5f9..9ec246a85e41 100644 --- a/frontend/resources/translations/pt-BR.json +++ b/frontend/resources/translations/pt-BR.json @@ -183,9 +183,7 @@ "editContact": "Editar um contato" }, "button": { - "OK": "Ok", - "Done": "Feito", - "Cancel": "Cancelar", + "done": "Feito", "signIn": "Conectar", "signOut": "Desconectar", "complete": "Completar", @@ -201,8 +199,10 @@ "edit": "Editar", "delete": "Excluir", "duplicate": "Duplicado", - "done": "Feito", "putback": "Por de volta", + "OK": "Ok", + "Done": "Feito", + "Cancel": "Cancelar", "tryAGain": "Tentar novamente" }, "label": { @@ -630,11 +630,11 @@ } }, "board": { + "menuName": "Quadro", + "referencedBoardPrefix": "Vista de", "column": { "create_new_card": "Novo" - }, - "menuName": "Quadro", - "referencedBoardPrefix": "Vista de" + } }, "calendar": { "menuName": "Calendário", @@ -651,9 +651,9 @@ "firstDayOfWeek": "Comece a semana em", "layoutDateField": "Calendário de layout por", "noDateTitle": "sem data", - "noDateHint": "Eventos não agendados aparecerão aqui", "clickToAdd": "Clique para adicionar ao calendário", - "name": "Layout do calendário" + "name": "Layout do calendário", + "noDateHint": "Eventos não agendados aparecerão aqui" }, "referencedCalendarPrefix": "Vista de" }, diff --git a/frontend/resources/translations/pt-PT.json b/frontend/resources/translations/pt-PT.json index 805109c9355f..7556474d2b6e 100644 --- a/frontend/resources/translations/pt-PT.json +++ b/frontend/resources/translations/pt-PT.json @@ -194,9 +194,7 @@ "editContact": "Editar um conctato" }, "button": { - "OK": "OK", - "Done": "Feito", - "Cancel": "Cancelar", + "done": "Feito", "signIn": "Entrar", "signOut": "Sair", "complete": "Completar", @@ -212,8 +210,10 @@ "edit": "Editar", "delete": "Excluir", "duplicate": "Duplicado", - "done": "Feito", - "putback": "Por de volta" + "putback": "Por de volta", + "OK": "OK", + "Done": "Feito", + "Cancel": "Cancelar" }, "label": { "welcome": "Bem vindo!", @@ -725,11 +725,11 @@ } }, "board": { + "menuName": "Quadro", + "referencedBoardPrefix": "Vista de", "column": { "create_new_card": "Novo" - }, - "menuName": "Quadro", - "referencedBoardPrefix": "Vista de" + } }, "calendar": { "menuName": "Calendário", @@ -747,9 +747,9 @@ "firstDayOfWeek": "Comece a semana em", "layoutDateField": "Calendário de layout por", "noDateTitle": "sem data", - "noDateHint": "Eventos não agendados aparecerão aqui", "clickToAdd": "Clique para adicionar ao calendário", - "name": "Layout do calendário" + "name": "Layout do calendário", + "noDateHint": "Eventos não agendados aparecerão aqui" }, "referencedCalendarPrefix": "Vista de" }, diff --git a/frontend/resources/translations/ru-RU.json b/frontend/resources/translations/ru-RU.json index 7afb27d4c20f..49d369ae56b8 100644 --- a/frontend/resources/translations/ru-RU.json +++ b/frontend/resources/translations/ru-RU.json @@ -158,9 +158,7 @@ "editContact": "Редактировать контакт" }, "button": { - "OK": "OK", - "Done": "Завершить", - "Cancel": "Отмена", + "done": "Сделанный", "signIn": "Войти", "signOut": "Выйти", "complete": "Завершить", @@ -176,8 +174,10 @@ "edit": "Редактировать", "delete": "Удалить", "duplicate": "Дублировать", - "done": "Сделанный", - "putback": "Вернуть" + "putback": "Вернуть", + "OK": "OK", + "Done": "Завершить", + "Cancel": "Отмена" }, "label": { "welcome": "Добро пожаловать!", @@ -546,11 +546,11 @@ } }, "board": { + "menuName": "Доска", + "referencedBoardPrefix": "Просмотр", "column": { "create_new_card": "Создать" - }, - "menuName": "Доска", - "referencedBoardPrefix": "Просмотр" + } }, "calendar": { "menuName": "Календарь", @@ -567,9 +567,9 @@ "firstDayOfWeek": "Первый день недели", "layoutDateField": "Вид календаря", "noDateTitle": "No Date", - "noDateHint": "Unscheduled events will show up here", "clickToAdd": "Click to add to the calendar", - "name": "Calendar layout" + "name": "Calendar layout", + "noDateHint": "Unscheduled events will show up here" }, "referencedCalendarPrefix": "Вид" }, diff --git a/frontend/resources/translations/sv.json b/frontend/resources/translations/sv.json index fd3f43c45684..600e076ae048 100644 --- a/frontend/resources/translations/sv.json +++ b/frontend/resources/translations/sv.json @@ -159,9 +159,7 @@ "editContact": "Redigera kontakt" }, "button": { - "OK": "OK", - "Done": "Gjort", - "Cancel": "Avbryt", + "done": "Gjort", "signIn": "Logga in", "signOut": "Logga ut", "complete": "Slutfört", @@ -177,8 +175,10 @@ "edit": "Redigera", "delete": "Radera", "duplicate": "Duplicera", - "done": "Gjort", - "putback": "Ställ tillbaka" + "putback": "Ställ tillbaka", + "OK": "OK", + "Done": "Gjort", + "Cancel": "Avbryt" }, "label": { "welcome": "Välkommen!", @@ -547,11 +547,11 @@ } }, "board": { + "menuName": "Tavla", + "referencedBoardPrefix": "Utsikt över", "column": { "create_new_card": "Nytt" - }, - "menuName": "Tavla", - "referencedBoardPrefix": "Utsikt över" + } }, "calendar": { "menuName": "Kalender", @@ -568,9 +568,9 @@ "firstDayOfWeek": "Börja veckan på", "layoutDateField": "Layoutkalender av", "noDateTitle": "Inget datum", - "noDateHint": "Icke schemalagda händelser kommer att dyka upp här", "clickToAdd": "Klicka för att lägga till i kalendern", - "name": "Kalenderlayout" + "name": "Kalenderlayout", + "noDateHint": "Icke schemalagda händelser kommer att dyka upp här" }, "referencedCalendarPrefix": "Utsikt över" }, diff --git a/frontend/resources/translations/tr-TR.json b/frontend/resources/translations/tr-TR.json index a6b5e8310550..9a147d3277cc 100644 --- a/frontend/resources/translations/tr-TR.json +++ b/frontend/resources/translations/tr-TR.json @@ -159,9 +159,7 @@ "editContact": "Kişiyi Düzenle" }, "button": { - "OK": "TAMAM", - "Done": "Tamamlamak", - "Cancel": "İptal", + "done": "Tamamlamak", "signIn": "Oturum Aç", "signOut": "Oturum Kapat", "complete": "Tamamlandı", @@ -177,8 +175,10 @@ "edit": "Düzenlemek", "delete": "Silmek", "duplicate": "Kopyalamak", - "done": "Tamamlamak", - "putback": "Geri koy" + "putback": "Geri koy", + "OK": "TAMAM", + "Done": "Tamamlamak", + "Cancel": "İptal" }, "label": { "welcome": "Merhaba!", @@ -546,11 +546,11 @@ } }, "board": { + "menuName": "Pano", + "referencedBoardPrefix": "görünümü", "column": { "create_new_card": "Yeni" - }, - "menuName": "Pano", - "referencedBoardPrefix": "görünümü" + } }, "calendar": { "menuName": "Takvim", @@ -567,9 +567,9 @@ "firstDayOfWeek": "hafta başla", "layoutDateField": "Yerleşim takvimi", "noDateTitle": "Tarih yok", - "noDateHint": "Planlanmamış etkinlikler burada gösterilir", "clickToAdd": "Takvime eklemek için tıklayın", - "name": "Takvim düzeni" + "name": "Takvim düzeni", + "noDateHint": "Planlanmamış etkinlikler burada gösterilir" }, "referencedCalendarPrefix": "görünümü" }, diff --git a/frontend/resources/translations/vi-VN.json b/frontend/resources/translations/vi-VN.json index 4710e36778ca..324d13de6e46 100644 --- a/frontend/resources/translations/vi-VN.json +++ b/frontend/resources/translations/vi-VN.json @@ -68,10 +68,10 @@ } }, "board": { + "menuName": "Bảng", "column": { "create_new_card": "Mới" - }, - "menuName": "Bảng" + } }, "calendar": { "menuName": "Lịch", diff --git a/frontend/resources/translations/zh-CN.json b/frontend/resources/translations/zh-CN.json index b5289f4c620a..8e223b72c33c 100644 --- a/frontend/resources/translations/zh-CN.json +++ b/frontend/resources/translations/zh-CN.json @@ -159,9 +159,7 @@ "editContact": "编辑联系人" }, "button": { - "OK": "确认", - "Done": "完成", - "Cancel": "取消", + "done": "完毕", "signIn": "登录", "signOut": "登出", "complete": "完成", @@ -177,8 +175,10 @@ "edit": "编辑", "delete": "删除", "duplicate": "复制", - "done": "完毕", - "putback": "放回去" + "putback": "放回去", + "OK": "确认", + "Done": "完成", + "Cancel": "取消" }, "label": { "welcome": "欢迎!", @@ -547,11 +547,11 @@ } }, "board": { + "menuName": "看板", + "referencedBoardPrefix": "视图", "column": { "create_new_card": "新建" - }, - "menuName": "看板", - "referencedBoardPrefix": "视图" + } }, "calendar": { "menuName": "日历", @@ -568,9 +568,9 @@ "firstDayOfWeek": "一周开始于", "layoutDateField": "以……为日历布局", "noDateTitle": "没有日期", - "noDateHint": "计划外事件将显示在此处", "clickToAdd": "单击以添加到日历", - "name": "日历布局" + "name": "日历布局", + "noDateHint": "计划外事件将显示在此处" }, "referencedCalendarPrefix": "视图" }, diff --git a/frontend/resources/translations/zh-TW.json b/frontend/resources/translations/zh-TW.json index dd8677419f82..dad5a435b789 100644 --- a/frontend/resources/translations/zh-TW.json +++ b/frontend/resources/translations/zh-TW.json @@ -159,9 +159,7 @@ "editContact": "編輯聯絡人" }, "button": { - "OK": "OK", - "Done": "完畢", - "Cancel": "取消", + "done": "完畢", "signIn": "登入", "signOut": "登出", "complete": "完成", @@ -177,8 +175,10 @@ "edit": "編輯", "delete": "刪除", "duplicate": "複製", - "done": "完畢", - "putback": "放回去" + "putback": "放回去", + "OK": "OK", + "Done": "完畢", + "Cancel": "取消" }, "label": { "welcome": "歡迎!", @@ -547,11 +547,11 @@ } }, "board": { + "menuName": "看板", + "referencedBoardPrefix": "視圖", "column": { "create_new_card": "建立" - }, - "menuName": "看板", - "referencedBoardPrefix": "視圖" + } }, "calendar": { "menuName": "月曆", @@ -568,9 +568,9 @@ "firstDayOfWeek": "一週的第一天", "layoutDateField": "排列方式", "noDateTitle": "未註明日期的", - "noDateHint": "未安排的事件將顯示在這裡", "clickToAdd": "點擊添加到日曆", - "name": "日曆佈局" + "name": "日曆佈局", + "noDateHint": "未安排的事件將顯示在這裡" }, "referencedCalendarPrefix": "視圖" }, From eb54cf99d467849f005b52acb3df8547524ac759 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 6 Nov 2023 13:03:32 +0800 Subject: [PATCH 26/56] fix: calendar event editor field name overflow (#3877) --- .../presentation/calendar_event_editor.dart | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_event_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_event_editor.dart index 8ccef33fdf48..12b568dfb46d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_event_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_event_editor.dart @@ -144,7 +144,7 @@ class EventPropertyList extends StatelessWidget { textStyle: Theme.of(context) .textTheme .bodyMedium - ?.copyWith(fontSize: 11), + ?.copyWith(fontSize: 11, overflow: TextOverflow.ellipsis), autofocus: true, useRoundedBorder: true, ), @@ -213,10 +213,13 @@ class _PropertyCellState extends State<PropertyCell> { size: const Size.square(14), ), const HSpace(4.0), - FlowyText.regular( - widget.cellContext.fieldInfo.name, - color: Theme.of(context).hintColor, - fontSize: 11, + Expanded( + child: FlowyText.regular( + widget.cellContext.fieldInfo.name, + color: Theme.of(context).hintColor, + overflow: TextOverflow.ellipsis, + fontSize: 11, + ), ), ], ), From 2b684ae7bf051b2ef087105f319eccc65c871312 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Mon, 6 Nov 2023 14:39:31 +0800 Subject: [PATCH 27/56] test: rename group test (#3790) * refactor: rename group * test: add rename group test * chore: add group operation interceptor * refactor: impl interceptor trait * chore: update type option when group change --------- Co-authored-by: Richard Shiue <71320345+richardshiue@users.noreply.github.com> --- .../tests/database/local_test/group_test.rs | 30 +++++++++++++++++++ .../tests/database/local_test/mod.rs | 1 + .../src/services/database/database_editor.rs | 2 ++ 3 files changed, 33 insertions(+) create mode 100644 frontend/rust-lib/event-integration/tests/database/local_test/group_test.rs diff --git a/frontend/rust-lib/event-integration/tests/database/local_test/group_test.rs b/frontend/rust-lib/event-integration/tests/database/local_test/group_test.rs new file mode 100644 index 000000000000..033d2cb81af8 --- /dev/null +++ b/frontend/rust-lib/event-integration/tests/database/local_test/group_test.rs @@ -0,0 +1,30 @@ +use event_integration::EventIntegrationTest; + +#[tokio::test] +async fn update_group_name_test() { + let test = EventIntegrationTest::new_with_guest_user().await; + let current_workspace = test.get_current_workspace().await.workspace; + let board_view = test + .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) + .await; + + let groups = test.get_groups(&board_view.id).await; + assert_eq!(groups.len(), 4); + assert_eq!(groups[1].group_name, "To Do"); + assert_eq!(groups[2].group_name, "Doing"); + + test + .update_group( + &board_view.id, + &groups[1].group_id, + &groups[1].field_id, + Some("To Do?".to_string()), + None, + ) + .await; + + let groups = test.get_groups(&board_view.id).await; + assert_eq!(groups.len(), 4); + assert_eq!(groups[1].group_name, "To Do?"); + assert_eq!(groups[2].group_name, "Doing"); +} diff --git a/frontend/rust-lib/event-integration/tests/database/local_test/mod.rs b/frontend/rust-lib/event-integration/tests/database/local_test/mod.rs index 585722915df8..8b91f8511371 100644 --- a/frontend/rust-lib/event-integration/tests/database/local_test/mod.rs +++ b/frontend/rust-lib/event-integration/tests/database/local_test/mod.rs @@ -1 +1,2 @@ +mod group_test; mod test; diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index 767542d407a0..9d4f1ab4bdf1 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -306,6 +306,8 @@ impl DatabaseEditor { Ok(()) } + /// Update the field type option data. + /// Do nothing if the [TypeOptionData] is empty. pub async fn update_field_type_option( &self, view_id: &str, From 4d82bb53223898e89d7579e72c0b00ca1283f530 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 6 Nov 2023 14:48:59 +0800 Subject: [PATCH 28/56] chore: move field width to field settings (#3830) * refactor: remove unnecessary builder * feat: add width to field settings * refactor: field settings logic * chore: oopsies * chore: implement UI * chore: remove GridFieldCellEquatable * test: rust-lib test fix --- .../field/field_action_sheet_bloc.dart | 16 +- .../application/field/field_cell_bloc.dart | 74 ++++----- .../application/field/field_service.dart | 12 -- .../field_settings_service.dart | 5 + .../board/application/board_bloc.dart | 25 --- .../toolbar/calendar_layout_setting.dart | 7 +- .../grid/application/grid_bloc.dart | 33 +--- .../grid/application/row/row_bloc.dart | 2 +- .../grid/presentation/grid_page.dart | 2 +- .../grid/presentation/layout/layout.dart | 2 +- .../widgets/header/field_cell.dart | 41 +++-- .../header/field_cell_action_sheet.dart | 66 ++++---- .../widgets/header/grid_header.dart | 21 +-- .../grid/presentation/widgets/row/row.dart | 2 +- .../setting/setting_property_list.dart | 7 +- .../lib/startup/deps_resolver.dart | 25 +-- .../test/bloc_test/board_test/util.dart | 6 - .../grid_test/field/field_cell_bloc_test.dart | 17 +-- .../grid_test/grid_header_bloc_test.dart | 3 +- .../test/bloc_test/grid_test/util.dart | 6 - .../src/entities/field_entities.rs | 7 - .../src/entities/field_settings_entities.rs | 9 ++ .../flowy-database2/src/event_handler.rs | 8 +- .../src/services/database/database_editor.rs | 142 ++++++++---------- .../src/services/database/util.rs | 4 +- .../src/services/database_view/view_editor.rs | 9 +- .../services/database_view/view_operation.rs | 3 +- .../src/services/field/field_builder.rs | 2 +- .../src/services/field_settings/entities.rs | 42 ++++-- .../field_settings/field_settings_builder.rs | 80 ++++------ .../src/services/share/csv/import.rs | 5 +- .../rust-lib/flowy-database2/src/template.rs | 11 +- .../database/field_settings_test/script.rs | 25 +-- .../database/field_settings_test/test.rs | 19 +-- .../database/mock_data/board_mock_data.rs | 5 +- .../database/mock_data/calendar_mock_data.rs | 5 +- .../database/mock_data/grid_mock_data.rs | 8 +- 37 files changed, 316 insertions(+), 440 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_action_sheet_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_action_sheet_bloc.dart index d72ced9fbe3b..929cbb9a3ce3 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_action_sheet_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_action_sheet_bloc.dart @@ -4,6 +4,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'field_info.dart'; import 'field_service.dart'; part 'field_action_sheet_bloc.freezed.dart'; @@ -14,17 +15,18 @@ class FieldActionSheetBloc final FieldBackendService fieldService; final FieldSettingsBackendService fieldSettingsService; - FieldActionSheetBloc({required FieldContext fieldCellContext}) - : fieldId = fieldCellContext.fieldInfo.id, + FieldActionSheetBloc({ + required String viewId, + required FieldInfo fieldInfo, + }) : fieldId = fieldInfo.id, fieldService = FieldBackendService( - viewId: fieldCellContext.viewId, - fieldId: fieldCellContext.fieldInfo.id, + viewId: viewId, + fieldId: fieldInfo.id, ), - fieldSettingsService = - FieldSettingsBackendService(viewId: fieldCellContext.viewId), + fieldSettingsService = FieldSettingsBackendService(viewId: viewId), super( FieldActionSheetState.initial( - TypeOptionPB.create()..field_2 = fieldCellContext.fieldInfo.field, + TypeOptionPB.create()..field_2 = fieldInfo.field, ), ) { on<FieldActionSheetEvent>( diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_cell_bloc.dart index b916177a55ce..807f683c5ac8 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_cell_bloc.dart @@ -1,77 +1,55 @@ import 'dart:math'; -import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:appflowy/plugins/database_view/application/field_settings/field_settings_service.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'dart:async'; -import 'field_listener.dart'; -import 'field_service.dart'; +import 'field_info.dart'; part 'field_cell_bloc.freezed.dart'; class FieldCellBloc extends Bloc<FieldCellEvent, FieldCellState> { - final SingleFieldListener _fieldListener; - final FieldBackendService _fieldBackendSvc; + FieldInfo fieldInfo; + final FieldSettingsBackendService _fieldSettingsService; - FieldCellBloc({ - required FieldContext fieldContext, - }) : _fieldListener = - SingleFieldListener(fieldId: fieldContext.fieldInfo.id), - _fieldBackendSvc = FieldBackendService( - viewId: fieldContext.viewId, - fieldId: fieldContext.fieldInfo.id, + FieldCellBloc({required String viewId, required this.fieldInfo}) + : _fieldSettingsService = FieldSettingsBackendService( + viewId: viewId, ), - super(FieldCellState.initial(fieldContext)) { + super(FieldCellState.initial(fieldInfo)) { on<FieldCellEvent>( (event, emit) async { event.when( - initial: () { - _startListening(); - }, - didReceiveFieldUpdate: (field) { - emit(state.copyWith(field: fieldContext.fieldInfo.field)); + onFieldChanged: (newFieldInfo) { + fieldInfo = newFieldInfo; + emit(FieldCellState.initial(newFieldInfo)); }, onResizeStart: () { - emit(state.copyWith(resizeStart: state.width)); + emit(state.copyWith(isResizing: true, resizeStart: state.width)); }, startUpdateWidth: (offset) { final width = max(offset + state.resizeStart, 50).toDouble(); emit(state.copyWith(width: width)); }, endUpdateWidth: () { - if (state.width != state.field.width.toDouble()) { - _fieldBackendSvc.updateField(width: state.width); + if (state.width != fieldInfo.fieldSettings?.width.toDouble()) { + _fieldSettingsService.updateFieldSettings( + fieldId: fieldInfo.id, + width: state.width, + ); } + emit(state.copyWith(isResizing: false, resizeStart: 0)); }, ); }, ); } - - @override - Future<void> close() async { - await _fieldListener.stop(); - return super.close(); - } - - void _startListening() { - _fieldListener.start( - onFieldChanged: (updatedField) { - if (isClosed) { - return; - } - add(FieldCellEvent.didReceiveFieldUpdate(updatedField)); - }, - ); - } } @freezed class FieldCellEvent with _$FieldCellEvent { - const factory FieldCellEvent.initial() = _InitialCell; - const factory FieldCellEvent.didReceiveFieldUpdate(FieldPB field) = - _DidReceiveFieldUpdate; + const factory FieldCellEvent.onFieldChanged(FieldInfo newFieldInfo) = + _OnFieldChanged; const factory FieldCellEvent.onResizeStart() = _OnResizeStart; const factory FieldCellEvent.startUpdateWidth(double offset) = _StartUpdateWidth; @@ -81,16 +59,16 @@ class FieldCellEvent with _$FieldCellEvent { @freezed class FieldCellState with _$FieldCellState { const factory FieldCellState({ - required String viewId, - required FieldPB field, + required FieldInfo fieldInfo, required double width, + required bool isResizing, required double resizeStart, }) = _FieldCellState; - factory FieldCellState.initial(FieldContext cellContext) => FieldCellState( - viewId: cellContext.viewId, - field: cellContext.fieldInfo.field, - width: cellContext.fieldInfo.field.width.toDouble(), + factory FieldCellState.initial(FieldInfo fieldInfo) => FieldCellState( + fieldInfo: fieldInfo, + isResizing: false, + width: fieldInfo.fieldSettings!.width.toDouble(), resizeStart: 0, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart index 6cf15e8b44c2..9d83019063ff 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart @@ -1,12 +1,8 @@ -import 'package:appflowy/plugins/database_view/application/field/field_info.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart'; import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'field_service.freezed.dart'; /// FieldService consists of lots of event functions. We define the events in the backend(Rust), /// you can find the corresponding event implementation in event_map.rs of the corresponding crate. @@ -104,11 +100,3 @@ class FieldBackendService { return DatabaseEventGetPrimaryField(payload).send(); } } - -@freezed -class FieldContext with _$FieldContext { - const factory FieldContext({ - required String viewId, - required FieldInfo fieldInfo, - }) = _FieldCellContext; -} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field_settings/field_settings_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field_settings/field_settings_service.dart index e699b61ad842..8c66949958d1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field_settings/field_settings_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field_settings/field_settings_service.dart @@ -58,6 +58,7 @@ class FieldSettingsBackendService { Future<Either<Unit, FlowyError>> updateFieldSettings({ required String fieldId, FieldVisibility? fieldVisibility, + double? width, }) { final FieldSettingsChangesetPB payload = FieldSettingsChangesetPB.create() ..viewId = viewId @@ -67,6 +68,10 @@ class FieldSettingsBackendService { payload.visibility = fieldVisibility; } + if (width != null) { + payload.width = width.round(); + } + return DatabaseEventUpdateFieldSettings(payload).send(); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart index 8c5d99c1264d..f9cfb22c02f8 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart @@ -6,9 +6,7 @@ import 'package:appflowy/plugins/database_view/application/field/field_info.dart import 'package:appflowy/plugins/database_view/application/group/group_service.dart'; import 'package:appflowy/plugins/database_view/application/row/row_service.dart'; import 'package:appflowy_board/appflowy_board.dart'; -import 'package:collection/collection.dart'; import 'package:dartz/dartz.dart'; -import 'package:equatable/equatable.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; @@ -400,29 +398,6 @@ class BoardState with _$BoardState { ); } -class GridFieldEquatable extends Equatable { - final UnmodifiableListView<FieldPB> _fields; - const GridFieldEquatable( - UnmodifiableListView<FieldPB> fields, - ) : _fields = fields; - - @override - List<Object?> get props { - if (_fields.isEmpty) { - return []; - } - - return [ - _fields.length, - _fields - .map((field) => field.width) - .reduce((value, element) => value + element), - ]; - } - - UnmodifiableListView<FieldPB> get value => UnmodifiableListView(_fields); -} - class GroupItem extends AppFlowyGroupItem { final RowMetaPB row; final FieldInfo fieldInfo; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart index 7b4a03142427..d9149b996654 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart @@ -4,7 +4,6 @@ import 'package:appflowy/plugins/database_view/application/field/field_controlle import 'package:appflowy/plugins/database_view/application/setting/property_bloc.dart'; import 'package:appflowy/plugins/database_view/calendar/application/calendar_setting_bloc.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; @@ -234,9 +233,9 @@ class LayoutDateField extends StatelessWidget { offset: const Offset(-14, 0), popupBuilder: (context) { return BlocProvider( - create: (context) => getIt<DatabasePropertyBloc>( - param1: viewId, - param2: fieldController, + create: (context) => DatabasePropertyBloc( + viewId: viewId, + fieldController: fieldController, )..add(const DatabasePropertyEvent.initial()), child: BlocBuilder<DatabasePropertyBloc, DatabasePropertyState>( builder: (context, state) { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart index d5fef3b3c7d3..f1b976a1cb04 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart @@ -6,13 +6,11 @@ import 'package:appflowy/plugins/database_view/application/row/row_service.dart' import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/filter_info.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart'; import 'package:dartz/dartz.dart'; -import 'package:equatable/equatable.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import '../../application/database_controller.dart'; -import 'dart:collection'; part 'grid_bloc.freezed.dart'; @@ -54,7 +52,7 @@ class GridBloc extends Bloc<GridEvent, GridState> { didReceiveFieldUpdate: (fields) { emit( state.copyWith( - fields: GridFieldEquatable(fields), + fields: FieldList(fields), ), ); }, @@ -175,7 +173,7 @@ class GridState with _$GridState { const factory GridState({ required String viewId, required Option<DatabasePB> grid, - required GridFieldEquatable fields, + required FieldList fields, required List<RowInfo> rowInfos, required int rowCount, required LoadingState loadingState, @@ -186,7 +184,7 @@ class GridState with _$GridState { }) = _GridState; factory GridState.initial(String viewId) => GridState( - fields: GridFieldEquatable(UnmodifiableListView([])), + fields: FieldList([]), rowInfos: [], rowCount: 0, grid: none(), @@ -199,26 +197,7 @@ class GridState with _$GridState { ); } -class GridFieldEquatable extends Equatable { - final List<FieldInfo> _fieldInfos; - const GridFieldEquatable( - List<FieldInfo> fieldInfos, - ) : _fieldInfos = fieldInfos; - - @override - List<Object?> get props { - if (_fieldInfos.isEmpty) { - return []; - } - - return [ - _fieldInfos.length, - _fieldInfos - .map((fieldInfo) => fieldInfo.field.width) - .reduce((value, element) => value + element), - ]; - } - - UnmodifiableListView<FieldInfo> get value => - UnmodifiableListView(_fieldInfos); +@freezed +class FieldList with _$FieldList { + factory FieldList(List<FieldInfo> fields) = _FieldList; } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_bloc.dart index 9842bd5f0c1e..cf436df89ff6 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_bloc.dart @@ -132,7 +132,7 @@ class GridCellEquatable extends Equatable { _fieldInfo.id, _fieldInfo.fieldType, _fieldInfo.field.visibility, - _fieldInfo.field.width, + _fieldInfo.fieldSettings?.width, ]; } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart index defba2c6bff4..99c4151bcc7f 100755 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart @@ -172,7 +172,7 @@ class _GridPageContentState extends State<GridPageContent> { return BlocBuilder<GridBloc, GridState>( buildWhen: (previous, current) => previous.fields != current.fields, builder: (context, state) { - final contentWidth = GridLayout.headerWidth(state.fields.value); + final contentWidth = GridLayout.headerWidth(state.fields.fields); return Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/layout/layout.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/layout/layout.dart index 29acb595d1c5..2b1882be1f9b 100755 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/layout/layout.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/layout/layout.dart @@ -6,7 +6,7 @@ class GridLayout { if (fields.isEmpty) return 0; final fieldsWidth = fields - .map((fieldInfo) => fieldInfo.field.width.toDouble()) + .map((fieldInfo) => fieldInfo.fieldSettings!.width.toDouble()) .reduce((value, element) => value + element); return fieldsWidth + diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell.dart index e3cacc6140d9..6d832235c0f4 100755 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/plugins/database_view/application/field/field_cell_bloc.dart'; -import 'package:appflowy/plugins/database_view/application/field/field_service.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_info.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra/theme_extension.dart'; @@ -15,31 +15,41 @@ import 'field_cell_action_sheet.dart'; import 'field_type_extension.dart'; class GridFieldCell extends StatefulWidget { - final FieldContext cellContext; + final String viewId; + final FieldInfo fieldInfo; const GridFieldCell({ - Key? key, - required this.cellContext, - }) : super(key: key); + super.key, + required this.viewId, + required this.fieldInfo, + }); @override State<GridFieldCell> createState() => _GridFieldCellState(); } class _GridFieldCellState extends State<GridFieldCell> { + late final FieldCellBloc _bloc; late PopoverController popoverController; @override void initState() { - popoverController = PopoverController(); super.initState(); + popoverController = PopoverController(); + _bloc = FieldCellBloc(viewId: widget.viewId, fieldInfo: widget.fieldInfo); + } + + @override + didUpdateWidget(covariant oldWidget) { + if (widget.fieldInfo != oldWidget.fieldInfo && !_bloc.isClosed) { + _bloc.add(FieldCellEvent.onFieldChanged(widget.fieldInfo)); + } + super.didUpdateWidget(oldWidget); } @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) { - return FieldCellBloc(fieldContext: widget.cellContext); - }, + return BlocProvider.value( + value: _bloc, child: BlocBuilder<FieldCellBloc, FieldCellState>( builder: (context, state) { final button = AppFlowyPopover( @@ -50,11 +60,12 @@ class _GridFieldCellState extends State<GridFieldCell> { controller: popoverController, popupBuilder: (BuildContext context) { return GridFieldCellActionSheet( - cellContext: widget.cellContext, + viewId: widget.viewId, + fieldInfo: widget.fieldInfo, ); }, child: FieldCellButton( - field: widget.cellContext.fieldInfo.field, + field: widget.fieldInfo.field, onTap: () => popoverController.show(), ), ); @@ -78,6 +89,12 @@ class _GridFieldCellState extends State<GridFieldCell> { ), ); } + + @override + Future<void> dispose() async { + super.dispose(); + await _bloc.close(); + } } class _GridHeaderCellContainer extends StatelessWidget { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell_action_sheet.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell_action_sheet.dart index 3f4de78de92e..199ac1e6e079 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell_action_sheet.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell_action_sheet.dart @@ -1,8 +1,8 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/plugins/database_view/application/field/field_action_sheet_bloc.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_info.dart'; import 'package:appflowy/plugins/database_view/application/field/field_service.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; -import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; @@ -20,9 +20,13 @@ import '../../layout/sizes.dart'; import 'field_editor.dart'; class GridFieldCellActionSheet extends StatefulWidget { - final FieldContext cellContext; - const GridFieldCellActionSheet({required this.cellContext, Key? key}) - : super(key: key); + final String viewId; + final FieldInfo fieldInfo; + const GridFieldCellActionSheet({ + required this.viewId, + required this.fieldInfo, + Key? key, + }) : super(key: key); @override State<StatefulWidget> createState() => _GridFieldCellActionSheetState(); @@ -37,31 +41,35 @@ class _GridFieldCellActionSheetState extends State<GridFieldCellActionSheet> { return SizedBox( width: 400, child: FieldEditor( - viewId: widget.cellContext.viewId, - fieldInfo: widget.cellContext.fieldInfo, + viewId: widget.viewId, + fieldInfo: widget.fieldInfo, typeOptionLoader: FieldTypeOptionLoader( - viewId: widget.cellContext.viewId, - field: widget.cellContext.fieldInfo.field, + viewId: widget.viewId, + field: widget.fieldInfo.field, ), ), ); } return BlocProvider( - create: (context) => - getIt<FieldActionSheetBloc>(param1: widget.cellContext), + create: (context) => FieldActionSheetBloc( + viewId: widget.viewId, + fieldInfo: widget.fieldInfo, + ), child: IntrinsicWidth( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ _EditFieldButton( - cellContext: widget.cellContext, onTap: () { setState(() => _showFieldEditor = true); }, ), VSpace(GridSize.typeOptionSeparatorHeight), - _FieldOperationList(widget.cellContext), + _FieldOperationList( + viewId: widget.viewId, + fieldInfo: widget.fieldInfo, + ), ], ), ), @@ -70,10 +78,8 @@ class _GridFieldCellActionSheetState extends State<GridFieldCellActionSheet> { } class _EditFieldButton extends StatelessWidget { - final FieldContext cellContext; final void Function()? onTap; - const _EditFieldButton({required this.cellContext, Key? key, this.onTap}) - : super(key: key); + const _EditFieldButton({Key? key, this.onTap}) : super(key: key); @override Widget build(BuildContext context) { @@ -96,8 +102,13 @@ class _EditFieldButton extends StatelessWidget { } class _FieldOperationList extends StatelessWidget { - final FieldContext fieldContext; - const _FieldOperationList(this.fieldContext, {Key? key}) : super(key: key); + final String viewId; + final FieldInfo fieldInfo; + const _FieldOperationList({ + required this.viewId, + required this.fieldInfo, + Key? key, + }) : super(key: key); @override Widget build(BuildContext context) { @@ -128,7 +139,7 @@ class _FieldOperationList extends StatelessWidget { bool enable = true; // If the field is primary, delete and duplicate are disabled. - if (fieldContext.fieldInfo.isPrimary) { + if (fieldInfo.isPrimary) { switch (action) { case FieldAction.hide: break; @@ -145,7 +156,8 @@ class _FieldOperationList extends StatelessWidget { child: SizedBox( height: GridSize.popoverItemHeight, child: FieldActionCell( - fieldInfo: fieldContext, + viewId: viewId, + fieldInfo: fieldInfo, action: action, enable: enable, ), @@ -155,11 +167,13 @@ class _FieldOperationList extends StatelessWidget { } class FieldActionCell extends StatelessWidget { - final FieldContext fieldInfo; + final String viewId; + final FieldInfo fieldInfo; final FieldAction action; final bool enable; const FieldActionCell({ + required this.viewId, required this.fieldInfo, required this.action, required this.enable, @@ -177,7 +191,7 @@ class FieldActionCell extends StatelessWidget { ? AFThemeExtension.of(context).textColor : Theme.of(context).disabledColor, ), - onTap: () => action.run(context, fieldInfo), + onTap: () => action.run(context, viewId, fieldInfo), leftIcon: FlowySvg( action.icon(), color: enable @@ -217,7 +231,7 @@ extension _FieldActionExtension on FieldAction { } } - void run(BuildContext context, FieldContext fieldContext) { + void run(BuildContext context, String viewId, FieldInfo fieldInfo) { switch (this) { case FieldAction.hide: context @@ -228,8 +242,8 @@ extension _FieldActionExtension on FieldAction { PopoverContainer.of(context).close(); FieldBackendService( - viewId: fieldContext.viewId, - fieldId: fieldContext.fieldInfo.id, + viewId: viewId, + fieldId: fieldInfo.id, ).duplicateField(); break; @@ -240,8 +254,8 @@ extension _FieldActionExtension on FieldAction { title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(), confirm: () { FieldBackendService( - viewId: fieldContext.viewId, - fieldId: fieldContext.fieldInfo.field.id, + viewId: viewId, + fieldId: fieldInfo.field.id, ).deleteField(); }, ).show(context); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/grid_header.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/grid_header.dart index 71f9f1375fa1..022991015a8c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/grid_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/grid_header.dart @@ -1,10 +1,8 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; -import 'package:appflowy/plugins/database_view/application/field/field_service.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; import 'package:appflowy/plugins/database_view/grid/application/grid_header_bloc.dart'; -import 'package:appflowy/startup/startup.dart'; import 'package:appflowy_backend/log.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; @@ -42,9 +40,9 @@ class _GridHeaderSliverAdaptorState extends State<GridHeaderSliverAdaptor> { Widget build(BuildContext context) { return BlocProvider( create: (context) { - return getIt<GridHeaderBloc>( - param1: widget.viewId, - param2: widget.fieldController, + return GridHeaderBloc( + viewId: widget.viewId, + fieldController: widget.fieldController, )..add(const GridHeaderEvent.initial()); }, child: BlocBuilder<GridHeaderBloc, GridHeaderState>( @@ -96,15 +94,10 @@ class _GridHeaderState extends State<_GridHeader> { builder: (context, state) { final cells = state.fields .map( - (field) => FieldContext( + (fieldInfo) => GridFieldCell( + key: _getKeyById(fieldInfo.id), viewId: widget.viewId, - fieldInfo: field, - ), - ) - .map( - (ctx) => GridFieldCell( - key: _getKeyById(ctx.fieldInfo.id), - cellContext: ctx, + fieldInfo: fieldInfo, ), ) .toList(); @@ -136,7 +129,7 @@ class _GridHeaderState extends State<_GridHeader> { int newIndex, ) { if (cells.length > oldIndex) { - final field = cells[oldIndex].cellContext.fieldInfo.field; + final field = cells[oldIndex].fieldInfo.field; context .read<GridHeaderBloc>() .add(GridHeaderEvent.moveField(field, oldIndex, newIndex)); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/row.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/row.dart index f1f0976e8486..96bf4baf6475 100755 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/row.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/row.dart @@ -276,7 +276,7 @@ class RowContent extends StatelessWidget { final GridCellWidget child = builder.build(cellId); return CellContainer( - width: cellId.fieldInfo.field.width.toDouble(), + width: cellId.fieldInfo.fieldSettings?.width.toDouble() ?? 140, isPrimary: cellId.fieldInfo.field.isPrimary, cellContainerNotifier: CellContainerNotifier(child), accessoryBuilder: (buildContext) { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/setting_property_list.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/setting_property_list.dart index 53e43ea4dc80..f2fb63970e75 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/setting_property_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/setting_property_list.dart @@ -6,7 +6,6 @@ import 'package:appflowy/plugins/database_view/application/field/field_info.dart import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; import 'package:appflowy/plugins/database_view/application/setting/property_bloc.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart'; -import 'package:appflowy/startup/startup.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:collection/collection.dart'; @@ -40,9 +39,9 @@ class _DatabasePropertyListState extends State<DatabasePropertyList> { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => getIt<DatabasePropertyBloc>( - param1: widget.viewId, - param2: widget.fieldController, + create: (context) => DatabasePropertyBloc( + viewId: widget.viewId, + fieldController: widget.fieldController, )..add(const DatabasePropertyEvent.initial()), child: BlocBuilder<DatabasePropertyBloc, DatabasePropertyState>( builder: (context, state) { diff --git a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart index a14ada4100a3..bc66aaf878e3 100644 --- a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart +++ b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart @@ -1,11 +1,6 @@ import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/network_monitor.dart'; import 'package:appflowy/env/env.dart'; -import 'package:appflowy/plugins/database_view/application/field/field_action_sheet_bloc.dart'; -import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; -import 'package:appflowy/plugins/database_view/application/field/field_service.dart'; -import 'package:appflowy/plugins/database_view/application/setting/property_bloc.dart'; -import 'package:appflowy/plugins/database_view/grid/application/grid_header_bloc.dart'; import 'package:appflowy/plugins/document/application/prelude.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart'; @@ -48,7 +43,7 @@ class DependencyResolver { _resolveHomeDeps(getIt); _resolveFolderDeps(getIt); _resolveDocDeps(getIt); - _resolveGridDeps(getIt); + // _resolveGridDeps(getIt); _resolveCommonService(getIt, mode); } } @@ -218,21 +213,3 @@ void _resolveDocDeps(GetIt getIt) { (view, _) => DocumentBloc(view: view), ); } - -void _resolveGridDeps(GetIt getIt) { - getIt.registerFactoryParam<GridHeaderBloc, String, FieldController>( - (viewId, fieldController) => GridHeaderBloc( - viewId: viewId, - fieldController: fieldController, - ), - ); - - getIt.registerFactoryParam<FieldActionSheetBloc, FieldContext, void>( - (data, _) => FieldActionSheetBloc(fieldCellContext: data), - ); - - getIt.registerFactoryParam<DatabasePropertyBloc, String, FieldController>( - (viewId, cache) => - DatabasePropertyBloc(viewId: viewId, fieldController: cache), - ); -} diff --git a/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart b/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart index d9c11130fea5..0e4b92b9e85b 100644 --- a/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart +++ b/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart @@ -3,7 +3,6 @@ import 'package:appflowy/plugins/database_view/application/cell/cell_controller_ import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart'; import 'package:appflowy/plugins/database_view/application/field/field_info.dart'; -import 'package:appflowy/plugins/database_view/application/field/field_service.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; import 'package:appflowy/plugins/database_view/application/row/row_controller.dart'; @@ -141,11 +140,6 @@ class BoardTestContext { return fieldInfo; } - FieldContext singleSelectFieldCellContext() { - final fieldInfo = singleSelectFieldContext(); - return FieldContext(viewId: gridView.id, fieldInfo: fieldInfo); - } - FieldInfo textFieldContext() { final fieldInfo = fieldContexts .firstWhere((element) => element.fieldType == FieldType.RichText); diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/field/field_cell_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/field/field_cell_bloc_test.dart index e36b9587c825..3aa656f39392 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/field/field_cell_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/field/field_cell_bloc_test.dart @@ -1,5 +1,4 @@ import 'package:appflowy/plugins/database_view/application/field/field_cell_bloc.dart'; -import 'package:appflowy/plugins/database_view/application/field/field_service.dart'; import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -23,11 +22,9 @@ void main() { blocTest( 'update field width', build: () => FieldCellBloc( - fieldContext: FieldContext( - fieldInfo: context.fieldContexts[0], - viewId: context.gridView.id, - ), - )..add(const FieldCellEvent.initial()), + fieldInfo: context.fieldContexts[0], + viewId: context.gridView.id, + ), act: (bloc) { width = bloc.state.width; bloc.add(const FieldCellEvent.onResizeStart()); @@ -42,11 +39,9 @@ void main() { blocTest( 'field width should not be lesser than 50px', build: () => FieldCellBloc( - fieldContext: FieldContext( - fieldInfo: context.fieldContexts[0], - viewId: context.gridView.id, - ), - )..add(const FieldCellEvent.initial()), + viewId: context.gridView.id, + fieldInfo: context.fieldContexts[0], + ), act: (bloc) { bloc.add(const FieldCellEvent.onResizeStart()); bloc.add(const FieldCellEvent.startUpdateWidth(-110)); diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/grid_header_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/grid_header_bloc_test.dart index 571cb11d894a..29bb34d7b648 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/grid_header_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/grid_header_bloc_test.dart @@ -18,7 +18,8 @@ void main() { setUp(() async { context = await gridTest.createTestGrid(); actionSheetBloc = FieldActionSheetBloc( - fieldCellContext: context.singleSelectFieldCellContext(), + viewId: context.gridView.id, + fieldInfo: context.singleSelectFieldContext(), ); }); diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart index 52a981fcb6ea..7df94f39440c 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart @@ -3,7 +3,6 @@ import 'package:appflowy/plugins/database_view/application/cell/cell_controller_ import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart'; import 'package:appflowy/plugins/database_view/application/field/field_info.dart'; -import 'package:appflowy/plugins/database_view/application/field/field_service.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart'; import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; @@ -88,11 +87,6 @@ class GridTestContext { return fieldInfo; } - FieldContext singleSelectFieldCellContext() { - final fieldInfo = singleSelectFieldContext(); - return FieldContext(viewId: gridView.id, fieldInfo: fieldInfo); - } - FieldInfo textFieldContext() { final fieldInfo = fieldContexts .firstWhere((element) => element.fieldType == FieldType.RichText); diff --git a/frontend/rust-lib/flowy-database2/src/entities/field_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/field_entities.rs index 5d25b290f192..d3a0e7c09604 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/field_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/field_entities.rs @@ -521,13 +521,6 @@ impl FieldType { self.clone().into() } - pub fn default_cell_width(&self) -> i32 { - match self { - FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => 180, - _ => 150, - } - } - pub fn default_name(&self) -> String { let s = match self { FieldType::RichText => "Text", diff --git a/frontend/rust-lib/flowy-database2/src/entities/field_settings_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/field_settings_entities.rs index d7b828c44432..a99560795252 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/field_settings_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/field_settings_entities.rs @@ -15,6 +15,9 @@ pub struct FieldSettingsPB { #[pb(index = 2)] pub visibility: FieldVisibility, + + #[pb(index = 3)] + pub width: i32, } impl From<FieldSettings> for FieldSettingsPB { @@ -22,6 +25,7 @@ impl From<FieldSettings> for FieldSettingsPB { Self { field_id: value.field_id, visibility: value.visibility, + width: value.width, } } } @@ -99,6 +103,9 @@ pub struct FieldSettingsChangesetPB { #[pb(index = 3, one_of)] pub visibility: Option<FieldVisibility>, + + #[pb(index = 4, one_of)] + pub width: Option<i32>, } impl From<FieldSettingsChangesetParams> for FieldSettingsChangesetPB { @@ -107,6 +114,7 @@ impl From<FieldSettingsChangesetParams> for FieldSettingsChangesetPB { view_id: value.view_id, field_id: value.field_id, visibility: value.visibility, + width: value.width, } } } @@ -119,6 +127,7 @@ impl TryFrom<FieldSettingsChangesetPB> for FieldSettingsChangesetParams { view_id: value.view_id, field_id: value.field_id, visibility: value.visibility, + width: value.width, }) } } diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index b680364f1e2c..c49c78333945 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -892,10 +892,8 @@ pub(crate) async fn get_field_settings_handler( let (view_id, field_ids) = data.into_inner().try_into()?; let database_editor = manager.get_database_with_view_id(&view_id).await?; - let layout_ty = database_editor.get_layout_type(view_id.as_ref()).await; - let field_settings = database_editor - .get_field_settings(&view_id, layout_ty, field_ids.clone()) + .get_field_settings(&view_id, field_ids.clone()) .await? .into_iter() .map(FieldSettingsPB::from) @@ -915,10 +913,8 @@ pub(crate) async fn get_all_field_settings_handler( let view_id = data.into_inner(); let database_editor = manager.get_database_with_view_id(view_id.as_ref()).await?; - let layout_ty = database_editor.get_layout_type(view_id.as_ref()).await; - let field_settings = database_editor - .get_all_field_settings(view_id.as_ref(), layout_ty) + .get_all_field_settings(view_id.as_ref()) .await? .into_iter() .map(FieldSettingsPB::from) diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index 9d4f1ab4bdf1..ed2851b6fa72 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -1090,57 +1090,27 @@ impl DatabaseEditor { pub async fn get_field_settings( &self, view_id: &str, - layout_ty: DatabaseLayout, field_ids: Vec<String>, ) -> FlowyResult<Vec<FieldSettings>> { let view = self.database_views.get_view_editor(view_id).await?; - let default_field_settings = default_field_settings_by_layout_map() - .get(&layout_ty) - .unwrap() - .to_owned(); - let found_field_settings = view.v_get_field_settings(&field_ids).await; - - let field_settings = field_ids - .into_iter() - .map(|field_id| { - if let Some(field_settings) = found_field_settings.get(&field_id) { - field_settings.to_owned() - } else { - FieldSettings::try_from_anymap(field_id, default_field_settings.clone()).unwrap() - } - }) + let field_settings = view + .v_get_field_settings(&field_ids) + .await + .into_values() .collect(); Ok(field_settings) } - pub async fn get_all_field_settings( - &self, - view_id: &str, - layout_ty: DatabaseLayout, - ) -> FlowyResult<Vec<FieldSettings>> { - let view = self.database_views.get_view_editor(view_id).await?; - let default_field_settings = default_field_settings_by_layout_map() - .get(&layout_ty) - .unwrap() - .to_owned(); - let fields = self.get_fields(view_id, None); - - let found_field_settings = view.v_get_all_field_settings().await; - - let field_settings = fields - .into_iter() - .map(|field| { - if let Some(field_settings) = found_field_settings.get(&field.id) { - field_settings.to_owned() - } else { - FieldSettings::try_from_anymap(field.id, default_field_settings.clone()).unwrap() - } - }) + pub async fn get_all_field_settings(&self, view_id: &str) -> FlowyResult<Vec<FieldSettings>> { + let field_ids = self + .get_fields(view_id, None) + .iter() + .map(|field| field.id.clone()) .collect(); - Ok(field_settings) + self.get_field_settings(view_id, field_ids).await } pub async fn update_field_settings_with_changeset( @@ -1149,7 +1119,12 @@ impl DatabaseEditor { ) -> FlowyResult<()> { let view = self.database_views.get_view_editor(¶ms.view_id).await?; view - .v_update_field_settings(¶ms.view_id, ¶ms.field_id, params.visibility) + .v_update_field_settings( + ¶ms.view_id, + ¶ms.field_id, + params.visibility, + params.width, + ) .await?; Ok(()) @@ -1417,38 +1392,37 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl { view_id: &str, field_ids: &[String], ) -> HashMap<String, FieldSettings> { - let field_settings_map = self - .database - .lock() - .get_field_settings(view_id, Some(field_ids)); - - field_settings_map - .into_iter() - .filter_map(|(field_id, field_settings)| { - let field_settings = FieldSettings::try_from_anymap(field_id.clone(), field_settings); - if let Ok(settings) = field_settings { - Some((field_id, settings)) - } else { - None - } - }) - .collect() - } + let (layout_type, field_settings_map) = { + let database = self.database.lock(); + let layout_type = database.views.get_database_view_layout(view_id); + let field_settings_map = database.get_field_settings(view_id, Some(field_ids)); + (layout_type, field_settings_map) + }; - fn get_all_field_settings(&self, view_id: &str) -> HashMap<String, FieldSettings> { - let field_settings_map = self.database.lock().get_field_settings(view_id, None); + let default_field_settings = default_field_settings_by_layout_map() + .get(&layout_type) + .unwrap() + .to_owned(); - field_settings_map - .into_iter() - .filter_map(|(field_id, field_settings)| { - let field_settings = FieldSettings::try_from_anymap(field_id.clone(), field_settings); - if let Ok(settings) = field_settings { - Some((field_id, settings)) + let field_settings = field_ids + .iter() + .map(|field_id| { + if !field_settings_map.contains_key(field_id) { + let field_settings = + FieldSettings::from_anymap(field_id, layout_type, &default_field_settings); + (field_id.clone(), field_settings) } else { - None + let field_settings = FieldSettings::from_anymap( + field_id, + layout_type, + field_settings_map.get(field_id).unwrap(), + ); + (field_id.clone(), field_settings) } }) - .collect() + .collect(); + + field_settings } fn update_field_settings( @@ -1456,25 +1430,29 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl { view_id: &str, field_id: &str, visibility: Option<FieldVisibility>, + width: Option<i32>, ) { let field_settings_map = self.get_field_settings(view_id, &[field_id.to_string()]); let new_field_settings = if let Some(field_settings) = field_settings_map.get(field_id) { - let mut field_settings = field_settings.to_owned(); - field_settings.visibility = visibility.unwrap_or(field_settings.visibility); - field_settings + FieldSettings { + field_id: field_settings.field_id.clone(), + visibility: visibility.unwrap_or(field_settings.visibility.clone()), + width: width.unwrap_or(field_settings.width), + } } else { - let layout_ty = self.get_layout_for_view(view_id); - let mut field_settings = FieldSettings::try_from_anymap( - field_id.to_string(), - default_field_settings_by_layout_map() - .get(&layout_ty) - .unwrap() - .to_owned(), - ) - .unwrap(); - field_settings.visibility = visibility.unwrap_or(field_settings.visibility); - field_settings + let layout_type = self.get_layout_for_view(view_id); + let default_field_settings = default_field_settings_by_layout_map() + .get(&layout_type) + .unwrap() + .to_owned(); + let field_settings = + FieldSettings::from_anymap(field_id, layout_type, &default_field_settings); + FieldSettings { + field_id: field_settings.field_id.clone(), + visibility: visibility.unwrap_or(field_settings.visibility), + width: width.unwrap_or(field_settings.width), + } }; self.database.lock().update_field_settings( diff --git a/frontend/rust-lib/flowy-database2/src/services/database/util.rs b/frontend/rust-lib/flowy-database2/src/services/database/util.rs index 69bac217c89d..057cdea5e8fa 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/util.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/util.rs @@ -58,7 +58,9 @@ pub(crate) fn database_view_setting_pb_from_view(view: DatabaseView) -> Database .field_settings .into_inner() .into_iter() - .flat_map(|(field_id, field_settings)| FieldSettings::try_from_anymap(field_id, field_settings)) + .map(|(field_id, field_settings)| { + FieldSettings::from_anymap(&field_id, view.layout, &field_settings) + }) .map(FieldSettingsPB::from) .collect::<Vec<FieldSettingsPB>>(); diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs index 276ef4827373..0c63a64d73de 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs @@ -844,19 +844,20 @@ impl DatabaseViewEditor { self.delegate.get_field_settings(&self.view_id, field_ids) } - pub async fn v_get_all_field_settings(&self) -> HashMap<String, FieldSettings> { - self.delegate.get_all_field_settings(&self.view_id) - } + // pub async fn v_get_all_field_settings(&self) -> HashMap<String, FieldSettings> { + // self.delegate.get_all_field_settings(&self.view_id) + // } pub async fn v_update_field_settings( &self, view_id: &str, field_id: &str, visibility: Option<FieldVisibility>, + width: Option<i32>, ) -> FlowyResult<()> { self .delegate - .update_field_settings(view_id, field_id, visibility); + .update_field_settings(view_id, field_id, visibility, width); Ok(()) } diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_operation.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_operation.rs index 77472a3452b6..99df59b512ff 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_operation.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_operation.rs @@ -115,12 +115,11 @@ pub trait DatabaseViewOperation: Send + Sync + 'static { field_ids: &[String], ) -> HashMap<String, FieldSettings>; - fn get_all_field_settings(&self, view_id: &str) -> HashMap<String, FieldSettings>; - fn update_field_settings( &self, view_id: &str, field_id: &str, visibility: Option<FieldVisibility>, + width: Option<i32>, ); } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/field_builder.rs b/frontend/rust-lib/flowy-database2/src/services/field/field_builder.rs index 95347214afc2..7a2009de9179 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/field_builder.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/field_builder.rs @@ -15,7 +15,7 @@ impl FieldBuilder { field_type.clone().into(), false, ); - field.width = field_type.default_cell_width() as i64; + field.width = 150; field .type_options .insert(field_type.to_string(), type_option_data.into()); diff --git a/frontend/rust-lib/flowy-database2/src/services/field_settings/entities.rs b/frontend/rust-lib/flowy-database2/src/services/field_settings/entities.rs index ab3dd8d82428..674864afca14 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field_settings/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field_settings/entities.rs @@ -1,32 +1,42 @@ -use anyhow::bail; use collab::core::any_map::AnyMapExtension; -use collab_database::views::{FieldSettingsMap, FieldSettingsMapBuilder}; +use collab_database::views::{DatabaseLayout, FieldSettingsMap, FieldSettingsMapBuilder}; use crate::entities::FieldVisibility; +use crate::services::field_settings::default_field_visibility; /// Stores the field settings for a single field #[derive(Debug, Clone)] pub struct FieldSettings { pub field_id: String, pub visibility: FieldVisibility, + pub width: i32, } pub const VISIBILITY: &str = "visibility"; +pub const WIDTH: &str = "width"; + +pub const DEFAULT_WIDTH: i32 = 150; impl FieldSettings { - pub fn try_from_anymap( - field_id: String, - field_settings: FieldSettingsMap, - ) -> Result<Self, anyhow::Error> { - let visibility = match field_settings.get_i64_value(VISIBILITY) { - Some(visbility) => visbility.into(), - _ => bail!("Invalid field settings data"), - }; - - Ok(Self { - field_id, + pub fn from_anymap( + field_id: &str, + layout_type: DatabaseLayout, + field_settings: &FieldSettingsMap, + ) -> Self { + let visibility = field_settings + .get_i64_value(VISIBILITY) + .map(Into::into) + .unwrap_or_else(|| default_field_visibility(layout_type)); + let width = field_settings + .get_i64_value(WIDTH) + .map(|value| value as i32) + .unwrap_or(DEFAULT_WIDTH); + + Self { + field_id: field_id.to_string(), visibility, - }) + width, + } } } @@ -34,14 +44,16 @@ impl From<FieldSettings> for FieldSettingsMap { fn from(field_settings: FieldSettings) -> Self { FieldSettingsMapBuilder::new() .insert_i64_value(VISIBILITY, field_settings.visibility.into()) + .insert_i64_value(WIDTH, field_settings.width as i64) .build() } } /// Contains the changeset to a field's settings. -/// A `Some` value for constitutes a change in that particular setting +/// A `Some` value constitutes a change in that particular setting pub struct FieldSettingsChangesetParams { pub view_id: String, pub field_id: String, pub visibility: Option<FieldVisibility>, + pub width: Option<i32>, } diff --git a/frontend/rust-lib/flowy-database2/src/services/field_settings/field_settings_builder.rs b/frontend/rust-lib/flowy-database2/src/services/field_settings/field_settings_builder.rs index 01c0a6b875ff..751b84eafddc 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field_settings/field_settings_builder.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field_settings/field_settings_builder.rs @@ -1,7 +1,5 @@ use std::collections::HashMap; -use std::sync::Arc; -use collab_database::database::MutexDatabase; use collab_database::fields::Field; use collab_database::views::{ DatabaseLayout, FieldSettingsByFieldIdMap, FieldSettingsMap, FieldSettingsMapBuilder, @@ -14,7 +12,7 @@ use crate::services::field_settings::{FieldSettings, VISIBILITY}; /// Helper struct to create a new field setting pub struct FieldSettingsBuilder { - field_settings: FieldSettings, + inner: FieldSettings, } impl FieldSettingsBuilder { @@ -22,57 +20,49 @@ impl FieldSettingsBuilder { let field_settings = FieldSettings { field_id: field_id.to_string(), visibility: FieldVisibility::AlwaysShown, + width: 150, }; - Self { field_settings } + Self { + inner: field_settings, + } } - pub fn field_id(mut self, field_id: &str) -> Self { - self.field_settings.field_id = field_id.to_string(); + pub fn visibility(mut self, visibility: FieldVisibility) -> Self { + self.inner.visibility = visibility; self } - pub fn visibility(mut self, visibility: FieldVisibility) -> Self { - self.field_settings.visibility = visibility; + pub fn width(mut self, width: i32) -> Self { + self.inner.width = width; self } pub fn build(self) -> FieldSettings { - self.field_settings + self.inner } } -pub struct DatabaseFieldSettingsMapBuilder { - pub fields: Vec<Field>, - pub database_layout: DatabaseLayout, -} - -impl DatabaseFieldSettingsMapBuilder { - pub fn new(fields: Vec<Field>, database_layout: DatabaseLayout) -> Self { - Self { - fields, - database_layout, - } - } - - pub fn from_database(database: Arc<MutexDatabase>, database_layout: DatabaseLayout) -> Self { - let fields = database.lock().get_fields(None); - Self { - fields, - database_layout, - } +#[inline] +pub fn default_field_visibility(layout_type: DatabaseLayout) -> FieldVisibility { + match layout_type { + DatabaseLayout::Grid => FieldVisibility::AlwaysShown, + DatabaseLayout::Board => FieldVisibility::HideWhenEmpty, + DatabaseLayout::Calendar => FieldVisibility::HideWhenEmpty, } +} - pub fn build(self) -> FieldSettingsByFieldIdMap { - self - .fields - .into_iter() - .map(|field| { - let field_settings = field_settings_for_field(self.database_layout, &field); - (field.id, field_settings) - }) - .collect::<HashMap<String, FieldSettingsMap>>() - .into() - } +pub fn default_field_settings_for_fields( + fields: &Vec<Field>, + layout_type: DatabaseLayout, +) -> FieldSettingsByFieldIdMap { + fields + .iter() + .map(|field| { + let field_settings = field_settings_for_field(layout_type, field); + (field.id.clone(), field_settings) + }) + .collect::<HashMap<_, _>>() + .into() } pub fn field_settings_for_field( @@ -82,11 +72,7 @@ pub fn field_settings_for_field( let visibility = if field.is_primary { FieldVisibility::AlwaysShown } else { - match database_layout { - DatabaseLayout::Grid => FieldVisibility::AlwaysShown, - DatabaseLayout::Board => FieldVisibility::HideWhenEmpty, - DatabaseLayout::Calendar => FieldVisibility::HideWhenEmpty, - } + default_field_visibility(database_layout) }; FieldSettingsBuilder::new(&field.id) @@ -98,11 +84,7 @@ pub fn field_settings_for_field( pub fn default_field_settings_by_layout_map() -> HashMap<DatabaseLayout, FieldSettingsMap> { let mut map = HashMap::new(); for layout_ty in DatabaseLayout::iter() { - let visibility = match layout_ty { - DatabaseLayout::Grid => FieldVisibility::AlwaysShown, - DatabaseLayout::Board => FieldVisibility::HideWhenEmpty, - DatabaseLayout::Calendar => FieldVisibility::HideWhenEmpty, - }; + let visibility = default_field_visibility(layout_ty); let field_settings = FieldSettingsMapBuilder::new() .insert_i64_value(VISIBILITY, visibility.into()) .build(); diff --git a/frontend/rust-lib/flowy-database2/src/services/share/csv/import.rs b/frontend/rust-lib/flowy-database2/src/services/share/csv/import.rs index ee16cdc94d87..037c53597d75 100644 --- a/frontend/rust-lib/flowy-database2/src/services/share/csv/import.rs +++ b/frontend/rust-lib/flowy-database2/src/services/share/csv/import.rs @@ -9,7 +9,7 @@ use flowy_error::{FlowyError, FlowyResult}; use crate::entities::FieldType; use crate::services::field::{default_type_option_data_from_type, CELL_DATA}; -use crate::services::field_settings::DatabaseFieldSettingsMapBuilder; +use crate::services::field_settings::default_field_settings_for_fields; use crate::services::share::csv::CSVFormat; #[derive(Default)] @@ -97,8 +97,7 @@ fn database_from_fields_and_rows( }) .collect::<Vec<Field>>(); - let field_settings = - DatabaseFieldSettingsMapBuilder::new(fields.clone(), DatabaseLayout::Grid).build(); + let field_settings = default_field_settings_for_fields(&fields, DatabaseLayout::Grid); let created_rows = rows .iter() diff --git a/frontend/rust-lib/flowy-database2/src/template.rs b/frontend/rust-lib/flowy-database2/src/template.rs index 23a217638c8a..bc3f23bc056d 100644 --- a/frontend/rust-lib/flowy-database2/src/template.rs +++ b/frontend/rust-lib/flowy-database2/src/template.rs @@ -7,7 +7,7 @@ use crate::services::cell::{insert_select_option_cell, insert_text_cell}; use crate::services::field::{ FieldBuilder, SelectOption, SelectOptionColor, SingleSelectTypeOption, }; -use crate::services::field_settings::DatabaseFieldSettingsMapBuilder; +use crate::services::field_settings::default_field_settings_for_fields; use crate::services::setting::{BoardLayoutSetting, CalendarLayoutSetting}; pub fn make_default_grid(view_id: &str, name: &str) -> CreateDatabaseParams { @@ -29,8 +29,7 @@ pub fn make_default_grid(view_id: &str, name: &str) -> CreateDatabaseParams { let fields = vec![text_field, single_select, checkbox_field]; - let field_settings = - DatabaseFieldSettingsMapBuilder::new(fields.clone(), DatabaseLayout::Grid).build(); + let field_settings = default_field_settings_for_fields(&fields, DatabaseLayout::Grid); CreateDatabaseParams { database_id: gen_database_id(), @@ -90,8 +89,7 @@ pub fn make_default_board(view_id: &str, name: &str) -> CreateDatabaseParams { let fields = vec![text_field, single_select]; - let field_settings = - DatabaseFieldSettingsMapBuilder::new(fields.clone(), DatabaseLayout::Board).build(); + let field_settings = default_field_settings_for_fields(&fields, DatabaseLayout::Board); let mut layout_settings = LayoutSettings::default(); layout_settings.insert(DatabaseLayout::Board, BoardLayoutSetting::new().into()); @@ -134,8 +132,7 @@ pub fn make_default_calendar(view_id: &str, name: &str) -> CreateDatabaseParams let fields = vec![text_field, date_field, multi_select_field]; - let field_settings = - DatabaseFieldSettingsMapBuilder::new(fields.clone(), DatabaseLayout::Calendar).build(); + let field_settings = default_field_settings_for_fields(&fields, DatabaseLayout::Calendar); let mut layout_settings = LayoutSettings::default(); layout_settings.insert( diff --git a/frontend/rust-lib/flowy-database2/tests/database/field_settings_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/field_settings_test/script.rs index 492221184a23..e5251bd1213f 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/field_settings_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/field_settings_test/script.rs @@ -1,4 +1,3 @@ -use collab_database::views::DatabaseLayout; use flowy_database2::entities::FieldVisibility; use flowy_database2::services::field_settings::FieldSettingsChangesetParams; @@ -8,16 +7,17 @@ use crate::database::database_editor::DatabaseEditorTest; pub enum FieldSettingsScript { AssertFieldSettings { field_ids: Vec<String>, - layout_ty: DatabaseLayout, visibility: FieldVisibility, + width: i32, }, AssertAllFieldSettings { - layout_ty: DatabaseLayout, visibility: FieldVisibility, + width: i32, }, UpdateFieldSettings { field_id: String, visibility: Option<FieldVisibility>, + width: Option<i32>, }, } @@ -51,41 +51,42 @@ impl FieldSettingsTest { match script { FieldSettingsScript::AssertFieldSettings { field_ids, - layout_ty, visibility, + width, } => { let field_settings = self .editor - .get_field_settings(&self.view_id, layout_ty, field_ids) + .get_field_settings(&self.view_id, field_ids) .await .unwrap(); for field_settings in field_settings.into_iter() { - assert_eq!(field_settings.visibility, visibility) + assert_eq!(field_settings.width, width); + assert_eq!(field_settings.visibility, visibility); } }, - FieldSettingsScript::AssertAllFieldSettings { - layout_ty, - visibility, - } => { + FieldSettingsScript::AssertAllFieldSettings { visibility, width } => { let field_settings = self .editor - .get_all_field_settings(&self.view_id, layout_ty) + .get_all_field_settings(&self.view_id) .await .unwrap(); for field_settings in field_settings.into_iter() { - assert_eq!(field_settings.visibility, visibility) + assert_eq!(field_settings.width, width); + assert_eq!(field_settings.visibility, visibility); } }, FieldSettingsScript::UpdateFieldSettings { field_id, visibility, + width, } => { let params = FieldSettingsChangesetParams { view_id: self.view_id.clone(), field_id, visibility, + width, }; let _ = self .editor diff --git a/frontend/rust-lib/flowy-database2/tests/database/field_settings_test/test.rs b/frontend/rust-lib/flowy-database2/tests/database/field_settings_test/test.rs index 749f1c9419ef..3c8963b8e667 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/field_settings_test/test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/field_settings_test/test.rs @@ -1,6 +1,6 @@ -use collab_database::views::DatabaseLayout; use flowy_database2::entities::FieldType; use flowy_database2::entities::FieldVisibility; +use flowy_database2::services::field_settings::DEFAULT_WIDTH; use crate::database::field_settings_test::script::FieldSettingsScript::*; use crate::database::field_settings_test::script::FieldSettingsTest; @@ -10,8 +10,8 @@ use crate::database::field_settings_test::script::FieldSettingsTest; async fn get_default_field_settings() { let mut test = FieldSettingsTest::new_grid().await; let scripts = vec![AssertAllFieldSettings { - layout_ty: DatabaseLayout::Grid, visibility: FieldVisibility::AlwaysShown, + width: DEFAULT_WIDTH, }]; test.run_scripts(scripts).await; @@ -26,13 +26,13 @@ async fn get_default_field_settings() { let scripts = vec![ AssertFieldSettings { field_ids: non_primary_field_ids.clone(), - layout_ty: DatabaseLayout::Board, visibility: FieldVisibility::HideWhenEmpty, + width: DEFAULT_WIDTH, }, AssertFieldSettings { field_ids: vec![primary_field_id.clone()], - layout_ty: DatabaseLayout::Board, visibility: FieldVisibility::AlwaysShown, + width: DEFAULT_WIDTH, }, ]; test.run_scripts(scripts).await; @@ -48,13 +48,13 @@ async fn get_default_field_settings() { let scripts = vec![ AssertFieldSettings { field_ids: non_primary_field_ids.clone(), - layout_ty: DatabaseLayout::Calendar, visibility: FieldVisibility::HideWhenEmpty, + width: DEFAULT_WIDTH, }, AssertFieldSettings { field_ids: vec![primary_field_id.clone()], - layout_ty: DatabaseLayout::Calendar, visibility: FieldVisibility::AlwaysShown, + width: DEFAULT_WIDTH, }, ]; test.run_scripts(scripts).await; @@ -75,21 +75,22 @@ async fn update_field_settings_test() { let scripts = vec![ AssertFieldSettings { field_ids: non_primary_field_ids, - layout_ty: DatabaseLayout::Board, visibility: FieldVisibility::HideWhenEmpty, + width: DEFAULT_WIDTH, }, AssertFieldSettings { field_ids: vec![primary_field_id.clone()], - layout_ty: DatabaseLayout::Board, visibility: FieldVisibility::AlwaysShown, + width: DEFAULT_WIDTH, }, UpdateFieldSettings { field_id: primary_field_id, visibility: Some(FieldVisibility::HideWhenEmpty), + width: None, }, AssertAllFieldSettings { - layout_ty: DatabaseLayout::Board, visibility: FieldVisibility::HideWhenEmpty, + width: DEFAULT_WIDTH, }, ]; test.run_scripts(scripts).await; diff --git a/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs b/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs index d26f3bd59f6b..1e21cb9474db 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs @@ -1,6 +1,6 @@ use collab_database::database::{gen_database_id, gen_database_view_id, gen_row_id, DatabaseData}; use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting, LayoutSettings}; -use flowy_database2::services::field_settings::DatabaseFieldSettingsMapBuilder; +use flowy_database2::services::field_settings::default_field_settings_for_fields; use flowy_database2::services::setting::BoardLayoutSetting; use strum::IntoEnumIterator; @@ -131,8 +131,7 @@ pub fn make_test_board() -> DatabaseData { let board_setting: LayoutSetting = BoardLayoutSetting::new().into(); - let field_settings = - DatabaseFieldSettingsMapBuilder::new(fields.clone(), DatabaseLayout::Board).build(); + let field_settings = default_field_settings_for_fields(&fields, DatabaseLayout::Board); // We have many assumptions base on the number of the rows, so do not change the number of the loop. for i in 0..5 { diff --git a/frontend/rust-lib/flowy-database2/tests/database/mock_data/calendar_mock_data.rs b/frontend/rust-lib/flowy-database2/tests/database/mock_data/calendar_mock_data.rs index c41433e591c6..7587c8ca4f2b 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/mock_data/calendar_mock_data.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/mock_data/calendar_mock_data.rs @@ -1,6 +1,6 @@ use collab_database::database::{gen_database_id, gen_database_view_id, gen_row_id, DatabaseData}; use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting, LayoutSettings}; -use flowy_database2::services::field_settings::DatabaseFieldSettingsMapBuilder; +use flowy_database2::services::field_settings::default_field_settings_for_fields; use strum::IntoEnumIterator; use flowy_database2::entities::FieldType; @@ -40,8 +40,7 @@ pub fn make_test_calendar() -> DatabaseData { let calendar_setting: LayoutSetting = CalendarLayoutSetting::new(date_field_id).into(); - let field_settings = - DatabaseFieldSettingsMapBuilder::new(fields.clone(), DatabaseLayout::Calendar).build(); + let field_settings = default_field_settings_for_fields(&fields, DatabaseLayout::Calendar); for i in 0..5 { let mut row_builder = TestRowBuilder::new(gen_row_id(), &fields); diff --git a/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs b/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs index fcba0a73f21e..52790c2b2758 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs @@ -1,6 +1,6 @@ use collab_database::database::{gen_database_id, gen_database_view_id, gen_row_id, DatabaseData}; use collab_database::views::{DatabaseLayout, DatabaseView}; -use flowy_database2::services::field_settings::DatabaseFieldSettingsMapBuilder; +use flowy_database2::services::field_settings::default_field_settings_for_fields; use strum::IntoEnumIterator; use flowy_database2::entities::FieldType; @@ -131,8 +131,7 @@ pub fn make_test_grid() -> DatabaseData { } } - let field_settings = - DatabaseFieldSettingsMapBuilder::new(fields.clone(), DatabaseLayout::Grid).build(); + let field_settings = default_field_settings_for_fields(&fields, DatabaseLayout::Grid); for i in 0..7 { let mut row_builder = TestRowBuilder::new(gen_row_id(), &fields); @@ -297,8 +296,7 @@ pub fn make_no_date_test_grid() -> DatabaseData { } } - let field_settings = - DatabaseFieldSettingsMapBuilder::new(fields.clone(), DatabaseLayout::Grid).build(); + let field_settings = default_field_settings_for_fields(&fields, DatabaseLayout::Grid); for i in 0..3 { let mut row_builder = TestRowBuilder::new(gen_row_id(), &fields); From c4fc60612fdda3d8c9e90a07d23a8e31a08e11db Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 6 Nov 2023 16:17:05 +0800 Subject: [PATCH 29/56] feat: add new group (#3854) * feat: implement backend logic * fix: did_create_row not working properly * fix: did_delete_group not working properly * fix: test * chore: fix clippy * fix: new card not editable and in wrong position * feat: imlement UI for add new stack * test: add integration test * chore: i18n * chore: remove debug message * chore: merge conflict --------- Co-authored-by: nathan <nathan@appflowy.io> --- .../board/board_row_test.dart | 54 ++++++++ .../util/database_test_op.dart | 80 ++++++++++++ .../application/group/group_service.dart | 11 ++ .../board/application/board_bloc.dart | 5 + .../board/presentation/board_page.dart | 117 +++++++++++++++++- frontend/appflowy_flutter/pubspec.lock | 4 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- .../flowy_icons/24x/close_filled.svg | 3 + frontend/resources/translations/en.json | 3 +- .../tests/database/local_test/group_test.rs | 2 +- .../src/entities/group_entities/group.rs | 33 +++++ .../flowy-database2/src/event_handler.rs | 14 +++ .../rust-lib/flowy-database2/src/event_map.rs | 4 + .../src/services/database/database_editor.rs | 8 +- .../src/services/database_view/view_editor.rs | 91 ++++++++------ .../select_type_option.rs | 16 ++- .../src/services/group/action.rs | 87 ++++++++++--- .../src/services/group/configuration.rs | 3 +- .../src/services/group/controller.rs | 81 +++++++++--- .../controller_impls/checkbox_controller.rs | 12 +- .../group/controller_impls/date_controller.rs | 28 +++-- .../controller_impls/default_controller.rs | 43 +++++-- .../multi_select_controller.rs | 29 +++-- .../single_select_controller.rs | 34 +++-- .../select_option_controller/util.rs | 1 + .../group/controller_impls/url_controller.rs | 23 ++-- .../tests/database/group_test/script.rs | 8 ++ .../tests/database/group_test/test.rs | 16 +++ 28 files changed, 674 insertions(+), 138 deletions(-) create mode 100644 frontend/resources/flowy_icons/24x/close_filled.svg diff --git a/frontend/appflowy_flutter/integration_test/board/board_row_test.dart b/frontend/appflowy_flutter/integration_test/board/board_row_test.dart index a576dbe1c24c..32abba188829 100644 --- a/frontend/appflowy_flutter/integration_test/board/board_row_test.dart +++ b/frontend/appflowy_flutter/integration_test/board/board_row_test.dart @@ -1,11 +1,14 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database_view/widgets/card/card.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:appflowy_board/appflowy_board.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import '../util/util.dart'; +import '../util/database_test_op.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -46,5 +49,56 @@ void main() { await tester.tapButtonWithName(LocaleKeys.button_duplicate.tr()); expect(find.textContaining(name, findRichText: true), findsNWidgets(2)); }); + + testWidgets('add new group', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Board); + + // assert number of groups + tester.assertNumberOfGroups(4); + + // scroll the board horizontally to ensure add new group button appears + await tester.scrollBoardToEnd(); + + // assert and click on add new group button + tester.assertNewGroupTextField(false); + await tester.tapNewGroupButton(); + tester.assertNewGroupTextField(true); + + // enter new group name and submit + await tester.enterNewGroupName('needs design', submit: true); + + // assert number of groups has increased + tester.assertNumberOfGroups(5); + + // assert text field has disappeared + await tester.scrollBoardToEnd(); + tester.assertNewGroupTextField(false); + + // click on add new group button + await tester.tapNewGroupButton(); + tester.assertNewGroupTextField(true); + + // type some things + await tester.enterNewGroupName('needs planning', submit: false); + + // click on clear button and assert empty contents + await tester.clearNewGroupTextField(); + + // press escape to cancel + await tester.sendKeyEvent(LogicalKeyboardKey.escape); + await tester.pumpAndSettle(); + tester.assertNewGroupTextField(false); + + // click on add new group button + await tester.tapNewGroupButton(); + tester.assertNewGroupTextField(true); + + // press elsewhere to cancel + await tester.tap(find.byType(AppFlowyBoard)); + await tester.pumpAndSettle(); + tester.assertNewGroupTextField(false); + }); }); } diff --git a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart index b91511e5d3f5..907f41b57be0 100644 --- a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart'; +import 'package:appflowy/plugins/database_view/board/presentation/widgets/board_column_header.dart'; import 'package:appflowy/plugins/database_view/calendar/application/calendar_bloc.dart'; import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_day.dart'; import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_event_card.dart'; @@ -59,6 +60,7 @@ import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:appflowy_board/appflowy_board.dart'; import 'package:calendar_view/calendar_view.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -1390,6 +1392,84 @@ extension AppFlowyDatabaseTest on WidgetTester { await tapButton(findCreateButton); } + void assertNumberOfGroups(int number) { + final groups = find.byType(BoardColumnHeader, skipOffstage: false); + expect(groups, findsNWidgets(number)); + } + + Future<void> scrollBoardToEnd() async { + final scrollable = find + .descendant( + of: find.byType(AppFlowyBoard), + matching: find.byWidgetPredicate( + (widget) => widget is Scrollable && widget.axis == Axis.horizontal, + ), + ) + .first; + await scrollUntilVisible( + find.byType(BoardTrailing), + 300, + scrollable: scrollable, + ); + } + + Future<void> tapNewGroupButton() async { + final button = find.descendant( + of: find.byType(BoardTrailing), + matching: find.byWidgetPredicate( + (widget) => widget is FlowySvg && widget.svg == FlowySvgs.add_s, + ), + ); + expect(button, findsOneWidget); + await tapButton(button); + } + + void assertNewGroupTextField(bool isVisible) { + final textField = find.descendant( + of: find.byType(BoardTrailing), + matching: find.byType(TextField), + ); + if (isVisible) { + expect(textField, findsOneWidget); + } else { + expect(textField, findsNothing); + } + } + + Future<void> enterNewGroupName(String name, {required bool submit}) async { + final textField = find.descendant( + of: find.byType(BoardTrailing), + matching: find.byType(TextField), + ); + await enterText(textField, name); + await pumpAndSettle(); + if (submit) { + await testTextInput.receiveAction(TextInputAction.done); + await pumpAndSettle(); + } + } + + Future<void> clearNewGroupTextField() async { + final textField = find.descendant( + of: find.byType(BoardTrailing), + matching: find.byType(TextField), + ); + await tapButton( + find.descendant( + of: textField, + matching: find.byWidgetPredicate( + (widget) => + widget is FlowySvg && widget.svg == FlowySvgs.close_filled_m, + ), + ), + ); + final textFieldWidget = widget<TextField>(textField); + assert( + textFieldWidget.controller != null && + textFieldWidget.controller!.text.isEmpty, + ); + } + Future<void> tapTabBarLinkedViewByViewName(String name) async { final viewButton = findTabBarLinkViewByViewName(name); await tapButton(viewButton); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/group/group_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/group/group_service.dart index 7b14af888613..26233317b0c1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/group/group_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/group/group_service.dart @@ -37,4 +37,15 @@ class GroupBackendService { } return DatabaseEventUpdateGroup(payload).send(); } + + Future<Either<Unit, FlowyError>> createGroup({ + required String name, + String groupConfigId = "", + }) { + final payload = CreateGroupPayloadPB.create() + ..viewId = viewId + ..name = name; + + return DatabaseEventCreateGroup(payload).send(); + } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart index f9cfb22c02f8..30694c03d16a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart @@ -98,6 +98,10 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> { (err) => Log.error(err), ); }, + createGroup: (name) async { + final result = await groupBackendSvc.createGroup(name: name); + result.fold((_) {}, (err) => Log.error(err)); + }, didCreateRow: (group, row, int? index) { emit( state.copyWith( @@ -346,6 +350,7 @@ class BoardEvent with _$BoardEvent { const factory BoardEvent.initial() = _InitialBoard; const factory BoardEvent.createBottomRow(String groupId) = _CreateBottomRow; const factory BoardEvent.createHeaderRow(String groupId) = _CreateHeaderRow; + const factory BoardEvent.createGroup(String name) = _CreateGroup; const factory BoardEvent.startEditingHeader(String groupId) = _StartEditingHeader; const factory BoardEvent.endEditingHeader(String groupId, String groupName) = diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart index f2d584d9be43..c2260a2ff093 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart @@ -16,12 +16,12 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; import 'package:appflowy_board/appflowy_board.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui_web.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart' hide Card; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../widgets/card/cells/card_cell.dart'; @@ -127,6 +127,7 @@ class BoardContent extends StatefulWidget { class _BoardContentState extends State<BoardContent> { late AppFlowyBoardScrollController scrollManager; + late final ScrollController scrollController; final renderHook = RowCardRenderHook<String>(); final config = const AppFlowyBoardConfig( @@ -138,6 +139,7 @@ class _BoardContentState extends State<BoardContent> { super.initState(); scrollManager = AppFlowyBoardScrollController(); + scrollController = ScrollController(); renderHook.addSelectOptionHook((options, groupId, _) { // The cell should hide if the option id is equal to the groupId. final isInGroup = @@ -172,7 +174,7 @@ class _BoardContentState extends State<BoardContent> { Expanded( child: AppFlowyBoard( boardScrollController: scrollManager, - scrollController: ScrollController(), + scrollController: scrollController, controller: context.read<BoardBloc>().boardController, headerBuilder: (_, groupData) => BlocProvider<BoardBloc>.value( @@ -183,6 +185,7 @@ class _BoardContentState extends State<BoardContent> { ), ), footerBuilder: _buildFooter, + trailing: BoardTrailing(scrollController: scrollController), cardBuilder: (_, column, columnItem) => _buildCard( context, column, @@ -348,3 +351,109 @@ class _BoardContentState extends State<BoardContent> { ); } } + +class BoardTrailing extends StatefulWidget { + final ScrollController scrollController; + const BoardTrailing({required this.scrollController, super.key}); + + @override + State<BoardTrailing> createState() => _BoardTrailingState(); +} + +class _BoardTrailingState extends State<BoardTrailing> { + bool isEditing = false; + late final TextEditingController _textController; + late final FocusNode _focusNode; + + void _cancelAddNewGroup() { + _textController.clear(); + setState(() { + isEditing = false; + }); + } + + @override + void initState() { + super.initState(); + _textController = TextEditingController(); + _focusNode = FocusNode( + onKeyEvent: (node, event) { + if (_focusNode.hasFocus && + event.logicalKey == LogicalKeyboardKey.escape) { + _cancelAddNewGroup(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + )..addListener(() { + if (!_focusNode.hasFocus) { + _cancelAddNewGroup(); + } + }); + } + + @override + Widget build(BuildContext context) { + // call after every setState + WidgetsBinding.instance.addPostFrameCallback((_) { + if (isEditing) { + _focusNode.requestFocus(); + widget.scrollController.jumpTo( + widget.scrollController.position.maxScrollExtent, + ); + } + }); + + return Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Align( + alignment: AlignmentDirectional.topStart, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: isEditing + ? SizedBox( + width: 256, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + controller: _textController, + focusNode: _focusNode, + decoration: InputDecoration( + suffixIcon: Padding( + padding: const EdgeInsets.only(left: 4, bottom: 8.0), + child: FlowyIconButton( + icon: const FlowySvg(FlowySvgs.close_filled_m), + hoverColor: Colors.transparent, + onPressed: () => _textController.clear(), + ), + ), + suffixIconConstraints: + BoxConstraints.loose(const Size(20, 24)), + border: const UnderlineInputBorder(), + contentPadding: const EdgeInsets.fromLTRB(8, 4, 8, 8), + isDense: true, + ), + style: Theme.of(context).textTheme.bodySmall, + maxLines: 1, + onSubmitted: (groupName) => context + .read<BoardBloc>() + .add(BoardEvent.createGroup(groupName)), + ), + ), + ) + : FlowyTooltip( + message: LocaleKeys.board_column_createNewColumn.tr(), + child: FlowyIconButton( + width: 26, + icon: const FlowySvg(FlowySvgs.add_s), + iconColorOnHover: Theme.of(context).colorScheme.onSurface, + onPressed: () => setState(() { + isEditing = true; + }), + ), + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 74658d9c6723..bede73fec4a9 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -45,8 +45,8 @@ packages: dependency: "direct main" description: path: "." - ref: "6aba8dd" - resolved-ref: "6aba8ddd86839ca09b997cb2457f013236e0c337" + ref: "1a329c2" + resolved-ref: "1a329c21921c0d19871bea3237b7d80fe131f2ed" url: "https://github.com/AppFlowy-IO/appflowy-board.git" source: git version: "0.1.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 24f060e392d8..8e38938ddafb 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -43,7 +43,7 @@ dependencies: # path: packages/appflowy_board git: url: https://github.com/AppFlowy-IO/appflowy-board.git - ref: 6aba8dd + ref: 1a329c2 appflowy_editor: ^1.5.1 appflowy_popover: path: packages/appflowy_popover diff --git a/frontend/resources/flowy_icons/24x/close_filled.svg b/frontend/resources/flowy_icons/24x/close_filled.svg new file mode 100644 index 000000000000..e6dffe953a10 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/close_filled.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M8 15C11.866 15 15 11.866 15 8C15 4.13401 11.866 1 8 1C4.13401 1 1 4.13401 1 8C1 11.866 4.13401 15 8 15ZM10.1392 5.14768C10.3361 4.95077 10.6554 4.95077 10.8523 5.14768C11.0492 5.34459 11.0492 5.66385 10.8523 5.86076L8.71303 8L10.8523 10.1392C11.0492 10.3362 11.0492 10.6554 10.8523 10.8523C10.6554 11.0492 10.3361 11.0492 10.1392 10.8523L7.99996 8.71307L5.86076 10.8523C5.66385 11.0492 5.34459 11.0492 5.14768 10.8523C4.95077 10.6554 4.95077 10.3361 5.14768 10.1392L7.28688 8L5.14769 5.8608C4.95078 5.66389 4.95078 5.34464 5.14769 5.14773C5.3446 4.95082 5.66385 4.95082 5.86076 5.14773L7.99996 7.28692L10.1392 5.14768Z" fill="#1F2329" fill-opacity="0.4"/> +</svg> diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 09ff958b53bf..b38493cf7651 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -755,7 +755,8 @@ "board": { "column": { "createNewCard": "New", - "renameGroupTooltip": "Press to rename group" + "renameGroupTooltip": "Press to rename group", + "createNewColumn": "Add a new group" }, "menuName": "Board", "showUngrouped": "Show ungrouped items", diff --git a/frontend/rust-lib/event-integration/tests/database/local_test/group_test.rs b/frontend/rust-lib/event-integration/tests/database/local_test/group_test.rs index 033d2cb81af8..bd46d337b35c 100644 --- a/frontend/rust-lib/event-integration/tests/database/local_test/group_test.rs +++ b/frontend/rust-lib/event-integration/tests/database/local_test/group_test.rs @@ -3,7 +3,7 @@ use event_integration::EventIntegrationTest; #[tokio::test] async fn update_group_name_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let board_view = test .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) .await; diff --git a/frontend/rust-lib/flowy-database2/src/entities/group_entities/group.rs b/frontend/rust-lib/flowy-database2/src/entities/group_entities/group.rs index 7200c2987e33..4553cee745b4 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/group_entities/group.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/group_entities/group.rs @@ -197,3 +197,36 @@ impl From<UpdateGroupParams> for GroupChangeset { } } } + +#[derive(Debug, Default, ProtoBuf)] +pub struct CreateGroupPayloadPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub group_config_id: String, + + #[pb(index = 3)] + pub name: String, +} + +#[derive(Debug, Clone)] +pub struct CreateGroupParams { + pub view_id: String, + pub group_config_id: String, + pub name: String, +} + +impl TryFrom<CreateGroupPayloadPB> for CreateGroupParams { + type Error = ErrorCode; + + fn try_from(value: CreateGroupPayloadPB) -> Result<Self, Self::Error> { + let view_id = NotEmptyStr::parse(value.view_id).map_err(|_| ErrorCode::ViewIdIsInvalid)?; + let name = NotEmptyStr::parse(value.name).map_err(|_| ErrorCode::ViewIdIsInvalid)?; + Ok(CreateGroupParams { + view_id: view_id.0, + group_config_id: value.group_config_id, + name: name.0, + }) + } +} diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index c49c78333945..ccebbb8fa55a 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -741,6 +741,20 @@ pub(crate) async fn move_group_row_handler( Ok(()) } +#[tracing::instrument(level = "debug", skip(manager), err)] +pub(crate) async fn create_group_handler( + data: AFPluginData<CreateGroupPayloadPB>, + manager: AFPluginState<Weak<DatabaseManager>>, +) -> FlowyResult<()> { + let manager = upgrade_manager(manager)?; + let params: CreateGroupParams = data.into_inner().try_into()?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + database_editor + .create_group(¶ms.view_id, ¶ms.name) + .await?; + Ok(()) +} + #[tracing::instrument(level = "debug", skip(manager), err)] pub(crate) async fn get_databases_handler( manager: AFPluginState<Weak<DatabaseManager>>, diff --git a/frontend/rust-lib/flowy-database2/src/event_map.rs b/frontend/rust-lib/flowy-database2/src/event_map.rs index 4bb000afae99..436a9ec814ee 100644 --- a/frontend/rust-lib/flowy-database2/src/event_map.rs +++ b/frontend/rust-lib/flowy-database2/src/event_map.rs @@ -60,6 +60,7 @@ pub fn init(database_manager: Weak<DatabaseManager>) -> AFPlugin { .event(DatabaseEvent::GetGroups, get_groups_handler) .event(DatabaseEvent::GetGroup, get_group_handler) .event(DatabaseEvent::UpdateGroup, update_group_handler) + .event(DatabaseEvent::CreateGroup, create_group_handler) // Database .event(DatabaseEvent::GetDatabases, get_databases_handler) // Calendar @@ -284,6 +285,9 @@ pub enum DatabaseEvent { #[event(input = "UpdateGroupPB")] UpdateGroup = 113, + #[event(input = "CreateGroupPayloadPB")] + CreateGroup = 114, + /// Returns all the databases #[event(output = "RepeatedDatabaseDescriptionPB")] GetDatabases = 120, diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index ed2851b6fa72..1ff4e8d5526c 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -442,7 +442,7 @@ impl DatabaseEditor { let row_detail = self.database.lock().get_row_detail(&row_order.id); if let Some(row_detail) = row_detail { for view in self.database_views.editors().await { - view.v_did_create_row(&row_detail, &group_id, index).await; + view.v_did_create_row(&row_detail, index).await; } return Ok(Some(row_detail)); } @@ -961,6 +961,12 @@ impl DatabaseEditor { Ok(()) } + pub async fn create_group(&self, view_id: &str, name: &str) -> FlowyResult<()> { + let view_editor = self.database_views.get_view_editor(view_id).await?; + view_editor.v_create_group(name).await?; + Ok(()) + } + #[tracing::instrument(level = "trace", skip_all)] pub async fn set_layout_setting( &self, diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs index 0c63a64d73de..11bc2a497681 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs @@ -14,8 +14,8 @@ use lib_dispatch::prelude::af_spawn; use crate::entities::{ CalendarEventPB, DatabaseLayoutMetaPB, DatabaseLayoutSettingPB, DeleteFilterParams, DeleteGroupParams, DeleteSortParams, FieldType, FieldVisibility, GroupChangesPB, GroupPB, - GroupRowsNotificationPB, InsertedRowPB, LayoutSettingChangeset, LayoutSettingParams, RowMetaPB, - RowsChangePB, SortChangesetNotificationPB, SortPB, UpdateFilterParams, UpdateSortParams, + InsertedRowPB, LayoutSettingChangeset, LayoutSettingParams, RowMetaPB, RowsChangePB, + SortChangesetNotificationPB, SortPB, UpdateFilterParams, UpdateSortParams, }; use crate::notification::{send_notification, DatabaseNotification}; use crate::services::cell::CellCache; @@ -126,39 +126,22 @@ impl DatabaseViewEditor { .send(); } - pub async fn v_did_create_row( - &self, - row_detail: &RowDetail, - group_id: &Option<String>, - index: usize, - ) { - let changes: RowsChangePB; + pub async fn v_did_create_row(&self, row_detail: &RowDetail, index: usize) { // Send the group notification if the current view has groups - match group_id.as_ref() { - None => { - let row = InsertedRowPB::new(RowMetaPB::from(row_detail)).with_index(index as i32); - changes = RowsChangePB::from_insert(row); - }, - Some(group_id) => { - self - .mut_group_controller(|group_controller, _| { - group_controller.did_create_row(row_detail, group_id); - Ok(()) - }) - .await; + if let Some(controller) = self.group_controller.write().await.as_mut() { + let changesets = controller.did_create_row(row_detail, index); - let inserted_row = InsertedRowPB { - row_meta: RowMetaPB::from(row_detail), - index: Some(index as i32), - is_new: true, - }; - let changeset = - GroupRowsNotificationPB::insert(group_id.clone(), vec![inserted_row.clone()]); + for changeset in changesets { notify_did_update_group_rows(changeset).await; - changes = RowsChangePB::from_insert(inserted_row); - }, + } } + let inserted_row = InsertedRowPB { + row_meta: RowMetaPB::from(row_detail), + index: Some(index as i32), + is_new: true, + }; + let changes = RowsChangePB::from_insert(inserted_row); send_notification(&self.view_id, DatabaseNotification::DidUpdateViewRows) .payload(changes) .send(); @@ -168,16 +151,22 @@ impl DatabaseViewEditor { pub async fn v_did_delete_row(&self, row: &Row) { // Send the group notification if the current view has groups; let result = self - .mut_group_controller(|group_controller, field| { - group_controller.did_delete_delete_row(row, &field) - }) + .mut_group_controller(|group_controller, _| group_controller.did_delete_row(row)) .await; if let Some(result) = result { - tracing::trace!("Delete row in view changeset: {:?}", result.row_changesets); + tracing::trace!("Delete row in view changeset: {:?}", result); for changeset in result.row_changesets { notify_did_update_group_rows(changeset).await; } + if let Some(deleted_group) = result.deleted_group { + let payload = GroupChangesPB { + view_id: self.view_id.clone(), + deleted_groups: vec![deleted_group.group_id], + ..Default::default() + }; + notify_did_update_num_of_groups(&self.view_id, payload).await; + } } let changes = RowsChangePB::from_delete(row.id.clone().into_inner()); send_notification(&self.view_id, DatabaseNotification::DidUpdateViewRows) @@ -319,7 +308,7 @@ impl DatabaseViewEditor { .read() .await .as_ref()? - .groups() + .get_all_groups() .into_iter() .filter(|group| group.is_visible) .map(|group_data| GroupPB::from(group_data.clone())) @@ -371,6 +360,36 @@ impl DatabaseViewEditor { Ok(()) } + pub async fn v_create_group(&self, name: &str) -> FlowyResult<()> { + let mut old_field: Option<Field> = None; + let result = if let Some(controller) = self.group_controller.write().await.as_mut() { + let create_group_results = controller.create_group(name.to_string())?; + old_field = self.delegate.get_field(controller.field_id()); + create_group_results + } else { + (None, None) + }; + + if let Some(old_field) = old_field { + if let (Some(type_option_data), Some(payload)) = result { + self + .delegate + .update_field(&self.view_id, type_option_data, old_field) + .await?; + + let group_changes = GroupChangesPB { + view_id: self.view_id.clone(), + inserted_groups: vec![payload], + ..Default::default() + }; + + notify_did_update_num_of_groups(&self.view_id, group_changes).await; + } + } + + Ok(()) + } + pub async fn v_delete_group(&self, _params: DeleteGroupParams) -> FlowyResult<()> { Ok(()) } @@ -671,7 +690,7 @@ impl DatabaseViewEditor { .await?; let new_groups = new_group_controller - .groups() + .get_all_groups() .into_iter() .map(|group| GroupPB::from(group.clone())) .collect(); diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_type_option.rs index 666ff41792ea..26af4036d96f 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_type_option.rs @@ -25,15 +25,27 @@ pub trait SelectTypeOptionSharedAction: Send + Sync { /// If the option already exists, it will be updated. /// If the option does not exist, it will be inserted at the beginning. fn insert_option(&mut self, new_option: SelectOption) { + self.insert_option_at_index(new_option, None); + } + + fn insert_option_at_index(&mut self, new_option: SelectOption, new_index: Option<usize>) { let options = self.mut_options(); + let safe_new_index = new_index.map(|index| { + if index > options.len() { + options.len() + } else { + index + } + }); + if let Some(index) = options .iter() .position(|option| option.id == new_option.id || option.name == new_option.name) { options.remove(index); - options.insert(index, new_option); + options.insert(safe_new_index.unwrap_or(index), new_option); } else { - options.insert(0, new_option); + options.insert(safe_new_index.unwrap_or(0), new_option); } } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/action.rs b/frontend/rust-lib/flowy-database2/src/services/group/action.rs index 1c28fce8ad9b..dbc11f26bcad 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/action.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/action.rs @@ -54,7 +54,7 @@ pub trait GroupCustomize: Send + Sync { &mut self, row: &Row, cell_data: &<Self::GroupTypeOption as TypeOption>::CellData, - ) -> Vec<GroupRowsNotificationPB>; + ) -> (Option<GroupPB>, Vec<GroupRowsNotificationPB>); /// Move row from one group to another fn move_row( @@ -71,27 +71,71 @@ pub trait GroupCustomize: Send + Sync { ) -> Option<GroupPB> { None } + + fn generate_new_group( + &mut self, + _name: String, + ) -> FlowyResult<(Option<TypeOptionData>, Option<InsertedGroupPB>)> { + Ok((None, None)) + } } /// Defines the shared actions any group controller can perform. #[async_trait] pub trait GroupControllerOperation: Send + Sync { - /// The field that is used for grouping the rows + /// Returns the id of field that is being used to group the rows fn field_id(&self) -> &str; - /// Returns number of groups the current field has - fn groups(&self) -> Vec<&GroupData>; + /// Returns all of the groups currently managed by the controller + fn get_all_groups(&self) -> Vec<&GroupData>; - /// Returns the index and the group data with group_id + /// Returns the index and the group data with the given group id if it exists. + /// + /// * `group_id` - A string slice that is used to match the group fn get_group(&self, group_id: &str) -> Option<(usize, GroupData)>; - /// Separates the rows into different groups + /// Sort the rows into the different groups. + /// + /// * `rows`: rows to be inserted + /// * `field`: reference to the field being sorted (currently unused) fn fill_groups(&mut self, rows: &[&RowDetail], field: &Field) -> FlowyResult<()>; - /// Remove the group with from_group_id and insert it to the index with to_group_id + /// Create a new group, currently only supports single and multi-select. + /// + /// Returns a new type option data for the grouping field if it's altered. + /// + /// * `name`: name of the new group + fn create_group( + &mut self, + name: String, + ) -> FlowyResult<(Option<TypeOptionData>, Option<InsertedGroupPB>)>; + + /// Reorders the group in the group controller. + /// + /// * `from_group_id`: id of the group being moved + /// * `to_group_id`: id of the group whose index is the one at which the + /// reordered group will be placed fn move_group(&mut self, from_group_id: &str, to_group_id: &str) -> FlowyResult<()>; - /// Insert/Remove the row to the group if the corresponding cell data is changed + /// Adds a newly-created row to one or more suitable groups. + /// + /// Returns a changeset payload to be sent as a notification. + /// + /// * `row_detail`: the newly-created row + fn did_create_row( + &mut self, + row_detail: &RowDetail, + index: usize, + ) -> Vec<GroupRowsNotificationPB>; + + /// Called after a row's cell data is changed, this moves the row to the + /// correct group. It may also insert a new group and/or remove an old group. + /// + /// Returns the inserted and removed groups if necessary for notification. + /// + /// * `old_row_detail`: + /// * `row_detail`: + /// * `field`: fn did_update_group_row( &mut self, old_row_detail: &Option<RowDetail>, @@ -99,22 +143,31 @@ pub trait GroupControllerOperation: Send + Sync { field: &Field, ) -> FlowyResult<DidUpdateGroupRowResult>; - /// Remove the row from the group if the row gets deleted - fn did_delete_delete_row( - &mut self, - row: &Row, - field: &Field, - ) -> FlowyResult<DidMoveGroupRowResult>; + /// Called after the row is deleted, this removes the row from the group. + /// A group could be deleted as a result. + /// + /// Returns a the removed group when this occurs. + fn did_delete_row(&mut self, row: &Row) -> FlowyResult<DidMoveGroupRowResult>; - /// Move the row from one group to another group + /// Reorders a row within the current group or move the row to another group. + /// + /// * `context`: information about the row being moved and its destination fn move_group_row(&mut self, context: MoveGroupRowContext) -> FlowyResult<DidMoveGroupRowResult>; - /// Update the group if the corresponding field is changed + /// Updates the groups after a field change. (currently never does anything) + /// + /// * `field`: new changeset fn did_update_group_field(&mut self, field: &Field) -> FlowyResult<Option<GroupChangesPB>>; + /// Updates the name and/or visibility of groups. + /// + /// Returns a non-empty `TypeOptionData` when the changes require a change + /// in the field type option data. + /// + /// * `changesets`: list of changesets to be made to one or more groups async fn apply_group_changeset( &mut self, - changeset: &GroupChangesets, + changesets: &GroupChangesets, ) -> FlowyResult<TypeOptionData>; } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs b/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs index b71cdddf378b..97821aac5936 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs @@ -124,6 +124,7 @@ where /// Returns the no `status` group /// /// We take the `id` of the `field` as the no status group id + #[allow(dead_code)] pub(crate) fn get_no_status_group(&self) -> Option<&GroupData> { self.group_by_id.get(&self.field.id) } @@ -249,7 +250,7 @@ where /// /// # Arguments /// - /// * `generated_group_configs`: the generated groups contains a list of [GeneratedGroupConfig]. + /// * `generated_groups`: the generated groups contains a list of [GeneratedGroupConfig]. /// /// Each [FieldType] can implement the [GroupGenerator] trait in order to generate different /// groups. For example, the FieldType::Checkbox has the [CheckboxGroupGenerator] that implements diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller.rs index ab847755b21e..239e2910360f 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller.rs @@ -11,7 +11,7 @@ use serde::Serialize; use flowy_error::FlowyResult; use crate::entities::{ - FieldType, GroupChangesPB, GroupRowsNotificationPB, InsertedRowPB, RowMetaPB, + FieldType, GroupChangesPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB, RowMetaPB, }; use crate::services::cell::{get_cell_protobuf, CellProtobufBlobParser}; use crate::services::field::{default_type_option_data_from_type, TypeOption, TypeOptionCellData}; @@ -38,9 +38,6 @@ pub trait GroupController: GroupControllerOperation + Send + Sync { /// Called before the row was created. fn will_create_row(&mut self, cells: &mut Cells, field: &Field, group_id: &str); - - /// Called after the row was created. - fn did_create_row(&mut self, row_detail: &RowDetail, group_id: &str); } #[async_trait] @@ -184,7 +181,7 @@ where &self.grouping_field_id } - fn groups(&self) -> Vec<&GroupData> { + fn get_all_groups(&self) -> Vec<&GroupData> { self.context.groups() } @@ -233,10 +230,70 @@ where Ok(()) } + fn create_group( + &mut self, + name: String, + ) -> FlowyResult<(Option<TypeOptionData>, Option<InsertedGroupPB>)> { + self.generate_new_group(name) + } + fn move_group(&mut self, from_group_id: &str, to_group_id: &str) -> FlowyResult<()> { self.context.move_group(from_group_id, to_group_id) } + fn did_create_row( + &mut self, + row_detail: &RowDetail, + index: usize, + ) -> Vec<GroupRowsNotificationPB> { + let cell = match row_detail.row.cells.get(&self.grouping_field_id) { + None => self.placeholder_cell(), + Some(cell) => Some(cell.clone()), + }; + + let mut changesets: Vec<GroupRowsNotificationPB> = vec![]; + if let Some(cell) = cell { + let cell_data = <T as TypeOption>::CellData::from(&cell); + + let mut suitable_group_ids = vec![]; + + for group in self.get_all_groups() { + if self.can_group(&group.filter_content, &cell_data) { + suitable_group_ids.push(group.id.clone()); + let changeset = GroupRowsNotificationPB::insert( + group.id.clone(), + vec![InsertedRowPB { + row_meta: row_detail.into(), + index: Some(index as i32), + is_new: true, + }], + ); + changesets.push(changeset); + } + } + if !suitable_group_ids.is_empty() { + for group_id in suitable_group_ids.iter() { + if let Some(group) = self.context.get_mut_group(group_id) { + group.add_row(row_detail.clone()); + } + } + } else if let Some(no_status_group) = self.context.get_mut_no_status_group() { + no_status_group.add_row(row_detail.clone()); + let changeset = GroupRowsNotificationPB::insert( + no_status_group.id.clone(), + vec![InsertedRowPB { + row_meta: row_detail.into(), + index: Some(index as i32), + is_new: true, + }], + ); + changesets.push(changeset); + } + } + + changesets + } + fn did_update_group_row( &mut self, old_row_detail: &Option<RowDetail>, @@ -278,26 +335,21 @@ where Ok(result) } - fn did_delete_delete_row( - &mut self, - row: &Row, - _field: &Field, - ) -> FlowyResult<DidMoveGroupRowResult> { - // if the cell_rev is none, then the row must in the default group. + fn did_delete_row(&mut self, row: &Row) -> FlowyResult<DidMoveGroupRowResult> { let mut result = DidMoveGroupRowResult { deleted_group: None, row_changesets: vec![], }; + // early return if the row is not in the default group if let Some(cell) = row.cells.get(&self.grouping_field_id) { let cell_data = <T as TypeOption>::CellData::from(cell); if !cell_data.is_cell_empty() { - tracing::error!("did_delete_delete_row {:?}", cell); - result.row_changesets = self.delete_row(row, &cell_data); + (result.deleted_group, result.row_changesets) = self.delete_row(row, &cell_data); return Ok(result); } } - match self.context.get_no_status_group() { + match self.context.get_mut_no_status_group() { None => { tracing::error!("Unexpected None value. It should have the no status group"); }, @@ -305,6 +357,7 @@ where if !no_status_group.contains_row(&row.id) { tracing::error!("The row: {:?} should be in the no status group", row.id); } + no_status_group.remove_row(&row.id); result.row_changesets = vec![GroupRowsNotificationPB::delete( no_status_group.id.clone(), vec![row.id.clone().into_inner()], diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs index 490b80dca50e..64a7706cc265 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs @@ -3,7 +3,7 @@ use collab_database::fields::{Field, TypeOptionData}; use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail}; use serde::{Deserialize, Serialize}; -use crate::entities::{FieldType, GroupRowsNotificationPB, InsertedRowPB, RowMetaPB}; +use crate::entities::{FieldType, GroupPB, GroupRowsNotificationPB, InsertedRowPB, RowMetaPB}; use crate::services::cell::insert_checkbox_cell; use crate::services::field::{ CheckboxCellDataParser, CheckboxTypeOption, TypeOption, CHECK, UNCHECK, @@ -109,7 +109,7 @@ impl GroupCustomize for CheckboxGroupController { &mut self, row: &Row, _cell_data: &<Self::GroupTypeOption as TypeOption>::CellData, - ) -> Vec<GroupRowsNotificationPB> { + ) -> (Option<GroupPB>, Vec<GroupRowsNotificationPB>) { let mut changesets = vec![]; self.context.iter_mut_groups(|group| { let mut changeset = GroupRowsNotificationPB::new(group.id.clone()); @@ -122,7 +122,7 @@ impl GroupCustomize for CheckboxGroupController { changesets.push(changeset); } }); - changesets + (None, changesets) } fn move_row( @@ -155,12 +155,6 @@ impl GroupController for CheckboxGroupController { }, } } - - fn did_create_row(&mut self, row_detail: &RowDetail, group_id: &str) { - if let Some(group) = self.context.get_mut_group(group_id) { - group.add_row(row_detail.clone()) - } - } } pub struct CheckboxGroupBuilder(); diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs index 1797a270b930..316d628e28d3 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs @@ -178,8 +178,8 @@ impl GroupCustomize for DateGroupController { fn delete_row( &mut self, row: &Row, - _cell_data: &<Self::GroupTypeOption as TypeOption>::CellData, - ) -> Vec<GroupRowsNotificationPB> { + cell_data: &<Self::GroupTypeOption as TypeOption>::CellData, + ) -> (Option<GroupPB>, Vec<GroupRowsNotificationPB>) { let mut changesets = vec![]; self.context.iter_mut_groups(|group| { let mut changeset = GroupRowsNotificationPB::new(group.id.clone()); @@ -192,7 +192,23 @@ impl GroupCustomize for DateGroupController { changesets.push(changeset); } }); - changesets + + let setting_content = self.context.get_setting_content(); + let deleted_group = + match self + .context + .get_group(&group_id(cell_data, &self.type_option, &setting_content)) + { + Some((_, group)) if group.rows.len() == 1 => Some(group.clone()), + _ => None, + }; + + let deleted_group = deleted_group.map(|group| { + let _ = self.context.delete_group(&group.id); + group.into() + }); + + (deleted_group, changesets) } fn move_row( @@ -247,12 +263,6 @@ impl GroupController for DateGroupController { }, } } - - fn did_create_row(&mut self, row_detail: &RowDetail, group_id: &str) { - if let Some(group) = self.context.get_mut_group(group_id) { - group.add_row(row_detail.clone()) - } - } } pub struct DateGroupBuilder(); diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs index 19d02c028520..1de57413127a 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs @@ -6,7 +6,7 @@ use collab_database::rows::{Cells, Row, RowDetail}; use flowy_error::FlowyResult; -use crate::entities::GroupChangesPB; +use crate::entities::{GroupChangesPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB}; use crate::services::group::action::{ DidMoveGroupRowResult, DidUpdateGroupRowResult, GroupControllerOperation, }; @@ -44,7 +44,7 @@ impl GroupControllerOperation for DefaultGroupController { &self.field_id } - fn groups(&self) -> Vec<&GroupData> { + fn get_all_groups(&self) -> Vec<&GroupData> { vec![&self.group] } @@ -59,10 +59,34 @@ impl GroupControllerOperation for DefaultGroupController { Ok(()) } + fn create_group( + &mut self, + _name: String, + ) -> FlowyResult<(Option<TypeOptionData>, Option<InsertedGroupPB>)> { + Ok((None, None)) + } + fn move_group(&mut self, _from_group_id: &str, _to_group_id: &str) -> FlowyResult<()> { Ok(()) } + fn did_create_row( + &mut self, + row_detail: &RowDetail, + index: usize, + ) -> Vec<GroupRowsNotificationPB> { + self.group.add_row(row_detail.clone()); + + vec![GroupRowsNotificationPB::insert( + self.group.id.clone(), + vec![InsertedRowPB { + row_meta: row_detail.into(), + index: Some(index as i32), + is_new: true, + }], + )] + } + fn did_update_group_row( &mut self, _old_row_detail: &Option<RowDetail>, @@ -76,14 +100,15 @@ impl GroupControllerOperation for DefaultGroupController { }) } - fn did_delete_delete_row( - &mut self, - _row: &Row, - _field: &Field, - ) -> FlowyResult<DidMoveGroupRowResult> { + fn did_delete_row(&mut self, row: &Row) -> FlowyResult<DidMoveGroupRowResult> { + let mut changeset = GroupRowsNotificationPB::new(self.group.id.clone()); + if self.group.contains_row(&row.id) { + self.group.remove_row(&row.id); + changeset.deleted_rows.push(row.id.clone().into_inner()); + } Ok(DidMoveGroupRowResult { deleted_group: None, - row_changesets: vec![], + row_changesets: vec![changeset], }) } @@ -115,6 +140,4 @@ impl GroupController for DefaultGroupController { } fn will_create_row(&mut self, _cells: &mut Cells, _field: &Field, _group_id: &str) {} - - fn did_create_row(&mut self, _row_detail: &RowDetail, _group_id: &str) {} } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs index c90c5df3d37b..f7794a96240f 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs @@ -1,9 +1,10 @@ use async_trait::async_trait; use collab_database::fields::{Field, TypeOptionData}; use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail}; +use flowy_error::FlowyResult; use serde::{Deserialize, Serialize}; -use crate::entities::{FieldType, GroupRowsNotificationPB}; +use crate::entities::{FieldType, GroupPB, GroupRowsNotificationPB, InsertedGroupPB}; use crate::services::cell::insert_select_option_cell; use crate::services::field::{ MultiSelectTypeOption, SelectOption, SelectOptionCellDataParser, SelectTypeOptionSharedAction, @@ -13,7 +14,7 @@ use crate::services::group::action::GroupCustomize; use crate::services::group::controller::{BaseGroupController, GroupController}; use crate::services::group::{ add_or_remove_select_option_row, generate_select_option_groups, make_no_status_group, - move_group_row, remove_select_option_row, GeneratedGroups, GroupChangeset, GroupContext, + move_group_row, remove_select_option_row, GeneratedGroups, Group, GroupChangeset, GroupContext, GroupOperationInterceptor, GroupsBuilder, MoveGroupRowContext, }; @@ -69,14 +70,14 @@ impl GroupCustomize for MultiSelectGroupController { &mut self, row: &Row, cell_data: &<Self::GroupTypeOption as TypeOption>::CellData, - ) -> Vec<GroupRowsNotificationPB> { + ) -> (Option<GroupPB>, Vec<GroupRowsNotificationPB>) { let mut changesets = vec![]; self.context.iter_mut_status_groups(|group| { if let Some(changeset) = remove_select_option_row(group, cell_data, row) { changesets.push(changeset); } }); - changesets + (None, changesets) } fn move_row( @@ -92,6 +93,20 @@ impl GroupCustomize for MultiSelectGroupController { }); group_changeset } + + fn generate_new_group( + &mut self, + name: String, + ) -> FlowyResult<(Option<TypeOptionData>, Option<InsertedGroupPB>)> { + let mut new_type_option = self.type_option.clone(); + let new_select_option = self.type_option.create_option(&name); + new_type_option.insert_option(new_select_option.clone()); + + let new_group = Group::new(new_select_option.id, new_select_option.name); + let inserted_group_pb = self.context.add_new_group(new_group)?; + + Ok((Some(new_type_option.into()), Some(inserted_group_pb))) + } } impl GroupController for MultiSelectGroupController { @@ -106,12 +121,6 @@ impl GroupController for MultiSelectGroupController { }, } } - - fn did_create_row(&mut self, row_detail: &RowDetail, group_id: &str) { - if let Some(group) = self.context.get_mut_group(group_id) { - group.add_row(row_detail.clone()) - } - } } pub struct MultiSelectGroupBuilder; diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs index de94d8e1d0d4..a92c79c624bd 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs @@ -1,9 +1,10 @@ use async_trait::async_trait; use collab_database::fields::{Field, TypeOptionData}; use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail}; +use flowy_error::FlowyResult; use serde::{Deserialize, Serialize}; -use crate::entities::{FieldType, GroupRowsNotificationPB}; +use crate::entities::{FieldType, GroupPB, GroupRowsNotificationPB, InsertedGroupPB}; use crate::services::cell::insert_select_option_cell; use crate::services::field::{ SelectOption, SelectOptionCellDataParser, SelectTypeOptionSharedAction, SingleSelectTypeOption, @@ -14,8 +15,8 @@ use crate::services::group::controller::{BaseGroupController, GroupController}; use crate::services::group::controller_impls::select_option_controller::util::*; use crate::services::group::entities::GroupData; use crate::services::group::{ - make_no_status_group, GeneratedGroups, GroupChangeset, GroupContext, GroupOperationInterceptor, - GroupsBuilder, MoveGroupRowContext, + make_no_status_group, GeneratedGroups, Group, GroupChangeset, GroupContext, + GroupOperationInterceptor, GroupsBuilder, MoveGroupRowContext, }; #[derive(Default, Serialize, Deserialize)] @@ -70,14 +71,14 @@ impl GroupCustomize for SingleSelectGroupController { &mut self, row: &Row, cell_data: &<Self::GroupTypeOption as TypeOption>::CellData, - ) -> Vec<GroupRowsNotificationPB> { + ) -> (Option<GroupPB>, Vec<GroupRowsNotificationPB>) { let mut changesets = vec![]; self.context.iter_mut_status_groups(|group| { if let Some(changeset) = remove_select_option_row(group, cell_data, row) { changesets.push(changeset); } }); - changesets + (None, changesets) } fn move_row( @@ -93,6 +94,23 @@ impl GroupCustomize for SingleSelectGroupController { }); group_changeset } + + fn generate_new_group( + &mut self, + name: String, + ) -> FlowyResult<(Option<TypeOptionData>, Option<InsertedGroupPB>)> { + let mut new_type_option = self.type_option.clone(); + let new_select_option = self.type_option.create_option(&name); + new_type_option.insert_option_at_index( + new_select_option.clone(), + Some(new_type_option.options.len()), + ); + + let new_group = Group::new(new_select_option.id, new_select_option.name); + let inserted_group_pb = self.context.add_new_group(new_group)?; + + Ok((Some(new_type_option.into()), Some(inserted_group_pb))) + } } impl GroupController for SingleSelectGroupController { @@ -108,12 +126,6 @@ impl GroupController for SingleSelectGroupController { }, } } - - fn did_create_row(&mut self, row_detail: &RowDetail, group_id: &str) { - if let Some(group) = self.context.get_mut_group(group_id) { - group.add_row(row_detail.clone()) - } - } } pub struct SingleSelectGroupBuilder(); diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs index e68b1be0edb6..4ed745cad516 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs @@ -185,6 +185,7 @@ pub fn make_inserted_cell(group_id: &str, field: &Field) -> Option<Cell> { }, } } + pub fn generate_select_option_groups( _field_id: &str, options: &[SelectOption], diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs index 4e85557101da..475b871ffcc6 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs @@ -128,8 +128,8 @@ impl GroupCustomize for URLGroupController { fn delete_row( &mut self, row: &Row, - _cell_data: &<Self::GroupTypeOption as TypeOption>::CellData, - ) -> Vec<GroupRowsNotificationPB> { + cell_data: &<Self::GroupTypeOption as TypeOption>::CellData, + ) -> (Option<GroupPB>, Vec<GroupRowsNotificationPB>) { let mut changesets = vec![]; self.context.iter_mut_groups(|group| { let mut changeset = GroupRowsNotificationPB::new(group.id.clone()); @@ -142,7 +142,18 @@ impl GroupCustomize for URLGroupController { changesets.push(changeset); } }); - changesets + + let deleted_group = match self.context.get_group(&cell_data.data) { + Some((_, group)) if group.rows.len() == 1 => Some(group.clone()), + _ => None, + }; + + let deleted_group = deleted_group.map(|group| { + let _ = self.context.delete_group(&group.id); + group.into() + }); + + (deleted_group, changesets) } fn move_row( @@ -190,12 +201,6 @@ impl GroupController for URLGroupController { }, } } - - fn did_create_row(&mut self, row_detail: &RowDetail, group_id: &str) { - if let Some(group) = self.context.get_mut_group(group_id) { - group.add_row(row_detail.clone()) - } - } } pub struct URLGroupGenerator(); diff --git a/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs index a8a39c645ad7..330ecc9044ee 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs @@ -67,6 +67,9 @@ pub enum GroupScript { group_id: String, group_name: String, }, + CreateGroup { + name: String, + }, } pub struct DatabaseGroupTest { @@ -269,6 +272,11 @@ impl DatabaseGroupTest { assert_eq!(group_id, group.group_id, "group index: {}", group_index); assert_eq!(group_name, group.group_name, "group index: {}", group_index); }, + GroupScript::CreateGroup { name } => self + .editor + .create_group(&self.view_id, &name) + .await + .unwrap(), } } diff --git a/frontend/rust-lib/flowy-database2/tests/database/group_test/test.rs b/frontend/rust-lib/flowy-database2/tests/database/group_test/test.rs index d9fb97a86518..119986b04bee 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/group_test/test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/group_test/test.rs @@ -486,3 +486,19 @@ async fn group_group_by_other_field() { ]; test.run_scripts(scripts).await; } + +#[tokio::test] +async fn group_manual_create_new_group() { + let mut test = DatabaseGroupTest::new().await; + let new_group_name = "Resumed"; + let scripts = vec![ + AssertGroupCount(4), + CreateGroup { + name: new_group_name.to_string(), + }, + AssertGroupCount(5), + ]; + test.run_scripts(scripts).await; + let new_group = test.group_at_index(4).await; + assert_eq!(new_group.group_name, new_group_name); +} From 3e6529aeb8be29273c4e7591e2174c08e0027f07 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" <lucas.xu@appflowy.io> Date: Mon, 6 Nov 2023 16:44:21 +0800 Subject: [PATCH 30/56] chore: add Roboto Mono font family and its regular and italic variants as assets (#3876) --- .../google_fonts/Roboto_Mono/LICENSE.txt | 202 ++++++++++++++++++ .../Roboto_Mono/RobotoMono-Italic.ttf | Bin 0 -> 94372 bytes .../Roboto_Mono/RobotoMono-Regular.ttf | Bin 0 -> 87236 bytes frontend/appflowy_flutter/pubspec.yaml | 6 + 4 files changed, 208 insertions(+) create mode 100644 frontend/appflowy_flutter/assets/google_fonts/Roboto_Mono/LICENSE.txt create mode 100644 frontend/appflowy_flutter/assets/google_fonts/Roboto_Mono/RobotoMono-Italic.ttf create mode 100644 frontend/appflowy_flutter/assets/google_fonts/Roboto_Mono/RobotoMono-Regular.ttf diff --git a/frontend/appflowy_flutter/assets/google_fonts/Roboto_Mono/LICENSE.txt b/frontend/appflowy_flutter/assets/google_fonts/Roboto_Mono/LICENSE.txt new file mode 100644 index 000000000000..75b52484ea47 --- /dev/null +++ b/frontend/appflowy_flutter/assets/google_fonts/Roboto_Mono/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/frontend/appflowy_flutter/assets/google_fonts/Roboto_Mono/RobotoMono-Italic.ttf b/frontend/appflowy_flutter/assets/google_fonts/Roboto_Mono/RobotoMono-Italic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..61e5303325a1b4d196d3ba631ac4681b1fdfb7c9 GIT binary patch literal 94372 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 b*}N(wj2m$R)v})5rslno&Vv!&ZEE}jf<w$} literal 0 HcmV?d00001 diff --git a/frontend/appflowy_flutter/assets/google_fonts/Roboto_Mono/RobotoMono-Regular.ttf b/frontend/appflowy_flutter/assets/google_fonts/Roboto_Mono/RobotoMono-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..6df2b253603094de7f39886aae03181c686e375b GIT binary patch literal 87236 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 WiD&}1U9w};%2Kl9!gJKh^8W$jCY;;= literal 0 HcmV?d00001 diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 8e38938ddafb..86cfd59a8501 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -196,6 +196,12 @@ flutter: weight: 800 - asset: assets/google_fonts/Poppins/Poppins-ExtraBold.ttf weight: 900 + - family: RobotoMono + fonts: + - asset: assets/google_fonts/Roboto_Mono/RobotoMono-Regular.ttf + - asset: assets/google_fonts/Roboto_Mono/RobotoMono-Italic.ttf + style: italic + # To add assets to your application, add an assets section, like this: assets: From 8116ea1dbabfa70f1b02c9b8a83c784e05f3a27b Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" <lucas.xu@appflowy.io> Date: Tue, 7 Nov 2023 15:24:32 +0800 Subject: [PATCH 31/56] feat: adjust math_equation block and image block on mobile platform (#3890) * feat: add image toolbar entry * feat: add ... buttos on math_equation and image block * fix: review issues * feat: add copy link and save image to gallery * feat: support redo / undo on mobile toolbar --- frontend/appflowy_flutter/ios/Podfile.lock | 12 + .../appflowy_flutter/ios/Runner/Info.plist | 4 +- .../mobile/application/mobile_theme_data.dart | 2 + .../presentation/base/mobile_view_page.dart | 12 +- .../bottom_sheet_action_widget.dart | 1 + .../bottom_sheet_block_action_widget.dart | 78 ++++++ .../bottom_sheet/bottom_sheet_view_page.dart | 50 ++-- .../lib/plugins/document/document_page.dart | 22 +- .../presentation/editor_configuration.dart | 4 +- .../document/presentation/editor_page.dart | 6 +- .../actions/mobile_block_action_buttons.dart | 107 +++++++++ .../copy_and_paste/clipboard_service.dart | 6 + .../image/custom_image_block_component.dart | 222 ++++++++++++++---- .../image/embed_image_url_widget.dart | 1 + .../image/image_placeholder.dart | 155 ++++++++---- .../image/mobile_image_toolbar_item.dart | 17 ++ .../image/unsupport_image_widget.dart | 43 ++++ .../image/upload_image_file_widget.dart | 43 ++-- .../image/upload_image_menu.dart | 15 +- .../math_equation_block_component.dart | 81 +++++-- .../mobile_math_eqaution_toolbar_item.dart | 43 ++++ .../presentation/editor_plugins/plugins.dart | 4 + .../undo_redo/redo_mobile_toolbar_item.dart | 9 + .../undo_redo/undo_mobile_toolbar_item.dart | 9 + .../workspace/presentation/home/toast.dart | 14 +- .../lib/file_picker/file_picker_service.dart | 4 +- .../lib/style_widget/button.dart | 18 +- frontend/appflowy_flutter/pubspec.lock | 123 +++++++++- frontend/appflowy_flutter/pubspec.yaml | 7 +- .../flowy_icons/32x/m_toolbar_imae.svg | 3 + frontend/resources/translations/en.json | 14 +- 31 files changed, 928 insertions(+), 201 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/mobile_image_toolbar_item.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/unsupport_image_widget.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/mobile_math_eqaution_toolbar_item.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/redo_mobile_toolbar_item.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/undo_mobile_toolbar_item.dart create mode 100644 frontend/resources/flowy_icons/32x/m_toolbar_imae.svg diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index 47e63e5fe9f3..84858fb29ab8 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -48,6 +48,10 @@ PODS: - fluttertoast (0.0.2): - Flutter - Toast + - image_gallery_saver (2.0.2): + - Flutter + - image_picker_ios (0.0.1): + - Flutter - integration_test (0.0.1): - Flutter - irondash_engine_context (0.0.1): @@ -86,6 +90,8 @@ DEPENDENCIES: - flowy_infra_ui (from `.symlinks/plugins/flowy_infra_ui/ios`) - Flutter (from `Flutter`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) + - image_gallery_saver (from `.symlinks/plugins/image_gallery_saver/ios`) + - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) @@ -123,6 +129,10 @@ EXTERNAL SOURCES: :path: Flutter fluttertoast: :path: ".symlinks/plugins/fluttertoast/ios" + image_gallery_saver: + :path: ".symlinks/plugins/image_gallery_saver/ios" + image_picker_ios: + :path: ".symlinks/plugins/image_picker_ios/ios" integration_test: :path: ".symlinks/plugins/integration_test/ios" irondash_engine_context: @@ -155,6 +165,8 @@ SPEC CHECKSUMS: flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c + image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb + image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5 integration_test: 13825b8a9334a850581300559b8839134b124670 irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9 package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7 diff --git a/frontend/appflowy_flutter/ios/Runner/Info.plist b/frontend/appflowy_flutter/ios/Runner/Info.plist index ce7ee6a24137..91ee44ca33c9 100644 --- a/frontend/appflowy_flutter/ios/Runner/Info.plist +++ b/frontend/appflowy_flutter/ios/Runner/Info.plist @@ -2,8 +2,10 @@ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> + <key>NSCameraUsageDescription</key> + <string>AppFlowy requires access to the camera.</string> <key>NSPhotoLibraryUsageDescription</key> - <string>This app requires access to the photo library.</string> + <string>AppFlowy requires access to the photo library.</string> <key>CADisableMinimumFrameDurationOnPhone</key> <true/> <key>CFBundleDevelopmentRegion</key> diff --git a/frontend/appflowy_flutter/lib/mobile/application/mobile_theme_data.dart b/frontend/appflowy_flutter/lib/mobile/application/mobile_theme_data.dart index 0b4557f9feae..7d24ebc0a2fa 100644 --- a/frontend/appflowy_flutter/lib/mobile/application/mobile_theme_data.dart +++ b/frontend/appflowy_flutter/lib/mobile/application/mobile_theme_data.dart @@ -34,6 +34,7 @@ ThemeData getMobileThemeData( //Snack bar surface: Colors.white, onSurface: _onSurfaceColor, // text/body color + surfaceVariant: const Color.fromARGB(255, 216, 216, 216), ) : ColorScheme( brightness: brightness, @@ -223,6 +224,7 @@ ThemeData getMobileThemeData( ), ), colorScheme: mobileColorTheme, + indicatorColor: Colors.blue, extensions: [ AFThemeExtension( warning: theme.yellow, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart index bf798a11056a..99fef7559eb8 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart @@ -1,7 +1,8 @@ import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart'; import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; +import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart'; +import 'package:appflowy/plugins/document/document_page.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; @@ -178,12 +179,21 @@ class _MobileViewPageState extends State<MobileViewPage> { context.read<FavoriteBloc>().add(FavoriteEvent.toggle(view)); break; case MobileViewBottomSheetBodyAction.undo: + context.dispatchNotification( + const EditorNotification(type: EditorNotificationType.redo), + ); + context.pop(); + break; case MobileViewBottomSheetBodyAction.redo: + context.pop(); + context.dispatchNotification(EditorNotification.redo()); + break; case MobileViewBottomSheetBodyAction.helpCenter: // unimplemented context.pop(); break; case MobileViewBottomSheetBodyAction.rename: + // no need to implement, rename is handled by the onRename callback. throw UnimplementedError(); } }, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart index b4aa9deee101..2bc843cc0a44 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart @@ -24,6 +24,7 @@ class BottomSheetActionWidget extends StatelessWidget { icon: FlowySvg( svg, size: const Size.square(22.0), + blendMode: BlendMode.dst, color: iconColor, ), label: Text(text), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.dart new file mode 100644 index 000000000000..044e484fe9f4 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.dart @@ -0,0 +1,78 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +enum BlockActionBottomSheetType { + delete, + duplicate, + insertAbove, + insertBelow, +} + +// Only works on mobile. +class BlockActionBottomSheet extends StatelessWidget { + const BlockActionBottomSheet({ + super.key, + required this.onAction, + this.extendActionWidgets = const [], + }); + + final void Function(BlockActionBottomSheetType layout) onAction; + final List<Widget> extendActionWidgets; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + // insert above, insert below + Row( + children: [ + Expanded( + child: BottomSheetActionWidget( + svg: FlowySvgs.arrow_up_s, + text: LocaleKeys.button_insertAbove.tr(), + onTap: () => onAction(BlockActionBottomSheetType.insertAbove), + ), + ), + const HSpace(8), + Expanded( + child: BottomSheetActionWidget( + svg: FlowySvgs.arrow_down_s, + text: LocaleKeys.button_insertBelow.tr(), + onTap: () => onAction(BlockActionBottomSheetType.insertBelow), + ), + ), + ], + ), + const VSpace(8), + + // duplicate, delete + Row( + children: [ + Expanded( + child: BottomSheetActionWidget( + svg: FlowySvgs.m_duplicate_m, + text: LocaleKeys.button_duplicate.tr(), + onTap: () => onAction(BlockActionBottomSheetType.duplicate), + ), + ), + const HSpace(8), + Expanded( + child: BottomSheetActionWidget( + svg: FlowySvgs.m_delete_m, + text: LocaleKeys.button_delete.tr(), + onTap: () => onAction(BlockActionBottomSheetType.delete), + ), + ), + ], + ), + const VSpace(8), + + ...extendActionWidgets, + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart index 4317f3afa2c2..d1dd3527c32e 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart @@ -121,31 +121,31 @@ class MobileViewBottomSheetBody extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // undo, redo - Row( - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: BottomSheetActionWidget( - svg: FlowySvgs.m_undo_m, - text: LocaleKeys.toolbar_undo.tr(), - onTap: () => onAction( - MobileViewBottomSheetBodyAction.undo, - ), - ), - ), - const HSpace(8), - Expanded( - child: BottomSheetActionWidget( - svg: FlowySvgs.m_redo_m, - text: LocaleKeys.toolbar_redo.tr(), - onTap: () => onAction( - MobileViewBottomSheetBodyAction.redo, - ), - ), - ), - ], - ), - const VSpace(8), + // Row( + // mainAxisSize: MainAxisSize.max, + // children: [ + // Expanded( + // child: BottomSheetActionWidget( + // svg: FlowySvgs.m_undo_m, + // text: LocaleKeys.toolbar_undo.tr(), + // onTap: () => onAction( + // MobileViewBottomSheetBodyAction.undo, + // ), + // ), + // ), + // const HSpace(8), + // Expanded( + // child: BottomSheetActionWidget( + // svg: FlowySvgs.m_redo_m, + // text: LocaleKeys.toolbar_redo.tr(), + // onTap: () => onAction( + // MobileViewBottomSheetBodyAction.redo, + // ), + // ), + // ), + // ], + // ), + // const VSpace(8), // rename, duplicate Row( diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart index 607f139d3283..556a5d5a719e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart @@ -26,6 +26,22 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:path/path.dart' as p; +enum EditorNotificationType { + undo, + redo, +} + +class EditorNotification extends Notification { + const EditorNotification({ + required this.type, + }); + + EditorNotification.undo() : type = EditorNotificationType.undo; + EditorNotification.redo() : type = EditorNotificationType.redo; + + final EditorNotificationType type; +} + class DocumentPage extends StatefulWidget { const DocumentPage({ super.key, @@ -95,7 +111,10 @@ class _DocumentPageState extends State<DocumentPage> { ); } else { editorState = documentBloc.editorState!; - return _buildEditorPage(context, state); + return _buildEditorPage( + context, + state, + ); } }, ), @@ -116,6 +135,7 @@ class _DocumentPageState extends State<DocumentPage> { ), header: _buildCoverAndIcon(context), ); + return Column( children: [ if (state.isDeleted) _buildBanner(context), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart index 5d0394291e1d..d575aa29aa9f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart @@ -118,9 +118,7 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({ height: 28.0, ), MathEquationBlockKeys.type: MathEquationBlockComponentBuilder( - configuration: configuration.copyWith( - padding: (_) => const EdgeInsets.symmetric(vertical: 20), - ), + configuration: configuration, ), CodeBlockKeys.type: CodeBlockComponentBuilder( configuration: configuration.copyWith( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index 99f338bf807e..bd774e4669a5 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -245,7 +245,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> { contextMenuItems: customContextMenuItems, // customize the header and footer. header: widget.header, - footer: const VSpace(200), + footer: VSpace(PlatformExtension.isDesktopOrWeb ? 200 : 400), ), ); @@ -285,7 +285,11 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> { linkMobileToolbarItem, quoteMobileToolbarItem, dividerMobileToolbarItem, + imageMobileToolbarItem, + mathEquationMobileToolbarItem, codeMobileToolbarItem, + undoMobileToolbarItem, + redoMobileToolbarItem, ], ), ], diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart new file mode 100644 index 000000000000..b5fab176944c --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart @@ -0,0 +1,107 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.dart'; +import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +/// The ... button shows on the top right corner of a block. +/// +/// Default actions are: +/// - delete +/// - duplicate +/// - insert above +/// - insert below +/// +/// Only works on mobile. +class MobileBlockActionButtons extends StatelessWidget { + const MobileBlockActionButtons({ + super.key, + this.extendActionWidgets = const [], + required this.node, + required this.editorState, + required this.child, + }); + + final Node node; + final EditorState editorState; + final List<Widget> extendActionWidgets; + final Widget child; + + @override + Widget build(BuildContext context) { + if (!PlatformExtension.isMobile) { + return child; + } + + const padding = 5.0; + return Stack( + children: [ + child, + Positioned( + top: padding, + right: padding, + child: FlowyIconButton( + icon: const FlowySvg( + FlowySvgs.three_dots_s, + ), + width: 20.0, + onPressed: () => _showBottomSheet(context), + ), + ), + ], + ); + } + + void _showBottomSheet(BuildContext context) { + showFlowyMobileBottomSheet( + context, + title: LocaleKeys.document_plugins_action.tr(), + builder: (context) { + return BlockActionBottomSheet( + extendActionWidgets: extendActionWidgets, + onAction: (action) async { + context.pop(); + + final transaction = editorState.transaction; + switch (action) { + case BlockActionBottomSheetType.delete: + transaction.deleteNode(node); + break; + case BlockActionBottomSheetType.duplicate: + transaction.insertNode( + node.path.next, + node.copyWith(), + ); + break; + case BlockActionBottomSheetType.insertAbove: + case BlockActionBottomSheetType.insertBelow: + final path = action == BlockActionBottomSheetType.insertAbove + ? node.path + : node.path.next; + transaction + ..insertNode( + path, + paragraphNode(), + ) + ..afterSelection = Selection.collapsed( + Position( + path: path, + ), + ); + break; + default: + } + + if (transaction.operations.isNotEmpty) { + await editorState.apply(transaction); + } + }, + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart index 27d07450930b..a30cf4451f99 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart @@ -74,6 +74,12 @@ class ClipboardService { await ClipboardWriter.instance.write([item]); } + Future<void> setPlainText(String text) async { + await ClipboardWriter.instance.write([ + DataWriterItem()..add(Formats.plainText(text)), + ]); + } + Future<ClipboardServiceData> getData() async { final reader = await ClipboardReader.readClipboard(); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart index 0e3537179267..1112343de8fd 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart @@ -1,7 +1,24 @@ +import 'dart:io'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsupport_image_widget.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:http/http.dart'; +import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:provider/provider.dart'; +import 'package:string_validator/string_validator.dart'; const kImagePlaceholderKey = 'imagePlaceholderKey'; @@ -96,25 +113,30 @@ class CustomImageBlockComponentState extends State<CustomImageBlockComponent> final height = attributes[ImageBlockKeys.height]?.toDouble(); final imagePlaceholderKey = node.extraInfos?[kImagePlaceholderKey]; - Widget child = src.isEmpty - ? ImagePlaceholder( - key: imagePlaceholderKey is GlobalKey ? imagePlaceholderKey : null, - node: node, - ) - : ResizableImage( - src: src, - width: width, - height: height, - editable: editorState.editable, - alignment: alignment, - onResize: (width) { - final transaction = editorState.transaction - ..updateNode(node, { - ImageBlockKeys.width: width, - }); - editorState.apply(transaction); - }, - ); + Widget child; + if (src.isEmpty) { + child = ImagePlaceholder( + key: imagePlaceholderKey is GlobalKey ? imagePlaceholderKey : null, + node: node, + ); + } else if (!_checkIfURLIsValid(src)) { + child = const UnSupportImageWidget(); + } else { + child = ResizableImage( + src: src, + width: width, + height: height, + editable: editorState.editable, + alignment: alignment, + onResize: (width) { + final transaction = editorState.transaction + ..updateNode(node, { + ImageBlockKeys.width: width, + }); + editorState.apply(transaction); + }, + ); + } child = BlockSelectionContainer( node: node, @@ -139,40 +161,51 @@ class CustomImageBlockComponentState extends State<CustomImageBlockComponent> ); } - if (widget.showMenu && widget.menuBuilder != null) { - child = MouseRegion( - onEnter: (_) => showActionsNotifier.value = true, - onExit: (_) { - if (!alwaysShowMenu) { - showActionsNotifier.value = false; - } - }, - hitTestBehavior: HitTestBehavior.opaque, - opaque: false, - child: ValueListenableBuilder<bool>( - valueListenable: showActionsNotifier, - builder: (context, value, child) { - final url = node.attributes[ImageBlockKeys.url]; - return Stack( - children: [ - BlockSelectionContainer( - node: node, - delegate: this, - listenable: editorState.selectionNotifier, - cursorColor: editorState.editorStyle.cursorColor, - selectionColor: editorState.editorStyle.selectionColor, - child: child!, - ), - if (value && url.isNotEmpty == true) - widget.menuBuilder!( - widget.node, - this, - ), - ], - ); + // show a hover menu on desktop or web + if (PlatformExtension.isDesktopOrWeb) { + if (widget.showMenu && widget.menuBuilder != null) { + child = MouseRegion( + onEnter: (_) => showActionsNotifier.value = true, + onExit: (_) { + if (!alwaysShowMenu) { + showActionsNotifier.value = false; + } }, - child: child, - ), + hitTestBehavior: HitTestBehavior.opaque, + opaque: false, + child: ValueListenableBuilder<bool>( + valueListenable: showActionsNotifier, + builder: (context, value, child) { + final url = node.attributes[ImageBlockKeys.url]; + return Stack( + children: [ + BlockSelectionContainer( + node: node, + delegate: this, + listenable: editorState.selectionNotifier, + cursorColor: editorState.editorStyle.cursorColor, + selectionColor: editorState.editorStyle.selectionColor, + child: child!, + ), + if (value && url.isNotEmpty == true) + widget.menuBuilder!( + widget.node, + this, + ), + ], + ); + }, + child: child, + ), + ); + } + } else { + // show a fixed menu on mobile + child = MobileBlockActionButtons( + node: node, + editorState: editorState, + extendActionWidgets: _buildExtendActionWidgets(context), + child: child, ); } @@ -246,4 +279,89 @@ class CustomImageBlockComponentState extends State<CustomImageBlockComponent> bool shiftWithBaseOffset = false, }) => _renderBox!.localToGlobal(offset); + + // only used on mobile platform + List<Widget> _buildExtendActionWidgets(BuildContext context) { + final url = widget.node.attributes[ImageBlockKeys.url]; + if (!_checkIfURLIsValid(url)) { + return []; + } + + return [ + Row( + children: [ + Expanded( + child: BottomSheetActionWidget( + svg: FlowySvgs.copy_s, + text: LocaleKeys.editor_copyLink.tr(), + onTap: () async { + context.pop(); + showSnackBarMessage( + context, + LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(), + ); + await getIt<ClipboardService>().setPlainText(url); + }, + ), + ), + const HSpace(8.0), + Expanded( + child: BottomSheetActionWidget( + svg: FlowySvgs.image_placeholder_s, + text: LocaleKeys.document_imageBlock_saveImageToGallery.tr(), + onTap: () async { + context.pop(); + Uint8List? bytes; + if (isURL(url)) { + // network image + final result = await get(Uri.parse(url)); + if (result.statusCode == 200) { + bytes = result.bodyBytes; + } + } else { + final file = File(url); + bytes = await file.readAsBytes(); + } + if (bytes != null) { + await ImageGallerySaver.saveImage(bytes); + if (context.mounted) { + showSnackBarMessage( + context, + LocaleKeys.document_imageBlock_successToAddImageToGallery + .tr(), + ); + } + } else { + if (context.mounted) { + showSnackBarMessage( + context, + LocaleKeys.document_imageBlock_failedToAddImageToGallery + .tr(), + ); + } + } + }, + ), + ), + ], + ), + const VSpace(8), + ]; + } + + bool _checkIfURLIsValid(dynamic url) { + if (url is! String) { + return false; + } + + if (url.isEmpty) { + return false; + } + + if (!isURL(url) && !File(url).existsSync()) { + return false; + } + + return true; + } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/embed_image_url_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/embed_image_url_widget.dart index 5420d949db2a..04e33fbbc525 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/embed_image_url_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/embed_image_url_widget.dart @@ -32,6 +32,7 @@ class _EmbedImageUrlWidgetState extends State<EmbedImageUrlWidget> { SizedBox( width: 160, child: FlowyButton( + showDefaultBoxDecorationOnMobile: true, margin: const EdgeInsets.all(8.0), text: FlowyText( LocaleKeys.document_imageBlock_embedLink_label.tr(), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart index d77b864d57a0..bbc17f472dcb 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/settings/application_data_storage.dart'; @@ -15,6 +16,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; import 'package:http/http.dart'; import 'package:path/path.dart' as p; import 'package:string_validator/string_validator.dart'; @@ -37,65 +39,114 @@ class ImagePlaceholderState extends State<ImagePlaceholder> { @override Widget build(BuildContext context) { - return AppFlowyPopover( - controller: controller, - direction: PopoverDirection.bottomWithCenterAligned, - constraints: const BoxConstraints( - maxWidth: 540, - maxHeight: 360, - minHeight: 80, + final Widget child = DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceVariant, + borderRadius: BorderRadius.circular(4), ), - clickHandler: PopoverClickHandler.gestureDetector, - popupBuilder: (context) { - return UploadImageMenu( - onSelectedLocalImage: (path) { - controller.close(); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { - await insertLocalImage(path); - }); - }, - onSelectedAIImage: (url) { - controller.close(); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { - await insertAIImage(url); - }); - }, - onSelectedNetworkImage: (url) { - controller.close(); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { - await insertNetworkImage(url); - }); - }, - ); - }, - child: DecoratedBox( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant, + child: FlowyHover( + style: HoverStyle( borderRadius: BorderRadius.circular(4), ), - child: FlowyHover( - style: HoverStyle( - borderRadius: BorderRadius.circular(4), - ), - child: SizedBox( - height: 48, - child: Row( - children: [ - const HSpace(10), - const FlowySvg( - FlowySvgs.image_placeholder_s, - size: Size.square(24), - ), - const HSpace(10), - FlowyText( - LocaleKeys.document_plugins_image_addAnImage.tr(), - ), - ], - ), + child: SizedBox( + height: 52, + child: Row( + children: [ + const HSpace(10), + const FlowySvg( + FlowySvgs.image_placeholder_s, + size: Size.square(24), + ), + const HSpace(10), + FlowyText( + LocaleKeys.document_plugins_image_addAnImage.tr(), + ), + ], ), ), ), ); + + if (PlatformExtension.isDesktopOrWeb) { + return AppFlowyPopover( + controller: controller, + direction: PopoverDirection.bottomWithCenterAligned, + constraints: const BoxConstraints( + maxWidth: 540, + maxHeight: 360, + minHeight: 80, + ), + clickHandler: PopoverClickHandler.gestureDetector, + popupBuilder: (context) { + return UploadImageMenu( + onSelectedLocalImage: (path) { + controller.close(); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + await insertLocalImage(path); + }); + }, + onSelectedAIImage: (url) { + controller.close(); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + await insertAIImage(url); + }); + }, + onSelectedNetworkImage: (url) { + controller.close(); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + await insertNetworkImage(url); + }); + }, + ); + }, + child: child, + ); + } else { + return GestureDetector( + onTap: () { + showUploadImageMenu(); + }, + child: child, + ); + } + } + + void showUploadImageMenu() { + if (PlatformExtension.isDesktopOrWeb) { + controller.show(); + } else { + showFlowyMobileBottomSheet( + context, + title: LocaleKeys.editor_image.tr(), + builder: (context) { + return ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 340, + minHeight: 80, + ), + child: UploadImageMenu( + supportTypes: const [ + UploadImageType.local, + UploadImageType.url, + UploadImageType.unsplash, + ], + onSelectedLocalImage: (path) async { + context.pop(); + await insertLocalImage(path); + }, + onSelectedAIImage: (url) async { + context.pop(); + await insertAIImage(url); + }, + onSelectedNetworkImage: (url) async { + context.pop(); + await insertNetworkImage(url); + }, + ), + ); + }, + ); + } } Future<void> insertLocalImage(String? url) async { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/mobile_image_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/mobile_image_toolbar_item.dart new file mode 100644 index 000000000000..10a756c02fe8 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/mobile_image_toolbar_item.dart @@ -0,0 +1,17 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; + +final imageMobileToolbarItem = MobileToolbarItem.action( + itemIcon: const FlowySvg(FlowySvgs.m_toolbar_imae_lg), + actionHandler: (editorState, selection) async { + final imagePlaceholderKey = GlobalKey<ImagePlaceholderState>(); + await editorState.insertEmptyImageBlock(imagePlaceholderKey); + + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + imagePlaceholderKey.currentState?.showUploadImageMenu(); + }); + }, +); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/unsupport_image_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/unsupport_image_widget.dart new file mode 100644 index 000000000000..3403a1ff31b5 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/unsupport_image_widget.dart @@ -0,0 +1,43 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flutter/material.dart'; + +class UnSupportImageWidget extends StatelessWidget { + const UnSupportImageWidget({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceVariant, + borderRadius: BorderRadius.circular(4), + ), + child: FlowyHover( + style: HoverStyle( + borderRadius: BorderRadius.circular(4), + ), + child: SizedBox( + height: 52, + child: Row( + children: [ + const HSpace(10), + const FlowySvg( + FlowySvgs.image_placeholder_s, + size: Size.square(24), + ), + const HSpace(10), + FlowyText( + LocaleKeys.document_imageBlock_unableToLoadImage.tr(), + ), + ], + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_file_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_file_widget.dart index 40d1daf0724f..71c95ac4450a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_file_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_file_widget.dart @@ -1,10 +1,12 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/file_picker/file_picker_service.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; class UploadImageFileWidget extends StatelessWidget { const UploadImageFileWidget({ @@ -19,31 +21,34 @@ class UploadImageFileWidget extends StatelessWidget { @override Widget build(BuildContext context) { return FlowyHover( - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onTapDown: (_) async { - final result = await getIt<FilePickerService>().pickFiles( - dialogTitle: '', - allowMultiple: false, - type: FileType.image, - allowedExtensions: allowedExtensions, - ); - onPickFile(result?.files.firstOrNull?.path); - }, - child: Container( + child: FlowyButton( + showDefaultBoxDecorationOnMobile: true, + text: Container( + margin: const EdgeInsets.all(4.0), alignment: Alignment.center, - padding: const EdgeInsets.symmetric(vertical: 8.0), - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).colorScheme.surfaceVariant, - width: 1.0, - ), - ), child: FlowyText( LocaleKeys.document_imageBlock_upload_placeholder.tr(), ), ), + onTap: _uploadImage, ), ); } + + Future<void> _uploadImage() async { + if (PlatformExtension.isDesktopOrWeb) { + // on desktop, the users can pick a image file from folder + final result = await getIt<FilePickerService>().pickFiles( + dialogTitle: '', + allowMultiple: false, + type: FileType.image, + allowedExtensions: allowedExtensions, + ); + onPickFile(result?.files.firstOrNull?.path); + } else { + // on mobile, the users can pick a image file from camera or image library + final result = await ImagePicker().pickImage(source: ImageSource.gallery); + onPickFile(result?.path); + } + } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart index 0b8eed1f98d6..e7a9bfc7de68 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart @@ -5,6 +5,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/image/stab import 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_file_widget.dart'; import 'package:appflowy/user/application/user_service.dart'; +import 'package:appflowy/util/platform_extension.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; @@ -39,19 +40,21 @@ class UploadImageMenu extends StatefulWidget { required this.onSelectedLocalImage, required this.onSelectedAIImage, required this.onSelectedNetworkImage, + this.supportTypes = UploadImageType.values, }); final void Function(String? path) onSelectedLocalImage; final void Function(String url) onSelectedAIImage; final void Function(String url) onSelectedNetworkImage; + final List<UploadImageType> supportTypes; @override State<UploadImageMenu> createState() => _UploadImageMenuState(); } class _UploadImageMenuState extends State<UploadImageMenu> { + late final List<UploadImageType> values; int currentTabIndex = 0; - List<UploadImageType> values = UploadImageType.values; bool supportOpenAI = false; bool supportStabilityAI = false; @@ -59,6 +62,7 @@ class _UploadImageMenuState extends State<UploadImageMenu> { void initState() { super.initState(); + values = widget.supportTypes; UserBackendService.getCurrentUserProfile().then( (value) { final supportOpenAI = value.fold( @@ -97,15 +101,16 @@ class _UploadImageMenuState extends State<UploadImageMenu> { Theme.of(context).colorScheme.secondary, ), padding: EdgeInsets.zero, - // splashBorderRadius: BorderRadius.circular(4), tabs: values .map( (e) => FlowyHover( style: const HoverStyle(borderRadius: BorderRadius.zero), child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 8.0, + padding: EdgeInsets.only( + left: 12.0, + right: 12.0, + bottom: 8.0, + top: PlatformExtension.isMobile ? 0 : 8.0, ), child: FlowyText(e.description), ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart index bb17ff1551de..a3071ccef5fd 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart @@ -1,7 +1,9 @@ import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/widget/buttons/primary_button.dart'; import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart'; import 'package:flutter/material.dart'; @@ -44,7 +46,7 @@ SelectionMenuItem mathEquationItem = SelectionMenuItem.node( final mathEquationState = editorState.getNodeAtPath(path)?.key.currentState; if (mathEquationState != null && - mathEquationState is _MathEquationBlockComponentWidgetState) { + mathEquationState is MathEquationBlockComponentWidgetState) { mathEquationState.showEditingDialog(); } }); @@ -89,10 +91,10 @@ class MathEquationBlockComponentWidget extends BlockComponentStatefulWidget { @override State<MathEquationBlockComponentWidget> createState() => - _MathEquationBlockComponentWidgetState(); + MathEquationBlockComponentWidgetState(); } -class _MathEquationBlockComponentWidgetState +class MathEquationBlockComponentWidgetState extends State<MathEquationBlockComponentWidget> with BlockComponentConfigurable { @override @@ -112,35 +114,34 @@ class _MathEquationBlockComponentWidgetState return InkWell( onHover: (value) => setState(() => isHover = value), onTap: showEditingDialog, - child: _buildMathEquation(context), + child: _build(context), ); } - Widget _buildMathEquation(BuildContext context) { + Widget _build(BuildContext context) { Widget child = Container( - width: double.infinity, - constraints: const BoxConstraints(minHeight: 50), - padding: padding, + constraints: const BoxConstraints(minHeight: 52), decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(8.0)), - color: isHover || formula.isEmpty - ? Theme.of(context).colorScheme.tertiaryContainer - : Colors.transparent, + color: formula.isNotEmpty + ? Colors.transparent + : Theme.of(context).colorScheme.surfaceVariant, + borderRadius: BorderRadius.circular(4), ), - child: Center( + child: FlowyHover( + style: HoverStyle( + borderRadius: BorderRadius.circular(4), + ), child: formula.isEmpty - ? FlowyText.medium( - LocaleKeys.document_plugins_mathEquation_addMathEquation.tr(), - fontSize: 16, - ) - : Math.tex( - formula, - textStyle: const TextStyle(fontSize: 20), - mathStyle: MathStyle.display, - ), + ? _buildPlaceholderWidget(context) + : _buildMathEquation(context), ), ); + child = Padding( + padding: padding, + child: child, + ); + if (widget.showActions && widget.actionBuilder != null) { child = BlockComponentActionWrapper( node: node, @@ -149,9 +150,43 @@ class _MathEquationBlockComponentWidgetState ); } + if (PlatformExtension.isMobile) { + child = MobileBlockActionButtons( + node: node, + editorState: editorState, + child: child, + ); + } + return child; } + Widget _buildPlaceholderWidget(BuildContext context) { + return SizedBox( + height: 52, + child: Row( + children: [ + const HSpace(10), + const Icon(Icons.text_fields_outlined), + const HSpace(10), + FlowyText( + LocaleKeys.document_plugins_mathEquation_addMathEquation.tr(), + ), + ], + ), + ); + } + + Widget _buildMathEquation(BuildContext context) { + return Center( + child: Math.tex( + formula, + textStyle: const TextStyle(fontSize: 20), + mathStyle: MathStyle.display, + ), + ); + } + void showEditingDialog() { showDialog( context: context, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/mobile_math_eqaution_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/mobile_math_eqaution_toolbar_item.dart new file mode 100644 index 000000000000..5e7f7dcc77fd --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/mobile_math_eqaution_toolbar_item.dart @@ -0,0 +1,43 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; + +final mathEquationMobileToolbarItem = MobileToolbarItem.action( + itemIcon: const SizedBox(width: 22, child: FlowySvg(FlowySvgs.math_lg)), + actionHandler: (editorState, selection) async { + if (!selection.isCollapsed) { + return; + } + final path = selection.start.path; + final node = editorState.getNodeAtPath(path); + final delta = node?.delta; + if (node == null || delta == null) { + return; + } + final transaction = editorState.transaction; + final insertedNode = mathEquationNode(); + + if (delta.isEmpty) { + transaction + ..insertNode(path, insertedNode) + ..deleteNode(node); + } else { + transaction.insertNode( + path.next, + insertedNode, + ); + } + + await editorState.apply(transaction); + + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + final mathEquationState = + editorState.getNodeAtPath(path)?.key.currentState; + if (mathEquationState != null && + mathEquationState is MathEquationBlockComponentWidgetState) { + mathEquationState.showEditingDialog(); + } + }); + }, +); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart index 37d38be4d855..49b212c40e0c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart @@ -21,9 +21,11 @@ export 'header/custom_cover_picker.dart'; export 'header/document_header_node_widget.dart'; export 'image/image_menu.dart'; export 'image/image_selection_menu.dart'; +export 'image/mobile_image_toolbar_item.dart'; export 'inline_math_equation/inline_math_equation.dart'; export 'inline_math_equation/inline_math_equation_toolbar_item.dart'; export 'math_equation/math_equation_block_component.dart'; +export 'math_equation/mobile_math_eqaution_toolbar_item.dart'; export 'openai/widgets/auto_completion_node_widget.dart'; export 'openai/widgets/smart_edit_node_widget.dart'; export 'openai/widgets/smart_edit_toolbar_item.dart'; @@ -33,3 +35,5 @@ export 'table/table_menu.dart'; export 'table/table_option_action.dart'; export 'toggle/toggle_block_component.dart'; export 'toggle/toggle_block_shortcut_event.dart'; +export 'undo_redo/redo_mobile_toolbar_item.dart'; +export 'undo_redo/undo_mobile_toolbar_item.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/redo_mobile_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/redo_mobile_toolbar_item.dart new file mode 100644 index 000000000000..99b29f9b423e --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/redo_mobile_toolbar_item.dart @@ -0,0 +1,9 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; + +final redoMobileToolbarItem = MobileToolbarItem.action( + itemIcon: const FlowySvg(FlowySvgs.m_redo_m), + actionHandler: (editorState, selection) async { + editorState.undoManager.redo(); + }, +); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/undo_mobile_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/undo_mobile_toolbar_item.dart new file mode 100644 index 000000000000..cf132c14866e --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/undo_mobile_toolbar_item.dart @@ -0,0 +1,9 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; + +final undoMobileToolbarItem = MobileToolbarItem.action( + itemIcon: const FlowySvg(FlowySvgs.m_undo_m), + actionHandler: (editorState, selection) async { + editorState.undoManager.undo(); + }, +); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart index 854cb8e6c92f..3cb102db4249 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart @@ -22,6 +22,7 @@ class FlowyMessageToast extends StatelessWidget { child: FlowyText.medium( message, fontSize: FontSizes.s16, + maxLines: 3, ), ), ); @@ -32,12 +33,17 @@ void initToastWithContext(BuildContext context) { getIt<FToast>().init(context); } -void showMessageToast(String message) { +void showMessageToast( + String message, { + BuildContext? context, + ToastGravity gravity = ToastGravity.BOTTOM, +}) { final child = FlowyMessageToast(message: message); - - getIt<FToast>().showToast( + final toast = context == null ? getIt<FToast>() : FToast() + ..init(context!); + toast.showToast( child: child, - gravity: ToastGravity.BOTTOM, + gravity: gravity, toastDuration: const Duration(seconds: 3), ); } diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/file_picker/file_picker_service.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/file_picker/file_picker_service.dart index 8039ea1e25d2..e8991397a9f8 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/file_picker/file_picker_service.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/file_picker/file_picker_service.dart @@ -1,8 +1,8 @@ +import 'package:file_picker/file_picker.dart'; + export 'package:file_picker/file_picker.dart' show FileType, FilePickerStatus, PlatformFile; -import 'package:file_picker/file_picker.dart'; - class FilePickerResult { const FilePickerResult(this.files); diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart index 906058e6d1d9..c9e21e273de7 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; @@ -24,6 +26,7 @@ class FlowyButton extends StatelessWidget { final Size? leftIconSize; final bool expandText; final MainAxisAlignment mainAxisAlignment; + final bool showDefaultBoxDecorationOnMobile; const FlowyButton({ Key? key, @@ -44,6 +47,7 @@ class FlowyButton extends StatelessWidget { this.leftIconSize = const Size.square(16), this.expandText = true, this.mainAxisAlignment = MainAxisAlignment.center, + this.showDefaultBoxDecorationOnMobile = false, }) : super(key: key); @override @@ -65,12 +69,12 @@ class FlowyButton extends StatelessWidget { ), onHover: disable ? null : onHover, isSelected: () => isSelected, - builder: (context, onHover) => _render(), + builder: (context, onHover) => _render(context), ), ); } - Widget _render() { + Widget _render(BuildContext context) { List<Widget> children = List.empty(growable: true); if (leftIcon != null) { @@ -105,6 +109,16 @@ class FlowyButton extends StatelessWidget { child = IntrinsicWidth(child: child); } + final decoration = this.decoration ?? + (showDefaultBoxDecorationOnMobile && + (Platform.isIOS || Platform.isAndroid) + ? BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.surfaceVariant, + width: 1.0, + )) + : null); + return Container( decoration: decoration, child: Padding( diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index bede73fec4a9..9efa52c66dff 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -53,11 +53,12 @@ packages: appflowy_editor: dependency: "direct main" description: - name: appflowy_editor - sha256: d3112408f28ca3b7b8d3d1ecc90a0c1ba7c1fe807ab285c07b1e9d312b1d3cad - url: "https://pub.dev" - source: hosted - version: "1.5.1" + path: "." + ref: a47fc6f + resolved-ref: a47fc6fc712b06991f578ae2ab314cbe23034e96 + url: "https://github.com/AppFlowy-IO/appflowy-editor.git" + source: git + version: "1.5.2" appflowy_popover: dependency: "direct main" description: @@ -273,6 +274,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.6.3" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "445db18de832dba8d851e287aff8ccf169bed30d2e94243cb54c7d2f1ed2142c" + url: "https://pub.dev" + source: hosted + version: "0.3.3+6" crypto: dependency: transitive description: @@ -442,6 +451,38 @@ packages: url: "https://pub.dev" source: hosted version: "5.3.1" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" + url: "https://pub.dev" + source: hosted + version: "0.9.2+1" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: b15c3da8bd4908b9918111fa486903f5808e388b8d1c559949f584725a6594d6 + url: "https://pub.dev" + source: hosted + version: "0.9.3+3" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "0aa47a725c346825a2bd396343ce63ac00bda6eff2fbc43eabe99737dede8262" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 + url: "https://pub.dev" + source: hosted + version: "0.9.3+1" fixnum: dependency: "direct main" description: @@ -740,6 +781,78 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image_gallery_saver: + dependency: "direct main" + description: + name: image_gallery_saver + sha256: "0aba74216a4d9b0561510cb968015d56b701ba1bd94aace26aacdd8ae5761816" + url: "https://pub.dev" + source: hosted + version: "2.0.3" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "7d7f2768df2a8b0a3cefa5ef4f84636121987d403130e70b17ef7e2cf650ba84" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: d6a6e78821086b0b737009b09363018309bbc6de3fd88cc5c26bc2bb44a4957f + url: "https://pub.dev" + source: hosted + version: "0.8.8+2" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "50bc9ae6a77eea3a8b11af5eb6c661eeb858fdd2f734c2a4fd17086922347ef7" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "76ec722aeea419d03aa915c2c96bf5b47214b053899088c9abb4086ceecf97a7" + url: "https://pub.dev" + source: hosted + version: "0.8.8+4" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: ed9b00e63977c93b0d2d2b343685bed9c324534ba5abafbb3dfbd6a780b1b514 + url: "https://pub.dev" + source: hosted + version: "2.9.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" integration_test: dependency: "direct dev" description: flutter diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 86cfd59a8501..862d1cbea986 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -44,7 +44,10 @@ dependencies: git: url: https://github.com/AppFlowy-IO/appflowy-board.git ref: 1a329c2 - appflowy_editor: ^1.5.1 + appflowy_editor: + git: + url: https://github.com/AppFlowy-IO/appflowy-editor.git + ref: a47fc6f appflowy_popover: path: packages/appflowy_popover @@ -118,6 +121,8 @@ dependencies: local_notifier: ^0.1.5 app_links: ^3.4.1 flutter_slidable: ^3.0.0 + image_picker: ^1.0.4 + image_gallery_saver: ^2.0.3 dev_dependencies: flutter_lints: ^2.0.1 diff --git a/frontend/resources/flowy_icons/32x/m_toolbar_imae.svg b/frontend/resources/flowy_icons/32x/m_toolbar_imae.svg new file mode 100644 index 000000000000..e694a1bc7cdb --- /dev/null +++ b/frontend/resources/flowy_icons/32x/m_toolbar_imae.svg @@ -0,0 +1,3 @@ +<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M7.49805 2.99072C5.28905 2.99072 3.49805 4.78172 3.49805 6.99072V15.9907V16.9907C3.49805 19.1997 5.28905 20.9907 7.49805 20.9907H17.498C19.707 20.9907 21.498 19.1997 21.498 16.9907V15.9907V6.99072C21.498 4.78172 19.707 2.99072 17.498 2.99072H7.49805ZM7.49805 4.99072H17.498C18.603 4.99072 19.498 5.88572 19.498 6.99072L19.502 13.1867C18.724 12.4647 17.9311 12.0127 17.0601 11.9907C17.0291 11.9897 17.03 11.9907 16.998 11.9907C15.861 11.9907 14.597 12.8647 13.748 13.9597C13.531 13.4697 13.3121 12.9857 13.0601 12.5217C11.8801 10.3427 10.591 8.99372 8.99805 8.99072C7.64405 8.98772 6.48205 10.0947 5.49005 11.6107L5.49805 6.99072C5.49805 5.88572 6.39305 4.99072 7.49805 4.99072ZM16.498 6.99072C15.946 6.99072 15.498 7.43872 15.498 7.99072C15.498 8.54272 15.946 8.99072 16.498 8.99072C17.05 8.99072 17.498 8.54272 17.498 7.99072C17.498 7.43872 17.05 6.99072 16.498 6.99072ZM8.99805 10.9907C9.58005 10.9917 10.4641 11.8977 11.3101 13.4597C11.6461 14.0777 11.947 14.7587 12.217 15.4287C12.379 15.8287 12.5041 16.1347 12.5601 16.3027C12.8331 17.1197 13.941 17.2357 14.373 16.4907C14.416 16.4167 14.494 16.2747 14.623 16.0847C14.839 15.7647 15.085 15.4407 15.342 15.1467C15.984 14.4107 16.603 13.9907 16.998 13.9907C17.397 14.0007 18.0131 14.4197 18.6541 15.1467C18.9141 15.4417 19.154 15.7647 19.373 16.0847C19.442 16.1847 19.452 16.2317 19.498 16.3027V16.9907C19.498 18.0957 18.603 18.9907 17.498 18.9907H7.49805C6.39305 18.9907 5.49805 18.0957 5.49805 16.9907V16.1467C5.56105 15.9687 5.64505 15.7577 5.77905 15.4287C6.05205 14.7587 6.34804 14.0787 6.68604 13.4597C7.53904 11.8947 8.41705 10.9897 8.99805 10.9907Z" fill="#676666"/> +</svg> diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index b38493cf7651..786a6048db60 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -214,7 +214,8 @@ "tryAgain": "Try again", "discard": "Discard", "replace": "Replace", - "insertBelow": "Insert Below", + "insertBelow": "Insert below", + "insertAbove": "Insert above", "upload": "Upload", "edit": "Edit", "delete": "Delete", @@ -639,7 +640,7 @@ "alertDialogConfirmation": "Are you sure, you want to continue?" }, "mathEquation": { - "addMathEquation": "Add Math Equation", + "addMathEquation": "Add a TeX equation", "editMathEquation": "Edit Math Equation" }, "optionAction": { @@ -676,7 +677,8 @@ "copy": "Copy", "cut": "Cut", "paste": "Paste" - } + }, + "action": "Actions" }, "textBlock": { "placeholder": "Type '/' for commands" @@ -715,7 +717,11 @@ }, "searchForAnImage": "Search for an image", "pleaseInputYourOpenAIKey": "please input your OpenAI key in Settings page", - "pleaseInputYourStabilityAIKey": "please input your Stability AI key in Settings page" + "pleaseInputYourStabilityAIKey": "please input your Stability AI key in Settings page", + "saveImageToGallery": "Save image", + "failedToAddImageToGallery": "Failed to add image to gallery", + "successToAddImageToGallery": "Image added to gallery successfully", + "unableToLoadImage": "Unable to load image" }, "codeBlock": { "language": { From fe23183aef200bfeadf6f89e015c3a078204665a Mon Sep 17 00:00:00 2001 From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Date: Tue, 7 Nov 2023 15:58:22 +0100 Subject: [PATCH 32/56] feat: add placeholder to card editor (#3870) --- .../lib/plugins/database_view/widgets/row/row_document.dart | 4 ++++ .../plugins/document/presentation/editor_configuration.dart | 5 ++++- .../lib/plugins/document/presentation/editor_page.dart | 6 ++++++ frontend/resources/translations/en.json | 3 +++ 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_document.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_document.dart index 9aa9c2c10aae..22e8ba09799a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_document.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_document.dart @@ -116,6 +116,10 @@ class _RowEditorState extends State<RowEditor> { context: context, padding: const EdgeInsets.symmetric(horizontal: 10), ), + showParagraphPlaceholder: (editorState, node) => + editorState.document.isEmpty, + placeholderText: (node) => + LocaleKeys.cardDetails_notesPlaceholder.tr(), ), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart index d575aa29aa9f..5618431420c2 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart @@ -11,6 +11,8 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({ required EditorStyleCustomizer styleCustomizer, List<SelectionMenuItem>? slashMenuItems, bool editable = true, + ShowPlaceholder? showParagraphPlaceholder, + String Function(Node)? placeholderText, }) { final standardActions = [ OptionAction.delete, @@ -29,7 +31,8 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({ final customBlockComponentBuilderMap = { PageBlockKeys.type: PageBlockComponentBuilder(), ParagraphBlockKeys.type: ParagraphBlockComponentBuilder( - configuration: configuration, + configuration: configuration.copyWith(placeholderText: placeholderText), + showPlaceholder: showParagraphPlaceholder, ), TodoListBlockKeys.type: TodoListBlockComponentBuilder( configuration: configuration.copyWith( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index bd774e4669a5..542ff548eaaf 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -47,6 +47,8 @@ class AppFlowyEditorPage extends StatefulWidget { this.scrollController, this.autoFocus, required this.styleCustomizer, + this.showParagraphPlaceholder, + this.placeholderText, }); final Widget? header; @@ -55,6 +57,8 @@ class AppFlowyEditorPage extends StatefulWidget { final bool shrinkWrap; final bool? autoFocus; final EditorStyleCustomizer styleCustomizer; + final ShowPlaceholder? showParagraphPlaceholder; + final String Function(Node)? placeholderText; @override State<AppFlowyEditorPage> createState() => _AppFlowyEditorPageState(); @@ -112,6 +116,8 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> { context: context, editorState: widget.editorState, styleCustomizer: widget.styleCustomizer, + showParagraphPlaceholder: widget.showParagraphPlaceholder, + placeholderText: widget.placeholderText, ); List<CharacterShortcutEvent> get characterShortcutEvents => [ diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 786a6048db60..852098d8efc5 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -1040,5 +1040,8 @@ "favorite": { "noFavorite": "No favorite page", "noFavoriteHintText": "Swipe the page to the left to add it to your favorites" + }, + "cardDetails": { + "notesPlaceholder": "Enter a / to insert a block, or start typing" } } From 5d4142d5b68300d1c8f65e3ab851784b3f8dacfe Mon Sep 17 00:00:00 2001 From: Generic-Ripe <144800764+Generic-Ripe@users.noreply.github.com> Date: Wed, 8 Nov 2023 03:14:13 +0100 Subject: [PATCH 33/56] chore: update sv.json (#3896) * Update sv.json Some small typos * Rename sv.json to sv-SE.json --- .../resources/translations/{sv.json => sv-SE.json} | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) rename frontend/resources/translations/{sv.json => sv-SE.json} (99%) diff --git a/frontend/resources/translations/sv.json b/frontend/resources/translations/sv-SE.json similarity index 99% rename from frontend/resources/translations/sv.json rename to frontend/resources/translations/sv-SE.json index 600e076ae048..b6f838065f8b 100644 --- a/frontend/resources/translations/sv.json +++ b/frontend/resources/translations/sv-SE.json @@ -233,7 +233,7 @@ }, "theme": "Tema", "builtInsLabel": "Inbyggda teman", - "pluginsLabel": "Plugins" + "pluginsLabel": "Plugin" }, "files": { "copy": "Kopiera", @@ -253,7 +253,7 @@ "open": "Öppen", "openFolder": "Öppna en befintlig mapp", "openFolderDesc": "Läs och skriv det till din befintliga AppFlowy-mapp", - "folderHintText": "mapp namn", + "folderHintText": "mappnamn", "location": "Skapar en ny mapp", "locationDesc": "Välj ett namn för din AppFlowy-datamapp", "browser": "Bläddra", @@ -455,12 +455,12 @@ "createInlineMathEquation": "Skapa ekvation", "toggleList": "Växla lista", "cover": { - "changeCover": "Byta omslag", + "changeCover": "Byt omslag", "colors": "Färger", "images": "Bilder", "clearAll": "Rensa alla", "abstract": "Abstrakt", - "addCover": "Lägg till lock", + "addCover": "Lägg till omslag", "addLocalImage": "Lägg till lokal bild", "invalidImageUrl": "Ogiltig bildadress", "failedToAddImageToGallery": "Det gick inte att lägga till bild i galleriet", @@ -596,4 +596,4 @@ "deleteContentTitle": "Är du säker på att du vill ta bort {pageType}?", "deleteContentCaption": "om du tar bort denna {pageType} kan du återställa den från papperskorgen." } -} \ No newline at end of file +} From 663f9d3423c948cf0e3b5124f565b306666c0366 Mon Sep 17 00:00:00 2001 From: "Kilu.He" <108015703+qinluhe@users.noreply.github.com> Date: Wed, 8 Nov 2023 14:13:17 +0800 Subject: [PATCH 34/56] fix: support inserting grid block in editor (#3875) * fix: support inserting grid block in editor * feat: support adding view in table * feat: support the operations of row in tauri grid --- frontend/appflowy_tauri/package.json | 4 + frontend/appflowy_tauri/pnpm-lock.yaml | 98 +++++++++- .../appflowy_tauri/scripts/i18n/index.cjs | 2 +- .../src/appflowy_app/assets/up.svg | 3 + .../components/database/Database.hooks.ts | 113 ++++++++---- .../components/database/Database.tsx | 88 +++++---- .../components/database/DatabaseLoader.tsx | 12 +- .../components/database/DatabaseTitle.tsx | 33 ++-- .../components/database/DatabaseView.tsx | 23 ++- .../database/_shared/VirtualizedList.tsx | 9 +- .../database/_shared/dnd/drag.hooks.ts | 9 +- .../database_view/database_view_service.ts | 51 +++--- .../database_view/database_view_types.ts | 25 --- .../application/database_view/index.ts | 1 - .../database/components/cell/Cell.hooks.ts | 25 ++- .../database/components/cell/Cell.tsx | 13 +- .../database/components/cell/CheckboxCell.tsx | 18 +- .../components/cell/SelectCell/SelectCell.tsx | 130 +++++++------- .../database/components/cell/TextCell.tsx | 26 ++- .../database_settings/DatabaseCollection.tsx | 17 ++ .../database_settings/DatabaseSettings.tsx | 13 -- .../components/database_settings/index.ts | 2 +- .../components/tab_bar/AddViewBtn.tsx | 25 +++ .../components/tab_bar/DatabaseTabBar.tsx | 69 +++---- .../database/components/tab_bar/ViewTabs.tsx | 25 ++- .../components/database/grid/Grid/Grid.tsx | 7 +- .../database/grid/GridField/GridField.tsx | 79 ++++---- .../GridRow/GridCellRow/GridCellRow.hooks.ts | 71 ++++++++ .../grid/GridRow/GridCellRow/GridCellRow.tsx | 168 ++++++++++-------- .../GridCellRow/GridCellRowActions.tsx | 71 ++++++-- .../GridCellRow/GridCellRowContextMenu.tsx | 36 ++++ .../GridRow/GridCellRow/GridCellRowMenu.tsx | 96 ++++++++++ .../database/grid/GridRow/GridFieldRow.tsx | 27 +-- .../database/grid/GridRow/GridNewRow.tsx | 16 +- .../database/grid/GridRow/GridRow.tsx | 16 +- .../database/grid/GridRow/constants.ts | 7 +- .../database/grid/GridTable/GridTable.tsx | 60 ++++--- .../database/proxy/grid/ui_state/Provider.tsx | 14 ++ .../database/proxy/grid/ui_state/actions.ts | 46 +++++ .../BlockSideToolbar.hooks.tsx | 4 + .../document/BlockSideToolbar/index.tsx | 3 +- .../document/BlockSlash/BlockSlashMenu.tsx | 152 +++++----------- .../document/BlockSlash/index.hooks.ts | 132 +++++++++++++- .../components/document/BlockSlash/index.tsx | 49 ++++- .../components/document/GridBlock/index.tsx | 36 ++++ .../components/document/Node/index.tsx | 32 ++-- .../_shared/DatabaseList/index.hooks.ts | 26 +++ .../document/_shared/DatabaseList/index.tsx | 103 +++++++++++ .../layout/WorkspaceManager/Workspace.tsx | 2 + .../appflowy_app/constants/document/config.ts | 3 + .../src/appflowy_app/interfaces/document.ts | 11 +- .../document/async-actions/turn_to.ts | 85 +++++---- .../src/appflowy_app/utils/document/block.ts | 4 +- .../src/appflowy_app/views/DatabasePage.tsx | 19 +- .../appflowy_tauri/src/styles/template.css | 5 + frontend/resources/translations/en.json | 5 +- .../src/ts_event/event_template.tera | 2 - 57 files changed, 1537 insertions(+), 684 deletions(-) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/assets/up.svg delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/database_view/database_view_types.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/DatabaseCollection.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/DatabaseSettings.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/AddViewBtn.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRow.hooks.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRowContextMenu.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRowMenu.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/proxy/grid/ui_state/Provider.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/proxy/grid/ui_state/actions.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/GridBlock/index.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/DatabaseList/index.hooks.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/DatabaseList/index.tsx diff --git a/frontend/appflowy_tauri/package.json b/frontend/appflowy_tauri/package.json index e92fdc163f91..53e5bde53a42 100644 --- a/frontend/appflowy_tauri/package.json +++ b/frontend/appflowy_tauri/package.json @@ -29,6 +29,7 @@ "@slate-yjs/core": "^1.0.0", "@tanstack/react-virtual": "3.0.0-beta.54", "@tauri-apps/api": "^1.2.0", + "@types/react-swipeable-views": "^0.13.4", "dayjs": "^1.11.9", "emoji-mart": "^5.5.2", "emoji-regex": "^10.2.1", @@ -40,6 +41,7 @@ "is-hotkey": "^0.2.0", "jest": "^29.5.0", "katex": "^0.16.7", + "lodash-es": "^4.17.21", "nanoid": "^4.0.0", "prismjs": "^1.29.0", "protoc-gen-ts": "^0.8.5", @@ -55,6 +57,7 @@ "react-katex": "^3.0.1", "react-redux": "^8.0.5", "react-router-dom": "^6.8.0", + "react-swipeable-views": "^0.14.0", "react-transition-group": "^4.4.5", "react18-input-otp": "^1.1.2", "redux": "^4.2.1", @@ -73,6 +76,7 @@ "@types/is-hotkey": "^0.1.7", "@types/jest": "^29.5.3", "@types/katex": "^0.16.0", + "@types/lodash-es": "^4.17.11", "@types/node": "^18.7.10", "@types/prismjs": "^1.26.0", "@types/quill": "^2.0.10", diff --git a/frontend/appflowy_tauri/pnpm-lock.yaml b/frontend/appflowy_tauri/pnpm-lock.yaml index 2eceb509243e..9c13ac1f54f8 100644 --- a/frontend/appflowy_tauri/pnpm-lock.yaml +++ b/frontend/appflowy_tauri/pnpm-lock.yaml @@ -34,6 +34,9 @@ dependencies: '@tauri-apps/api': specifier: ^1.2.0 version: 1.3.0 + '@types/react-swipeable-views': + specifier: ^0.13.4 + version: 0.13.4 dayjs: specifier: ^1.11.9 version: 1.11.9 @@ -67,6 +70,9 @@ dependencies: katex: specifier: ^0.16.7 version: 0.16.7 + lodash-es: + specifier: ^4.17.21 + version: 4.17.21 nanoid: specifier: ^4.0.0 version: 4.0.2 @@ -112,6 +118,9 @@ dependencies: react-router-dom: specifier: ^6.8.0 version: 6.11.1(react-dom@18.2.0)(react@18.2.0) + react-swipeable-views: + specifier: ^0.14.0 + version: 0.14.0(react@18.2.0) react-transition-group: specifier: ^4.4.5 version: 4.4.5(react-dom@18.2.0)(react@18.2.0) @@ -162,6 +171,9 @@ devDependencies: '@types/katex': specifier: ^0.16.0 version: 0.16.0 + '@types/lodash-es': + specifier: ^4.17.11 + version: 4.17.11 '@types/node': specifier: ^18.7.10 version: 18.16.9 @@ -553,6 +565,12 @@ packages: '@babel/helper-plugin-utils': 7.21.5 dev: true + /@babel/runtime@7.0.0: + resolution: {integrity: sha512-7hGhzlcmg01CvH1EHdSPVXYX1aJ8KCEyz6I9xYIi/asDtzBPMyMhVibhM/K6g/5qnKBwjZtp10bNZIEFTRW1MA==} + dependencies: + regenerator-runtime: 0.12.1 + dev: false + /@babel/runtime@7.21.5: resolution: {integrity: sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==} engines: {node: '>=6.9.0'} @@ -1951,6 +1969,12 @@ packages: resolution: {integrity: sha512-hz+S3nV6Mym5xPbT9fnO8dDhBFQguMYpY0Ipxv06JMi1ORgnEM4M1ymWDUhUNer3ElLmT583opRo4RzxKmh9jw==} dev: true + /@types/lodash-es@4.17.11: + resolution: {integrity: sha512-eCw8FYAWHt2DDl77s+AMLLzPn310LKohruumpucZI4oOFJkIgnlaJcy23OKMJxx4r9PeTF13Gv6w+jqjWQaYUg==} + dependencies: + '@types/lodash': 4.14.194 + dev: true + /@types/lodash.memoize@4.1.7: resolution: {integrity: sha512-lGN7WeO4vO6sICVpf041Q7BX/9k1Y24Zo3FY0aUezr1QlKznpjzsDk3T3wvH8ofYzoK0QupN9TWcFAFZlyPwQQ==} dependencies: @@ -1959,7 +1983,6 @@ packages: /@types/lodash@4.14.194: resolution: {integrity: sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g==} - dev: false /@types/node@18.16.9: resolution: {integrity: sha512-IeB32oIV4oGArLrd7znD2rkHQ6EDCM+2Sr76dJnrHwv9OHBTTM6nuDLK9bmikXzPa0ZlWMWtRGo/Uw4mrzQedA==} @@ -2030,6 +2053,12 @@ packages: redux: 4.2.1 dev: false + /@types/react-swipeable-views@0.13.4: + resolution: {integrity: sha512-hQV9Oq6oa+9HKdnGd43xkckElwf5dThOiegtQxqE7qX761oHhxnZO07fz6IsKSnUy9J3tzlRQBu3sNyvC8+kYw==} + dependencies: + '@types/react': 18.2.6 + dev: false + /@types/react-transition-group@4.4.6: resolution: {integrity: sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==} dependencies: @@ -2460,7 +2489,7 @@ packages: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} engines: {node: '>=10', npm: '>=6'} dependencies: - '@babel/runtime': 7.21.5 + '@babel/runtime': 7.22.10 cosmiconfig: 7.1.0 resolve: 1.22.2 dev: false @@ -4557,6 +4586,10 @@ packages: commander: 8.3.0 dev: false + /keycode@2.2.1: + resolution: {integrity: sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg==} + dev: false + /kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} @@ -5317,6 +5350,17 @@ packages: react: 18.2.0 dev: false + /react-event-listener@0.6.6(react@18.2.0): + resolution: {integrity: sha512-+hCNqfy7o9wvO6UgjqFmBzARJS7qrNoda0VqzvOuioEpoEXKutiKuv92dSz6kP7rYLmyHPyYNLesi5t/aH1gfw==} + peerDependencies: + react: ^16.3.0 + dependencies: + '@babel/runtime': 7.22.10 + prop-types: 15.8.1 + react: 18.2.0 + warning: 4.0.3 + dev: false + /react-i18next@12.2.2(i18next@22.4.15)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-KBB6buBmVKXUWNxXHdnthp+38gPyBT46hJCAIQ8rX19NFL/m2ahte2KARfIDf2tMnSAL7wwck6eDOd/9zn6aFg==} peerDependencies: @@ -5442,6 +5486,42 @@ packages: react: 18.2.0 dev: false + /react-swipeable-views-core@0.14.0: + resolution: {integrity: sha512-0W/e9uPweNEOSPjmYtuKSC/SvKKg1sfo+WtPdnxeLF3t2L82h7jjszuOHz9C23fzkvLfdgkaOmcbAxE9w2GEjA==} + engines: {node: '>=6.0.0'} + dependencies: + '@babel/runtime': 7.0.0 + warning: 4.0.3 + dev: false + + /react-swipeable-views-utils@0.14.0(react@18.2.0): + resolution: {integrity: sha512-W+fXBOsDqgFK1/g7MzRMVcDurp3LqO3ksC8UgInh2P/tKgb5DusuuB1geKHFc6o1wKl+4oyER4Zh3Lxmr8xbXA==} + engines: {node: '>=6.0.0'} + dependencies: + '@babel/runtime': 7.0.0 + keycode: 2.2.1 + prop-types: 15.8.1 + react-event-listener: 0.6.6(react@18.2.0) + react-swipeable-views-core: 0.14.0 + shallow-equal: 1.2.1 + transitivePeerDependencies: + - react + dev: false + + /react-swipeable-views@0.14.0(react@18.2.0): + resolution: {integrity: sha512-wrTT6bi2nC3JbmyNAsPXffUXLn0DVT9SbbcFr36gKpbaCgEp7rX/OFxsu5hPc/NBsUhHyoSRGvwqJNNrWTwCww==} + engines: {node: '>=6.0.0'} + peerDependencies: + react: ^15.3.0 || ^16.0.0 || ^17.0.0 + dependencies: + '@babel/runtime': 7.0.0 + prop-types: 15.8.1 + react: 18.2.0 + react-swipeable-views-core: 0.14.0 + react-swipeable-views-utils: 0.14.0(react@18.2.0) + warning: 4.0.3 + dev: false + /react-transition-group@4.4.5(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} peerDependencies: @@ -5509,6 +5589,10 @@ packages: '@babel/runtime': 7.21.5 dev: false + /regenerator-runtime@0.12.1: + resolution: {integrity: sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==} + dev: false + /regenerator-runtime@0.13.11: resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} dev: false @@ -5669,6 +5753,10 @@ packages: upper-case-first: 2.0.2 dev: true + /shallow-equal@1.2.1: + resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==} + dev: false + /shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -6365,6 +6453,12 @@ packages: dependencies: makeerror: 1.0.12 + /warning@4.0.3: + resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} + dependencies: + loose-envify: 1.4.0 + dev: false + /webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} diff --git a/frontend/appflowy_tauri/scripts/i18n/index.cjs b/frontend/appflowy_tauri/scripts/i18n/index.cjs index 0a95a0111083..21fef44e10ac 100644 --- a/frontend/appflowy_tauri/scripts/i18n/index.cjs +++ b/frontend/appflowy_tauri/scripts/i18n/index.cjs @@ -15,7 +15,7 @@ const languages = [ 'pt-BR', 'pt-PT', 'ru-RU', - 'sv', + 'sv-SE', 'tr-TR', 'zh-CN', 'zh-TW', diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/up.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/up.svg new file mode 100644 index 000000000000..bd8f3067d317 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/up.svg @@ -0,0 +1,3 @@ +<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M11 7L8 4.5M8 4.5L5 7M8 4.5V12.5" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.hooks.ts index 1c8f6724ed57..847d5aa03b18 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.hooks.ts @@ -1,32 +1,27 @@ -import { RefObject, createContext, createRef, useContext, useCallback, useMemo, useEffect } from 'react'; +import { createContext, useContext, useCallback, useMemo, useEffect, useState, useRef } from 'react'; import { useSearchParams } from 'react-router-dom'; import { proxy, useSnapshot } from 'valtio'; import { DatabaseLayoutPB, DatabaseNotification } from '@/services/backend'; import { subscribeNotifications } from '$app/hooks'; -import { - Database, - databaseService, - fieldService, - rowListeners, - sortListeners, -} from './application'; +import { Database, databaseService, fieldService, rowListeners, sortListeners } from './application'; -const VerticalScrollElementRefContext = createContext<RefObject<Element>>(createRef()); - -export const VerticalScrollElementProvider = VerticalScrollElementRefContext.Provider; -export const useVerticalScrollElement = () => useContext(VerticalScrollElementRefContext); - -export function useSelectDatabaseView() { +export function useSelectDatabaseView({ viewId }: { viewId?: string }) { const key = 'v'; const [searchParams, setSearchParams] = useSearchParams(); - const selectedViewId = useMemo(() => searchParams.get(key), [searchParams]); + const selectedViewId = useMemo(() => searchParams.get(key) || viewId, [searchParams, viewId]); - const selectViewId = useCallback((value: string) => { - setSearchParams({ [key]: value }); - }, [setSearchParams]); + const onChange = useCallback( + (value: string) => { + setSearchParams({ [key]: value }); + }, + [setSearchParams] + ); - return [selectedViewId, selectViewId] as const; + return { + selectedViewId, + onChange, + }; } const DatabaseContext = createContext<Database>({ @@ -59,26 +54,82 @@ export const useConnectDatabase = (viewId: string) => { groups: [], }); - void databaseService.openDatabase(viewId).then(value => Object.assign(proxyDatabase, value)); + void databaseService.openDatabase(viewId).then((value) => Object.assign(proxyDatabase, value)); return proxyDatabase; }, [viewId]); useEffect(() => { - const unsubscribePromise = subscribeNotifications({ - [DatabaseNotification.DidUpdateFields]: async () => { - database.fields = await fieldService.getFields(viewId); + const unsubscribePromise = subscribeNotifications( + { + [DatabaseNotification.DidUpdateFields]: async () => { + database.fields = await fieldService.getFields(viewId); + }, + [DatabaseNotification.DidUpdateViewRows]: (changeset) => { + rowListeners.didUpdateViewRows(database, changeset); + }, + [DatabaseNotification.DidReorderRows]: (changeset) => { + rowListeners.didReorderRows(database, changeset); + }, + [DatabaseNotification.DidReorderSingleRow]: (changeset) => { + rowListeners.didReorderSingleRow(database, changeset); + }, + + [DatabaseNotification.DidUpdateSort]: (changeset) => { + sortListeners.didUpdateSort(database, changeset); + }, }, + { id: viewId } + ); - [DatabaseNotification.DidUpdateViewRows]: changeset => rowListeners.didUpdateViewRows(database, changeset), - [DatabaseNotification.DidReorderRows]: changeset => rowListeners.didReorderRows(database, changeset), - [DatabaseNotification.DidReorderSingleRow]: changeset => rowListeners.didReorderSingleRow(database, changeset), - - [DatabaseNotification.DidUpdateSort]: changeset => sortListeners.didUpdateSort(database, changeset), - }, { id: viewId }); - - return () => void unsubscribePromise.then(unsubscribe => unsubscribe()); + return () => void unsubscribePromise.then((unsubscribe) => unsubscribe()); }, [viewId, database]); return database; }; + +export function useDatabaseResize() { + const ref = useRef<HTMLDivElement>(null); + const collectionRef = useRef<HTMLDivElement>(null); + + const [tableHeight, setTableHeight] = useState(0); + + useEffect(() => { + const element = ref.current; + + if (!element) return; + + const collectionElement = collectionRef.current; + const handleResize = () => { + const rect = element.getBoundingClientRect(); + const collectionRect = collectionElement?.getBoundingClientRect(); + let height = rect.height - 31; + + if (collectionRect) { + height -= collectionRect.height; + } + + setTableHeight(height); + }; + + handleResize(); + const resizeObserver = new ResizeObserver(() => { + handleResize(); + }); + + resizeObserver.observe(element); + if (collectionElement) { + resizeObserver.observe(collectionRef.current); + } + + return () => { + resizeObserver.disconnect(); + }; + }, []); + + return { + ref, + collectionRef, + tableHeight, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.tsx index 2e57c7fe9d82..997b05ab1083 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.tsx @@ -1,42 +1,70 @@ import { useEffect, useMemo, useState } from 'react'; -import { useViewId } from '$app/hooks'; -import { DatabaseView as DatabaseViewType, databaseViewService } from './application'; +import { useViewId } from '$app/hooks/ViewId.hooks'; +import { databaseViewService } from './application'; import { DatabaseTabBar } from './components'; -import { useSelectDatabaseView } from './Database.hooks'; import { DatabaseLoader } from './DatabaseLoader'; import { DatabaseView } from './DatabaseView'; -import { DatabaseSettings } from './components/database_settings'; +import { DatabaseCollection } from './components/database_settings'; +import { PageController } from '$app/stores/effects/workspace/page/page_controller'; +import SwipeableViews from 'react-swipeable-views'; +import { TabPanel } from '$app/components/database/components/tab_bar/ViewTabs'; +import { useDatabaseResize } from '$app/components/database/Database.hooks'; -export const Database = () => { +interface Props { + selectedViewId?: string; + setSelectedViewId?: (viewId: string) => void; +} + +export const Database = ({ selectedViewId, setSelectedViewId }: Props) => { const viewId = useViewId(); - const [views, setViews] = useState<DatabaseViewType[]>([]); - const [selectedViewId, selectViewId] = useSelectDatabaseView(); - const activeView = useMemo(() => views?.find((view) => view.id === selectedViewId), [views, selectedViewId]); + const [childViewIds, setChildViewIds] = useState<string[]>([]); + const { ref, collectionRef, tableHeight } = useDatabaseResize(); useEffect(() => { - setViews([]); - void databaseViewService.getDatabaseViews(viewId).then((value) => { - setViews(value); + const onPageChanged = () => { + void databaseViewService.getDatabaseViews(viewId).then((value) => { + setChildViewIds(value.map((view) => view.id)); + }); + }; + + onPageChanged(); + + const pageController = new PageController(viewId); + + void pageController.subscribe({ + onPageChanged, }); + + return () => { + void pageController.unsubscribe(); + }; }, [viewId]); - useEffect(() => { - if (!activeView) { - const firstViewId = views?.[0]?.id; - - if (firstViewId) { - selectViewId(firstViewId); - } - } - }, [views, activeView, selectViewId]); - - return activeView ? ( - <DatabaseLoader viewId={viewId}> - <div className='px-16'> - <DatabaseTabBar views={views} /> - <DatabaseSettings /> - </div> - <DatabaseView /> - </DatabaseLoader> - ) : null; + const index = useMemo(() => { + return Math.max(0, childViewIds.indexOf(selectedViewId ?? viewId)); + }, [childViewIds, selectedViewId, viewId]); + + return ( + <div ref={ref} className='appflowy-database flex flex-1 flex-col overflow-y-hidden'> + <DatabaseTabBar + pageId={viewId} + setSelectedViewId={setSelectedViewId} + selectedViewId={selectedViewId} + childViewIds={childViewIds} + /> + <SwipeableViews className={'flex-1 overflow-hidden'} axis={'x'} index={index}> + {childViewIds.map((id) => ( + <TabPanel key={id} index={index} value={index}> + <DatabaseLoader viewId={id}> + <div ref={collectionRef}> + <DatabaseCollection /> + </div> + + <DatabaseView isActivated={selectedViewId === id} tableHeight={tableHeight} /> + </DatabaseLoader> + </TabPanel> + ))} + </SwipeableViews> + </div> + ); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseLoader.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseLoader.tsx index 7ccfeef29622..b0aeab10a21a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseLoader.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseLoader.tsx @@ -3,20 +3,16 @@ import { ViewIdProvider } from '$app/hooks'; import { DatabaseProvider, useConnectDatabase } from './Database.hooks'; export interface DatabaseLoaderProps { - viewId: string + viewId: string; } -export const DatabaseLoader: FC<PropsWithChildren<DatabaseLoaderProps>> = ({ - viewId, - children, -}) => { +export const DatabaseLoader: FC<PropsWithChildren<DatabaseLoaderProps>> = ({ viewId, children }) => { const database = useConnectDatabase(viewId); return ( <DatabaseProvider value={database}> - <ViewIdProvider value={viewId}> - {children} - </ViewIdProvider> + {/* Make sure that the viewId is current */} + <ViewIdProvider value={viewId}>{children}</ViewIdProvider> </DatabaseProvider> ); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseTitle.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseTitle.tsx index 57689caf386a..34e9568c7bc5 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseTitle.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseTitle.tsx @@ -5,12 +5,12 @@ import { useViewId } from '$app/hooks'; export const DatabaseTitle = () => { const viewId = useViewId(); - const [ title, setTitle ] = useState(''); + const [title, setTitle] = useState(''); - const controller = useMemo(() => new PageController(viewId), [ viewId ]); + const controller = useMemo(() => new PageController(viewId), [viewId]); useEffect(() => { - void controller.getPage().then(page => { + void controller.getPage().then((page) => { setTitle(page.name); }); @@ -23,21 +23,24 @@ export const DatabaseTitle = () => { return () => { void controller.unsubscribe(); }; - }, [ controller ]); - - const handleInput = useCallback<FormEventHandler>((event) => { - const newTitle = (event.target as HTMLInputElement).value; - - void controller.updatePage({ - id: viewId, - name: newTitle, - }); - }, [ viewId, controller ]); + }, [controller]); + + const handleInput = useCallback<FormEventHandler>( + (event) => { + const newTitle = (event.target as HTMLInputElement).value; + + void controller.updatePage({ + id: viewId, + name: newTitle, + }); + }, + [viewId, controller] + ); return ( - <div className="px-16 pt-8 mb-6"> + <div className='mb-6 h-[70px] pt-8'> <input - className="text-3xl font-semibold" + className='text-3xl font-semibold' value={title} placeholder={t('grid.title.placeholder')} onInput={handleInput} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseView.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseView.tsx index f2b0966e9b6d..266ad6a100bd 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseView.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseView.tsx @@ -5,15 +5,20 @@ import { Grid } from './grid'; import { Board } from './board'; import { Calendar } from './calendar'; -const ViewMap: Record<DatabaseLayoutPB, FC | null> = { - [DatabaseLayoutPB.Grid]: Grid, - [DatabaseLayoutPB.Board]: Board, - [DatabaseLayoutPB.Calendar]: Calendar, -}; - -export const DatabaseView: FC = () => { +export const DatabaseView: FC<{ + tableHeight: number; + isActivated: boolean; +}> = (props) => { const { layoutType } = useDatabase(); - const View = ViewMap[layoutType]; - return View && <View />; + switch (layoutType) { + case DatabaseLayoutPB.Grid: + return <Grid {...props} />; + case DatabaseLayoutPB.Board: + return <Board />; + case DatabaseLayoutPB.Calendar: + return <Calendar />; + default: + return null; + } }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/VirtualizedList.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/VirtualizedList.tsx index 61982ea609ae..1ce7ffea21b1 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/VirtualizedList.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/VirtualizedList.tsx @@ -4,9 +4,10 @@ import React, { CSSProperties, FC } from 'react'; export interface VirtualizedListProps { className?: string; style?: CSSProperties | undefined; - virtualizer: Virtualizer<Element, Element>, + virtualizer: Virtualizer<HTMLDivElement, HTMLDivElement>; itemClassName?: string; renderItem: (index: number) => React.ReactNode; + getItemStyle?: (index: number) => CSSProperties | undefined; } export const VirtualizedList: FC<VirtualizedListProps> = ({ @@ -15,6 +16,7 @@ export const VirtualizedList: FC<VirtualizedListProps> = ({ itemClassName, virtualizer, renderItem, + getItemStyle, }) => { const virtualItems = virtualizer.getVirtualItems(); const { horizontal } = virtualizer.options; @@ -32,7 +34,10 @@ export const VirtualizedList: FC<VirtualizedListProps> = ({ <div key={key} className={itemClassName} - style={{ [sizeProp]: size }} + style={{ + [sizeProp]: size, + ...getItemStyle?.(index), + }} data-key={key} data-index={index} > diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/drag.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/drag.hooks.ts index 4df6623d0400..83cdd4abdcf1 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/drag.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/drag.hooks.ts @@ -37,7 +37,9 @@ export const useDraggable = ({ previewRef.current = previewElement; }, []); - const attributes = useMemo(() => { + const attributes: { + draggable?: boolean; + } = useMemo(() => { if (disabled) { return {}; } @@ -89,7 +91,10 @@ export const useDraggable = ({ context.dragging = null; }, [context]); - const listeners = useMemo( + const listeners: { + onDragStart?: DragEventHandler; + onDragEnd?: DragEventHandler; + } = useMemo( () => ({ onDragStart, onDragEnd, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/database_view/database_view_service.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/database_view/database_view_service.ts index 8827267bafce..aa893a901243 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/database_view/database_view_service.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/database_view/database_view_service.ts @@ -1,10 +1,4 @@ -import { - CreateViewPayloadPB, - RepeatedViewIdPB, - UpdateViewPayloadPB, - ViewIdPB, - ViewLayoutPB, -} from '@/services/backend'; +import { CreateViewPayloadPB, RepeatedViewIdPB, UpdateViewPayloadPB, ViewIdPB, ViewLayoutPB } from '@/services/backend'; import { FolderEventCreateView, FolderEventDeleteView, @@ -12,42 +6,45 @@ import { FolderEventUpdateView, } from '@/services/backend/events/flowy-folder2'; import { databaseService } from '../database'; -import { DatabaseView, DatabaseViewLayout, pbToDatabaseView } from './database_view_types'; +import { Page, parserViewPBToPage } from '$app_reducers/pages/slice'; -export async function getDatabaseViews(viewId: string): Promise<DatabaseView[]> { +export async function getDatabaseViews(viewId: string): Promise<Page[]> { const payload = ViewIdPB.fromObject({ value: viewId }); const result = await FolderEventReadView(payload); - return result.map(value => { - return [ - pbToDatabaseView(value), - ...value.child_views.map(pbToDatabaseView), - ]; - }).unwrap(); + if (result.ok) { + return [parserViewPBToPage(result.val), ...result.val.child_views.map(parserViewPBToPage)]; + } + + return Promise.reject(result.err); } export async function createDatabaseView( viewId: string, - layout: DatabaseViewLayout, + layout: ViewLayoutPB, name: string, - databaseId?: string, -): Promise<DatabaseView> { + databaseId?: string +): Promise<Page> { const payload = CreateViewPayloadPB.fromObject({ parent_view_id: viewId, name, layout, meta: { - 'database_id': databaseId || await databaseService.getDatabaseId(viewId), + database_id: databaseId || (await databaseService.getDatabaseId(viewId)), }, }); const result = await FolderEventCreateView(payload); - return result.map(pbToDatabaseView).unwrap(); + if (result.ok) { + return parserViewPBToPage(result.val); + } + + return Promise.reject(result.err); } -export async function updateView(viewId: string, view: { name?: string; layout?: ViewLayoutPB }): Promise<DatabaseView> { +export async function updateView(viewId: string, view: { name?: string; layout?: ViewLayoutPB }): Promise<Page> { const payload = UpdateViewPayloadPB.fromObject({ view_id: viewId, name: view.name, @@ -56,7 +53,11 @@ export async function updateView(viewId: string, view: { name?: string; layout?: const result = await FolderEventUpdateView(payload); - return result.map(pbToDatabaseView).unwrap(); + if (result.ok) { + return parserViewPBToPage(result.val); + } + + return Promise.reject(result.err); } export async function deleteView(viewId: string): Promise<void> { @@ -66,5 +67,9 @@ export async function deleteView(viewId: string): Promise<void> { const result = await FolderEventDeleteView(payload); - return result.unwrap(); + if (result.ok) { + return; + } + + return Promise.reject(result.err); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/database_view/database_view_types.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/database_view/database_view_types.ts deleted file mode 100644 index 2135a50f5af9..000000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/database_view/database_view_types.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { CalendarLayoutPB, ViewLayoutPB, ViewPB } from '@/services/backend'; - -export type DatabaseViewLayout = ViewLayoutPB.Grid | ViewLayoutPB.Board | ViewLayoutPB.Calendar; - -export interface DatabaseView { - id: string; - name: string; - layout: DatabaseViewLayout; -} - -export interface CalendarLayoutSetting { - fieldId?: string; - layoutTy?: CalendarLayoutPB; - firstDayOfWeek?: number; - showWeekends?: boolean; - showWeekNumbers?: boolean; -} - -export function pbToDatabaseView(viewPB: ViewPB): DatabaseView { - return { - id: viewPB.id, - layout: viewPB.layout as DatabaseViewLayout, - name: viewPB.name, - }; -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/database_view/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/database_view/index.ts index 2ffada0e45d0..b2a6e1a5f1ce 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/database_view/index.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/database_view/index.ts @@ -1,2 +1 @@ -export * from './database_view_types'; export * as databaseViewService from './database_view_service'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/Cell.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/Cell.hooks.ts index bb390db0e749..f456852b0eaf 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/Cell.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/Cell.hooks.ts @@ -1,22 +1,31 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { DatabaseNotification, FieldType } from '@/services/backend'; import { useNotification, useViewId } from '$app/hooks'; import { cellService, Cell } from '../../application'; +import { debounce } from 'lodash-es'; + +// delay for debounced fetch +// Because we don't want to fetch cell when element is scrolling +const DELAY = 200; export const useCell = (rowId: string, fieldId: string, fieldType: FieldType) => { const viewId = useViewId(); - const [cell, setCell] = useState<Cell | null>(null); + const [cell, setCell] = useState<Cell | undefined>(undefined); const fetchCell = useCallback(() => { - void cellService.getCell(viewId, rowId, fieldId, fieldType) - .then(data => { - setCell(data); - }); + void cellService.getCell(viewId, rowId, fieldId, fieldType).then((data) => { + setCell(data); + }); }, [viewId, rowId, fieldId, fieldType]); + const debouncedFetchCell = useMemo(() => debounce(fetchCell, DELAY), [fetchCell]); + useEffect(() => { - void fetchCell(); - }, [fetchCell]); + debouncedFetchCell(); + return () => { + debouncedFetchCell.cancel(); + }; + }, [debouncedFetchCell]); useNotification(DatabaseNotification.DidUpdateCell, fetchCell, { id: `${rowId}:${fieldId}` }); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/Cell.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/Cell.tsx index 72e596d7f420..148f6f2c1dba 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/Cell.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/Cell.tsx @@ -15,26 +15,23 @@ export interface CellProps { const getCellComponent = (fieldType: FieldType) => { switch (fieldType) { case FieldType.RichText: - return TextCell as FC<{ field: Field, cell: CellType }>; + return TextCell as FC<{ field: Field; cell?: CellType }>; case FieldType.SingleSelect: case FieldType.MultiSelect: - return SelectCell as FC<{ field: Field, cell: CellType }>; + return SelectCell as FC<{ field: Field; cell?: CellType }>; case FieldType.Checkbox: - return CheckboxCell as FC<{ field: Field, cell: CellType }>; + return CheckboxCell as FC<{ field: Field; cell?: CellType }>; default: return null; } }; -export const Cell: FC<CellProps> = ({ - rowId, - field, -}) => { +export const Cell: FC<CellProps> = ({ rowId, field }) => { const cell = useCell(rowId, field.id, field.type); const Component = getCellComponent(field.type); - if (!cell || !Component) { + if (!Component) { return null; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/CheckboxCell.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/CheckboxCell.tsx index c3120b61ed14..6aaee306f0ea 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/CheckboxCell.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/CheckboxCell.tsx @@ -6,23 +6,19 @@ import { useViewId } from '$app/hooks'; import { cellService, CheckboxCell as CheckboxCellType, Field } from '../../application'; export const CheckboxCell: FC<{ - field: Field, - cell: CheckboxCellType, + field: Field; + cell?: CheckboxCellType; }> = ({ field, cell }) => { const viewId = useViewId(); - const checked = cell.data === 'Yes'; + const checked = cell?.data === 'Yes'; const handleClick = useCallback(() => { - void cellService.updateCell( - viewId, - cell.rowId, - field.id, - !checked ? 'Yes' : 'No', - ); - }, [viewId, cell.rowId, field.id, checked ]); + if (!cell) return; + void cellService.updateCell(viewId, cell.rowId, field.id, !checked ? 'Yes' : 'No'); + }, [viewId, cell, field.id, checked]); return ( - <div className="flex items-center w-full px-2 cursor-pointer" onClick={handleClick}> + <div className='flex w-full cursor-pointer items-center px-2' onClick={handleClick}> <Checkbox disableRipple style={{ padding: 0 }} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell/SelectCell.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell/SelectCell.tsx index d7622e859072..5488b4481039 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell/SelectCell.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell/SelectCell.tsx @@ -1,14 +1,6 @@ import { FC, FormEvent, useCallback, useMemo, useState } from 'react'; import { t } from 'i18next'; -import { - ListSubheader, - Select, - OutlinedInput, - SelectChangeEvent, - InputBase, - MenuProps, - MenuItem, -} from '@mui/material'; +import { ListSubheader, Select, OutlinedInput, SelectChangeEvent, InputBase, MenuProps, MenuItem } from '@mui/material'; import { FieldType } from '@/services/backend'; import { useViewId } from '$app/hooks'; import { cellService, SelectField, SelectCell as SelectCellType } from '../../../application'; @@ -32,16 +24,21 @@ const menuProps: Partial<MenuProps> = { export const SelectCell: FC<{ field: SelectField; - cell: SelectCellType; + cell?: SelectCellType; }> = ({ field, cell }) => { - const rowId = cell.rowId; + const [open, setOpen] = useState(false); + const rowId = cell?.rowId; const viewId = useViewId(); const options = useMemo(() => field.typeOption.options ?? [], [field.typeOption.options]); - const selectedIds = useMemo(() => cell.data.selectedOptionIds ?? [], [cell.data.selectedOptionIds]); + const selectedIds = useMemo(() => cell?.data.selectedOptionIds ?? [], [cell?.data.selectedOptionIds]); const [newOptionName, setNewOptionName] = useState(''); - const filteredOptions = useMemo(() => options.filter(option => { - return option.name.toLowerCase().includes(newOptionName.toLowerCase()); - }), [options, newOptionName]); + const filteredOptions = useMemo( + () => + options.filter((option) => { + return option.name.toLowerCase().includes(newOptionName.toLowerCase()); + }), + [options, newOptionName] + ); const shouldCreateOption = !!newOptionName && filteredOptions.length === 0; @@ -53,14 +50,18 @@ export const SelectCell: FC<{ const handleClose = useCallback(() => { setNewOptionName(''); + setOpen(false); }, []); const handleChange = (event: SelectChangeEvent<string | string[]>) => { - const { target: { value } } = event; + if (!cell || !rowId) return; + const { + target: { value }, + } = event; const current = Array.isArray(value) ? value : [value]; const prev = cell.data.selectedOptionIds; - const deleteOptionIds = prev?.filter(id => current.find(cur => cur === id) === undefined); + const deleteOptionIds = prev?.filter((id) => current.find((cur) => cur === id) === undefined); void cellService.updateSelectCell(viewId, rowId, field.id, { insertOptionIds: current, @@ -69,7 +70,8 @@ export const SelectCell: FC<{ }; const handleNewTagClick = async () => { - const exist = options.find(option => option.name.toLowerCase() === newOptionName.toLowerCase()); + if (!cell || !rowId) return; + const exist = options.find((option) => option.name.toLowerCase() === newOptionName.toLowerCase()); if (exist) { return cellService.updateSelectCell(viewId, rowId, field.id, { @@ -83,9 +85,9 @@ export const SelectCell: FC<{ }; const searchInput = ( - <ListSubheader className="flex"> + <ListSubheader className='flex'> <OutlinedInput - size="small" + size='small' value={newOptionName} onInput={handleInput} placeholder={t('grid.selectOption.searchOrCreateOption')} @@ -93,50 +95,52 @@ export const SelectCell: FC<{ </ListSubheader> ); - const renderSelectedOptions = useCallback((selected: string[]) => selected - .map((id) => options.find(option => option.id === id)) - .map((option) => option && ( - <Tag - key={option.id} - size="small" - color={option.color} - label={option.name} - /> - )), [options]); + const renderSelectedOptions = useCallback( + (selected: string[]) => + selected + .map((id) => options.find((option) => option.id === id)) + .map((option) => option && <Tag key={option.id} size='small' color={option.color} label={option.name} />), + [options] + ); return ( - <Select - className="w-full" - classes={{ - select: 'flex items-center gap-2 px-4 py-1 h-6', - }} - size="small" - value={selectedIds} - multiple={field.type === FieldType.MultiSelect} - input={<InputBase />} - IconComponent={() => null} - MenuProps={menuProps} - renderValue={renderSelectedOptions} - onChange={handleChange} - onClose={handleClose} - > - {searchInput} - <ListSubheader className="text-xs mt-4 mb-2"> - {shouldCreateOption - ? t('grid.selectOption.createNew') - : t('grid.selectOption.orSelectOne')} - </ListSubheader> - {shouldCreateOption - ? <CreateOption label={newOptionName} onClick={handleNewTagClick} /> - : filteredOptions.map((option, index) => ( - <MenuItem - className={index === 0 ? '' : 'mt-2'} - key={option.id} - value={option.id} - > - <SelectOptionItem option={option} /> - </MenuItem> - ))} - </Select> + <div className={'relative w-full'}> + <div + onClick={() => { + setOpen(true); + }} + className={'absolute left-0 top-0 flex h-full w-full items-center gap-2 px-4 py-1'} + > + {renderSelectedOptions(selectedIds)} + </div> + {open ? ( + <Select + className='h-full w-full' + size='small' + value={selectedIds} + open={open} + multiple={field.type === FieldType.MultiSelect} + input={<InputBase />} + IconComponent={() => null} + MenuProps={menuProps} + onChange={handleChange} + onClose={handleClose} + > + {searchInput} + <ListSubheader className='mb-2 mt-4 text-xs'> + {shouldCreateOption ? t('grid.selectOption.createNew') : t('grid.selectOption.orSelectOne')} + </ListSubheader> + {shouldCreateOption ? ( + <CreateOption label={newOptionName} onClick={handleNewTagClick} /> + ) : ( + filteredOptions.map((option, index) => ( + <MenuItem className={index === 0 ? '' : 'mt-2'} key={option.id} value={option.id}> + <SelectOptionItem option={option} /> + </MenuItem> + )) + )} + </Select> + ) : null} + </div> ); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/TextCell.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/TextCell.tsx index dcd32e880ddb..d4e9d5a53508 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/TextCell.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/TextCell.tsx @@ -5,16 +5,17 @@ import { cellService, Field, TextCell as TextCellType } from '../../application' import { CellText } from '../../_shared'; export const TextCell: FC<{ - field: Field, - cell: TextCellType; + field: Field; + cell?: TextCellType; }> = ({ field, cell }) => { const viewId = useViewId(); const cellRef = useRef<HTMLDivElement>(null); - const [ editing, setEditing ] = useState(false); - const [ text, setText ] = useState(''); - const [ width, setWidth ] = useState<number | undefined>(undefined); + const [editing, setEditing] = useState(false); + const [text, setText] = useState(''); + const [width, setWidth] = useState<number | undefined>(undefined); const handleClose = () => { + if (!cell) return; if (editing) { if (text !== cell.data) { void cellService.updateCell(viewId, cell.rowId, field.id, text); @@ -25,9 +26,10 @@ export const TextCell: FC<{ }; const handleClick = useCallback(() => { + if (!cell) return; setText(cell.data); setEditing(true); - }, [cell.data]); + }, [cell]); const handleInput = useCallback<FormEventHandler<HTMLTextAreaElement>>((event) => { setText((event.target as HTMLTextAreaElement).value); @@ -41,12 +43,8 @@ export const TextCell: FC<{ return ( <> - <CellText - ref={cellRef} - className="w-full" - onClick={handleClick} - > - {cell.data} + <CellText ref={cellRef} className='w-full' onClick={handleClick}> + {cell?.data} </CellText> {editing && ( <Popover @@ -64,9 +62,9 @@ export const TextCell: FC<{ onClose={handleClose} > <TextareaAutosize - className="resize-none text-sm" + className='resize-none text-sm' autoFocus - autoCorrect="off" + autoCorrect='off' value={text} onInput={handleInput} /> diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/DatabaseCollection.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/DatabaseCollection.tsx new file mode 100644 index 000000000000..731f658e0384 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/DatabaseCollection.tsx @@ -0,0 +1,17 @@ +import { Sort } from '../../application'; +import { useDatabase } from '../../Database.hooks'; +import { Sorts } from '../sort'; + +export const DatabaseCollection = () => { + const { sorts } = useDatabase(); + + const showSorts = sorts && sorts.length > 0; + + const showCollection = showSorts; + + return ( + <div className={`flex items-center ${!showCollection ? 'h-0' : 'border-b border-line-divider py-3'}`}> + {showSorts && <Sorts sorts={sorts as Sort[]} />} + </div> + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/DatabaseSettings.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/DatabaseSettings.tsx deleted file mode 100644 index d6d8d0e854e1..000000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/DatabaseSettings.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Sort } from '../../application'; -import { useDatabase } from '../../Database.hooks'; -import { Sorts } from '../sort'; - -export const DatabaseSettings = () => { - const { sorts } = useDatabase(); - - return ( - <div className="flex items-center border-t"> - <Sorts sorts={sorts as Sort[]} /> - </div> - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/index.ts index ebac0b2a51e9..efb89af437c4 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/index.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/index.ts @@ -1 +1 @@ -export * from './DatabaseSettings'; +export * from './DatabaseCollection'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/AddViewBtn.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/AddViewBtn.tsx new file mode 100644 index 000000000000..fff307514ee2 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/AddViewBtn.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { IconButton } from '@mui/material'; +import { ReactComponent as AddSvg } from '$app/assets/add.svg'; +import { useTranslation } from 'react-i18next'; +import { ViewLayoutPB } from '@/services/backend'; +import { createDatabaseView } from '$app/components/database/application/database_view/database_view_service'; + +function AddViewBtn({ pageId }: { pageId: string }) { + const { t } = useTranslation(); + const onClick = async () => { + try { + await createDatabaseView(pageId, ViewLayoutPB.Grid, t('editor.table')); + } catch (e) { + console.error(e); + } + }; + + return ( + <IconButton onClick={onClick} size='small'> + <AddSvg /> + </IconButton> + ); +} + +export default AddViewBtn; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/DatabaseTabBar.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/DatabaseTabBar.tsx index 1cfacbb5ad1d..24846633afe4 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/DatabaseTabBar.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/DatabaseTabBar.tsx @@ -1,64 +1,51 @@ -import { FC, MouseEventHandler, useCallback, useState } from 'react'; -import { t } from 'i18next'; -import { IconButton, Stack } from '@mui/material'; -import { ReactComponent as AddSvg } from '$app/assets/add.svg'; -import { DatabaseView } from '../../application'; -import { useSelectDatabaseView } from '../../Database.hooks'; +import { FC, useEffect } from 'react'; import { ViewTabs, ViewTab } from './ViewTabs'; -import { TextButton } from './TextButton'; -import { SortMenu } from '../sort'; +import { useAppSelector } from '$app/stores/store'; +import { useTranslation } from 'react-i18next'; +import AddViewBtn from '$app/components/database/components/tab_bar/AddViewBtn'; export interface DatabaseTabBarProps { - views: DatabaseView[]; + childViewIds: string[]; + selectedViewId?: string; + setSelectedViewId?: (viewId: string) => void; + pageId: string; } -export const DatabaseTabBar: FC<DatabaseTabBarProps> = ({ - views, -}) => { - const [selectedViewId, selectViewId] = useSelectDatabaseView(); - const [sortAnchorEl, setSortAnchorEl] = useState<null | HTMLElement>(null); - const open = Boolean(sortAnchorEl); +export const DatabaseTabBar: FC<DatabaseTabBarProps> = ({ pageId, childViewIds, selectedViewId, setSelectedViewId }) => { + const { t } = useTranslation(); + const views = useAppSelector((state) => { + const map = state.pages.pageMap; - const handleChange = (event: React.SyntheticEvent, newValue: string) => { - selectViewId(newValue); - }; - - const handleClick = useCallback<MouseEventHandler<HTMLElement>>((event) => { - setSortAnchorEl(event.currentTarget); - }, []); + return childViewIds.map((id) => map[id]).filter(Boolean); + }); - const handleClose = () => { - setSortAnchorEl(null); + const handleChange = (_: React.SyntheticEvent, newValue: string) => { + setSelectedViewId?.(newValue); }; + useEffect(() => { + if (selectedViewId === undefined && views.length > 0) { + setSelectedViewId?.(views[0].id); + } + }, [selectedViewId, setSelectedViewId, views]); + return ( - <div className="flex items-center -mb-px"> + <div className='-mb-px flex items-center border-b border-line-divider'> <div className='flex flex-1 items-center'> <ViewTabs value={selectedViewId} onChange={handleChange}> - {views.map(view => ( + {views.map((view) => ( <ViewTab key={view.id} icon={undefined} - iconPosition="start" - color="inherit" - label={view.name} + iconPosition='start' + color='inherit' + label={view.name || t('grid.title.placeholder')} value={view.id} /> ))} </ViewTabs> - <IconButton size="small"> - <AddSvg /> - </IconButton> + <AddViewBtn pageId={pageId} /> </div> - <Stack className="text-neutral-500" direction="row" spacing="2px"> - <TextButton color="inherit"> - {t('grid.settings.filter')} - </TextButton> - <TextButton color="inherit" onClick={handleClick}> - {t('grid.settings.sort')} - </TextButton> - <SortMenu open={open} anchorEl={sortAnchorEl} onClose={handleClose} /> - </Stack> </div> ); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/ViewTabs.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/ViewTabs.tsx index 4b1e3aea5545..fea626a2fcc0 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/ViewTabs.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/ViewTabs.tsx @@ -5,7 +5,7 @@ export const ViewTabs = styled(Tabs)({ '& .MuiTabs-scroller': { paddingBottom: '2px', - } + }, }); export const ViewTab = styled((props: TabProps) => <Tab disableRipple {...props} />)({ @@ -19,3 +19,26 @@ export const ViewTab = styled((props: TabProps) => <Tab disableRipple {...props} color: 'inherit', }, }); + +interface TabPanelProps { + children?: React.ReactNode; + index: number; + value: number; +} + +export function TabPanel(props: TabPanelProps) { + const { children, value, index, ...other } = props; + + return ( + <div + role='tabpanel' + hidden={value !== index} + id={`full-width-tabpanel-${index}`} + aria-labelledby={`full-width-tab-${index}`} + dir={'ltr'} + {...other} + > + {value === index && children} + </div> + ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/Grid/Grid.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/Grid/Grid.tsx index 4bb1dea82352..73e96c2fda2a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/Grid/Grid.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/Grid/Grid.tsx @@ -1,8 +1,11 @@ import { FC } from 'react'; import { GridTable } from '../GridTable'; +import GridUIProvider from '$app/components/database/proxy/grid/ui_state/Provider'; -export const Grid: FC = () => { +export const Grid: FC<{ isActivated: boolean; tableHeight: number }> = ({ isActivated, tableHeight }) => { return ( - <GridTable /> + <GridUIProvider isActivated={isActivated}> + <GridTable tableHeight={tableHeight} /> + </GridUIProvider> ); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridField.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridField.tsx index bef24ebe22e9..a63061b87cc5 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridField.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridField.tsx @@ -15,9 +15,9 @@ export interface GridFieldProps { export const GridField: FC<GridFieldProps> = ({ field }) => { const viewId = useViewId(); const { fields } = useDatabase(); - const [ openMenu, setOpenMenu ] = useState(false); - const [ openTooltip, setOpenTooltip ] = useState(false); - const [ dropPosition, setDropPosition ] = useState<DropPosition>(DropPosition.Before); + const [openMenu, setOpenMenu] = useState(false); + const [openTooltip, setOpenTooltip] = useState(false); + const [dropPosition, setDropPosition] = useState<DropPosition>(DropPosition.Before); const handleClick = useCallback(() => { setOpenMenu(true); @@ -35,17 +35,14 @@ export const GridField: FC<GridFieldProps> = ({ field }) => { setOpenTooltip(false); }, []); - const draggingData = useMemo(() => ({ - field, - }), [field]); - - const { - isDragging, - attributes, - listeners, - setPreviewRef, - previewRef, - } = useDraggable({ + const draggingData = useMemo( + () => ({ + field, + }), + [field] + ); + + const { isDragging, attributes, listeners, setPreviewRef, previewRef } = useDraggable({ type: DragType.Field, data: draggingData, scrollOnEdge: { @@ -68,23 +65,23 @@ export const GridField: FC<GridFieldProps> = ({ field }) => { }, 20); }, [previewRef]); - const onDrop = useCallback(({ data }: DragItem) => { - const dragField = data.field as Field; - const fromIndex = fields.findIndex(item => item.id === dragField.id); - const dropIndex = fields.findIndex(item => item.id === field.id); - const toIndex = dropIndex + dropPosition + (fromIndex < dropIndex ? -1 : 0); + const onDrop = useCallback( + ({ data }: DragItem) => { + const dragField = data.field as Field; + const fromIndex = fields.findIndex((item) => item.id === dragField.id); + const dropIndex = fields.findIndex((item) => item.id === field.id); + const toIndex = dropIndex + dropPosition + (fromIndex < dropIndex ? -1 : 0); - if (fromIndex === toIndex) { - return; - } + if (fromIndex === toIndex) { + return; + } - void fieldService.moveField(viewId, dragField.id, fromIndex, toIndex); - }, [viewId, field, fields, dropPosition]); + void fieldService.moveField(viewId, dragField.id, fromIndex, toIndex); + }, + [viewId, field, fields, dropPosition] + ); - const { - isOver, - listeners: dropListeners, - } = useDroppable({ + const { isOver, listeners: dropListeners } = useDroppable({ accept: DragType.Field, disabled: isDragging, onDragOver, @@ -96,35 +93,35 @@ export const GridField: FC<GridFieldProps> = ({ field }) => { <Tooltip open={openTooltip && !isDragging} title={field.name} - placement="right" + placement='right' enterDelay={1000} enterNextDelay={1000} onOpen={handleTooltipOpen} onClose={handleTooltipClose} > <Button + color={'inherit'} ref={setPreviewRef} - className="flex items-center px-2 w-full relative" + className='relative flex w-full items-center px-2' disableRipple onClick={handleClick} {...attributes} {...listeners} {...dropListeners} > - <FieldTypeSvg className="text-base mr-1" type={field.type} /> - <span className="flex-1 text-left text-xs truncate"> - {field.name} - </span> - {isOver && <div className={`absolute top-0 bottom-0 w-0.5 bg-blue-500 z-10 ${dropPosition === DropPosition.Before ? 'left-[-1px]' : 'left-full'}`} />} + <FieldTypeSvg className='mr-1 text-base' type={field.type} /> + <span className='flex-1 truncate text-left text-xs'>{field.name}</span> + {isOver && ( + <div + className={`absolute bottom-0 top-0 z-10 w-0.5 bg-blue-500 ${ + dropPosition === DropPosition.Before ? 'left-[-1px]' : 'left-full' + }`} + /> + )} </Button> </Tooltip> {openMenu && ( - <GridFieldMenu - field={field} - open={openMenu} - anchorEl={previewRef.current} - onClose={handleMenuClose} - /> + <GridFieldMenu field={field} open={openMenu} anchorEl={previewRef.current} onClose={handleMenuClose} /> )} </> ); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRow.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRow.hooks.ts new file mode 100644 index 000000000000..87d65ba0eba4 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRow.hooks.ts @@ -0,0 +1,71 @@ +import { useGridUIStateDispatcher, useGridUIStateSelector } from '$app/components/database/proxy/grid/ui_state/actions'; +import { CSSProperties, useCallback, useEffect, useMemo, useState } from 'react'; + +export function useGridRowActionsDisplay(rowId: string, ref: React.RefObject<HTMLDivElement>) { + const { hoverRowId, isActivated } = useGridUIStateSelector(); + const hover = useMemo(() => { + return isActivated && hoverRowId === rowId; + }, [hoverRowId, rowId, isActivated]); + + const { setRowHover } = useGridUIStateDispatcher(); + const [actionsStyle, setActionsStyle] = useState<CSSProperties | undefined>(); + + const onMouseEnter = useCallback(() => { + setRowHover(rowId); + }, [setRowHover, rowId]); + + useEffect(() => { + // Next frame to avoid layout thrashing + requestAnimationFrame(() => { + const element = ref.current; + + if (!hover || !element) { + setActionsStyle(undefined); + return; + } + + const rect = element.getBoundingClientRect(); + + setActionsStyle({ + position: 'absolute', + top: rect.top + 6, + left: rect.left - 50, + }); + }); + }, [ref, hover]); + + return { + actionsStyle, + onMouseEnter, + hover, + }; +} + +export const useGridRowContextMenu = () => { + const [position, setPosition] = useState<{ left: number; top: number } | undefined>(); + + const isContextMenuOpen = useMemo(() => { + return !!position; + }, [position]); + + const closeContextMenu = useCallback(() => { + setPosition(undefined); + }, []); + + const openContextMenu = useCallback((event: MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + + setPosition({ + left: event.clientX, + top: event.clientY, + }); + }, []); + + return { + isContextMenuOpen, + closeContextMenu, + openContextMenu, + position, + }; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRow.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRow.tsx index 5b4ee6fd236f..cc9e913a969b 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRow.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRow.tsx @@ -1,56 +1,58 @@ import { Virtualizer } from '@tanstack/react-virtual'; -import { IconButton, Tooltip } from '@mui/material'; -import { t } from 'i18next'; -import { DragEventHandler, FC, useCallback, useMemo, useState } from 'react'; -import { ReactComponent as DragSvg } from '$app/assets/drag.svg'; +import { Portal } from '@mui/material'; +import { DragEventHandler, FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { throttle } from '$app/utils/tool'; import { useViewId } from '$app/hooks'; import { useDatabase } from '../../../Database.hooks'; import { rowService, RowMeta } from '../../../application'; -import { DragItem, DragType, DropPosition, VirtualizedList, useDraggable, useDroppable, ScrollDirection } from '../../../_shared'; +import { + DragItem, + DragType, + DropPosition, + VirtualizedList, + useDraggable, + useDroppable, + ScrollDirection, +} from '../../../_shared'; import { GridCell } from '../../GridCell'; import { GridCellRowActions } from './GridCellRowActions'; +import { + useGridRowActionsDisplay, + useGridRowContextMenu, +} from '$app/components/database/grid/GridRow/GridCellRow/GridCellRow.hooks'; +import GridCellRowContextMenu from '$app/components/database/grid/GridRow/GridCellRow/GridCellRowContextMenu'; export interface GridCellRowProps { rowMeta: RowMeta; - virtualizer: Virtualizer<Element, Element>; + virtualizer: Virtualizer<HTMLDivElement, HTMLDivElement>; + getPrevRowId: (id: string) => string | null; } -export const GridCellRow: FC<GridCellRowProps> = ({ - rowMeta, - virtualizer, -}) => { +export const GridCellRow: FC<GridCellRowProps> = ({ rowMeta, virtualizer, getPrevRowId }) => { + const rowId = rowMeta.id; const viewId = useViewId(); + const ref = useRef<HTMLDivElement | null>(null); + const { onMouseEnter, actionsStyle, hover } = useGridRowActionsDisplay(rowId, ref); + const { + isContextMenuOpen, + closeContextMenu, + openContextMenu, + position: contextMenuPosition, + } = useGridRowContextMenu(); const { fields } = useDatabase(); - const [ hover, setHover ] = useState(false); - const [ openTooltip, setOpenTooltip ] = useState(false); - const [ dropPosition, setDropPosition ] = useState<DropPosition>(DropPosition.Before); - - const handleMouseEnter = useCallback(() => { - setHover(true); - }, []); - - const handleMouseLeave = useCallback(() => { - setHover(false); - }, []); - - const handleTooltipOpen = useCallback(() => { - setOpenTooltip(true); - }, []); - - const handleTooltipClose = useCallback(() => { - setOpenTooltip(false); - }, []); - - const dragData = useMemo(() => ({ - rowMeta, - }), [rowMeta]); + const [dropPosition, setDropPosition] = useState<DropPosition>(DropPosition.Before); + const dragData = useMemo( + () => ({ + rowMeta, + }), + [rowMeta] + ); const { isDragging, - attributes, - listeners, + attributes: dragAttributes, + listeners: dragListeners, setPreviewRef, previewRef, } = useDraggable({ @@ -76,65 +78,73 @@ export const GridCellRow: FC<GridCellRowProps> = ({ }, 20); }, [previewRef]); - const onDrop = useCallback(({ data }: DragItem) => { - void rowService.moveRow(viewId, (data.rowMeta as RowMeta).id, rowMeta.id); - }, [viewId, rowMeta.id]); + const onDrop = useCallback( + ({ data }: DragItem) => { + void rowService.moveRow(viewId, (data.rowMeta as RowMeta).id, rowMeta.id); + }, + [viewId, rowMeta.id] + ); - const { - isOver, - listeners: dropListeners, - } = useDroppable({ + const { isOver, listeners: dropListeners } = useDroppable({ accept: DragType.Row, disabled: isDragging, onDragOver, onDrop, }); + useEffect(() => { + const element = ref.current; + + if (!element) { + return; + } + + element.addEventListener('contextmenu', openContextMenu); + return () => { + element.removeEventListener('contextmenu', openContextMenu); + }; + }, [openContextMenu]); + return ( - <div - className="flex grow ml-[-49px]" - onMouseEnter={handleMouseEnter} - onMouseLeave={handleMouseLeave} - {...dropListeners} - > - <GridCellRowActions - className={hover ? 'visible' : 'invisible'} - rowId={rowMeta.id} - > - <Tooltip - placement="top" - title={t('grid.row.drag')} - open={openTooltip && !isDragging} - onOpen={handleTooltipOpen} - onClose={handleTooltipClose} - > - <IconButton - className="mx-1 cursor-grab active:cursor-grabbing" - {...attributes} - {...listeners} - > - <DragSvg className='-mx-1' /> - </IconButton> - </Tooltip> - </GridCellRowActions> + <div ref={ref} className='flex grow' onMouseEnter={onMouseEnter} {...dropListeners}> <div ref={setPreviewRef} - className={`flex grow border-b border-line-divider relative ${isDragging ? 'bg-blue-50' : ''}`} + className={`relative flex grow border-b border-line-divider ${isDragging ? 'bg-blue-50' : ''}`} > <VirtualizedList - className="flex" - itemClassName="flex border-r border-line-divider" + className='flex' + itemClassName='flex border-r border-line-divider' virtualizer={virtualizer} - renderItem={index => ( - <GridCell - rowId={rowMeta.id} - field={fields[index]} - /> - )} + renderItem={(index) => <GridCell rowId={rowMeta.id} field={fields[index]} />} /> - <div className="min-w-20 grow" /> - {isOver && <div className={`absolute left-0 right-0 h-0.5 bg-blue-500 z-10 ${dropPosition === DropPosition.Before ? 'top-[-1px]' : 'top-full'}`} />} + <div className='min-w-20 grow' /> + {isOver && ( + <div + className={`absolute left-0 right-0 z-10 h-0.5 bg-blue-500 ${ + dropPosition === DropPosition.Before ? 'top-[-1px]' : 'top-full' + }`} + /> + )} </div> + <Portal> + <GridCellRowActions + isHidden={!hover} + style={actionsStyle} + dragProps={{ + ...dragListeners, + ...dragAttributes, + }} + rowId={rowMeta.id} + getPrevRowId={getPrevRowId} + /> + <GridCellRowContextMenu + open={isContextMenuOpen} + onClose={closeContextMenu} + anchorPosition={contextMenuPosition} + rowId={rowId} + getPrevRowId={getPrevRowId} + /> + </Portal> </div> ); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRowActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRowActions.tsx index a9b71fb30d80..250c48339d8b 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRowActions.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRowActions.tsx @@ -1,36 +1,85 @@ import { IconButton, Tooltip } from '@mui/material'; -import { FC, PropsWithChildren, useCallback } from 'react'; +import { DragEventHandler, FC, HTMLAttributes, PropsWithChildren, useCallback, useRef, useState } from 'react'; import { t } from 'i18next'; import { ReactComponent as AddSvg } from '$app/assets/add.svg'; import { useViewId } from '$app/hooks'; import { rowService } from '../../../application'; +import { ReactComponent as DragSvg } from '$app/assets/drag.svg'; +import GridCellRowMenu from '$app/components/database/grid/GridRow/GridCellRow/GridCellRowMenu'; +import Popover from '@mui/material/Popover'; -export interface GridCellRowActionsProps { - className?: string; +export interface GridCellRowActionsProps extends HTMLAttributes<HTMLDivElement> { rowId: string; + getPrevRowId: (id: string) => string | null; + dragProps: { + draggable?: boolean; + onDragStart?: DragEventHandler; + onDragEnd?: DragEventHandler; + }; + isHidden?: boolean; } export const GridCellRowActions: FC<PropsWithChildren<GridCellRowActionsProps>> = ({ - className, + isHidden, rowId, - children, + getPrevRowId, + className, + dragProps: { draggable, onDragStart, onDragEnd }, + ...props }) => { const viewId = useViewId(); - - const handleInsertRowClick = useCallback(() => { + const ref = useRef<HTMLDivElement | null>(null); + const [openMenu, setOpenMenu] = useState(false); + const handleInsertRecordBelow = useCallback(() => { void rowService.createRow(viewId, { startRowId: rowId, }); }, [viewId, rowId]); + const handleOpenMenu = () => { + setOpenMenu(true); + }; + + const handleCloseMenu = () => { + setOpenMenu(false); + }; + + if (isHidden) return null; + return ( - <div className={`inline-flex items-center ${className}`}> - <Tooltip placement="top" title={t('grid.row.add')}> - <IconButton onClick={handleInsertRowClick}> + <div ref={ref} className={`relative inline-flex items-center ${className || ''}`} {...props}> + <Tooltip placement='top' title={t('grid.row.add')}> + <IconButton onClick={handleInsertRecordBelow}> <AddSvg /> </IconButton> </Tooltip> - {children} + <Tooltip placement='top' title={t('grid.row.dragAndClick')}> + <IconButton + onClick={handleOpenMenu} + className='mx-1 cursor-grab active:cursor-grabbing' + draggable={draggable} + onDragStart={onDragStart} + onDragEnd={onDragEnd} + > + <DragSvg className='-mx-1' /> + </IconButton> + </Tooltip> + <Popover + open={openMenu} + onClose={handleCloseMenu} + transformOrigin={{ + vertical: 'center', + horizontal: 'left', + }} + anchorOrigin={{ + vertical: 'center', + horizontal: 'right', + }} + container={ref.current} + anchorEl={ref.current} + > + <GridCellRowMenu onClickItem={() => handleCloseMenu} rowId={rowId} getPrevRowId={getPrevRowId} /> + </Popover> </div> ); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRowContextMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRowContextMenu.tsx new file mode 100644 index 000000000000..f045991c7cda --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRowContextMenu.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import GridCellRowMenu from '$app/components/database/grid/GridRow/GridCellRow/GridCellRowMenu'; +import Popover from '@mui/material/Popover'; + +interface Props { + open: boolean; + onClose: () => void; + anchorPosition?: { + top: number; + left: number; + }; + rowId: string; + getPrevRowId: (id: string) => string | null; +} + +function GridCellRowContextMenu({ open, anchorPosition, onClose, rowId, getPrevRowId }: Props) { + return ( + <Popover + open={open} + onClose={onClose} + anchorPosition={anchorPosition} + anchorReference={'anchorPosition'} + transformOrigin={{ vertical: 'top', horizontal: 'left' }} + > + <GridCellRowMenu + rowId={rowId} + getPrevRowId={getPrevRowId} + onClickItem={() => { + onClose(); + }} + /> + </Popover> + ); +} + +export default GridCellRowContextMenu; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRowMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRowMenu.tsx new file mode 100644 index 000000000000..55664ac48b7a --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRowMenu.tsx @@ -0,0 +1,96 @@ +import React, { useCallback } from 'react'; +import { MenuList, MenuItem, Icon } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as UpSvg } from '$app/assets/up.svg'; +import { ReactComponent as AddSvg } from '$app/assets/add.svg'; +import { ReactComponent as DelSvg } from '$app/assets/delete.svg'; +import { ReactComponent as CopySvg } from '$app/assets/copy.svg'; +import { rowService } from '$app/components/database/application'; +import { useViewId } from '@/appflowy_app/hooks/ViewId.hooks'; + +interface Props { + rowId: string; + getPrevRowId: (id: string) => string | null; + onClickItem: (label: string) => void; +} + +interface Option { + label: string; + icon: JSX.Element; + onClick: () => void; + divider?: boolean; +} + +function GridCellRowMenu({ rowId, getPrevRowId, onClickItem }: Props) { + const viewId = useViewId(); + + const { t } = useTranslation(); + + const handleInsertRecordBelow = useCallback(() => { + void rowService.createRow(viewId, { + startRowId: rowId, + }); + }, [viewId, rowId]); + + const handleInsertRecordAbove = useCallback(() => { + const prevRowId = getPrevRowId(rowId); + + void rowService.createRow(viewId, { + startRowId: prevRowId || undefined, + }); + }, [getPrevRowId, rowId, viewId]); + + const handleDelRow = useCallback(() => { + void rowService.deleteRow(viewId, rowId); + }, [viewId, rowId]); + + const handleDuplicateRow = useCallback(() => { + void rowService.duplicateRow(viewId, rowId); + }, [viewId, rowId]); + + const options: Option[] = [ + { + label: t('grid.row.insertRecordAbove'), + icon: <UpSvg />, + onClick: handleInsertRecordAbove, + }, + { + label: t('grid.row.insertRecordBelow'), + icon: <AddSvg />, + onClick: handleInsertRecordBelow, + }, + { + label: t('grid.row.duplicate'), + icon: <CopySvg />, + onClick: handleDuplicateRow, + }, + + { + label: t('grid.row.delete'), + icon: <DelSvg />, + onClick: handleDelRow, + divider: true, + }, + ]; + + return ( + <MenuList> + {options.map((option) => ( + <div className={'w-full'} key={option.label}> + {option.divider && <div className='mx-2 my-1.5 h-[1px] bg-line-divider' />} + <MenuItem + onClick={() => { + option.onClick(); + onClickItem(option.label); + }} + > + <Icon className='mr-2'>{option.icon}</Icon> + {option.label} + </MenuItem> + </div> + ))} + </MenuList> + ); +} + +export default GridCellRowMenu; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridFieldRow.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridFieldRow.tsx index 6d37714cf18d..dc819e4ab8a3 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridFieldRow.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridFieldRow.tsx @@ -8,14 +8,14 @@ import { useDatabase } from '../../Database.hooks'; import { VirtualizedList } from '../../_shared'; import { GridField } from '../GridField'; import { useViewId } from '@/appflowy_app/hooks'; +import { useTranslation } from 'react-i18next'; export interface GridFieldRowProps { - virtualizer: Virtualizer<Element, Element>; + virtualizer: Virtualizer<HTMLDivElement, HTMLDivElement>; } -export const GridFieldRow: FC<GridFieldRowProps> = ({ - virtualizer, -}) => { +export const GridFieldRow: FC<GridFieldRowProps> = ({ virtualizer }) => { + const { t } = useTranslation(); const viewId = useViewId(); const { fields } = useDatabase(); const handleClick = async () => { @@ -23,20 +23,23 @@ export const GridFieldRow: FC<GridFieldRowProps> = ({ }; return ( - <div className="flex grow border-b border-line-divider"> + <div className='z-10 flex border-b border-line-divider'> <VirtualizedList - className="flex" + className='flex' virtualizer={virtualizer} - itemClassName="flex border-r border-line-divider" - renderItem={index => <GridField field={fields[index]} />} + itemClassName='flex border-r border-line-divider' + renderItem={(index) => <GridField field={fields[index]} />} /> - <div className="min-w-20 grow"> + <div className='min-w-20 grow'> <Button - className="w-full h-full" - size="small" + color={'inherit'} + className='flex h-full w-full items-center justify-start whitespace-nowrap text-left' + size='small' startIcon={<AddSvg />} onClick={handleClick} - /> + > + {t('grid.field.newColumn')} + </Button> </div> </div> ); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridNewRow.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridNewRow.tsx index 6bd080628e8f..051adde067d2 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridNewRow.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridNewRow.tsx @@ -10,10 +10,7 @@ export interface GridNewRowProps { groupId?: string; } -export const GridNewRow: FC<GridNewRowProps> = ({ - startRowId, - groupId, -}) => { +export const GridNewRow: FC<GridNewRowProps> = ({ startRowId, groupId }) => { const viewId = useViewId(); const handleClick = useCallback(() => { @@ -24,13 +21,10 @@ export const GridNewRow: FC<GridNewRowProps> = ({ }, [viewId, groupId, startRowId]); return ( - <div className="flex grow border-b border-line-divider"> - <Button - className="grow justify-start" - onClick={handleClick} - > - <span className="inline-flex items-center sticky left-2"> - <AddSvg className="text-base mr-1" /> + <div className='flex grow border-b border-line-divider'> + <Button className='grow justify-start' onClick={handleClick} color={'inherit'}> + <span className='sticky left-2 inline-flex items-center'> + <AddSvg className='mr-1 text-base' /> {t('grid.row.newRow')} </span> </Button> diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridRow.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridRow.tsx index aaad94ee045d..b3e53076e3df 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridRow.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridRow.tsx @@ -8,22 +8,14 @@ import { GridCalculateRow } from './GridCalculateRow'; export interface GridRowProps { row: RenderRow; - virtualizer: Virtualizer<Element, Element>; + virtualizer: Virtualizer<HTMLDivElement, HTMLDivElement>; + getPrevRowId: (id: string) => string | null; } -export const GridRow: FC<GridRowProps> = ({ - row, - virtualizer, -}) => { - +export const GridRow: FC<GridRowProps> = ({ row, virtualizer, getPrevRowId }) => { switch (row.type) { case RenderRowType.Row: - return ( - <GridCellRow - rowMeta={row.data.meta} - virtualizer={virtualizer} - /> - ); + return <GridCellRow rowMeta={row.data.meta} virtualizer={virtualizer} getPrevRowId={getPrevRowId} />; case RenderRowType.Fields: return <GridFieldRow virtualizer={virtualizer} />; case RenderRowType.NewRow: diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/constants.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/constants.ts index 71a2745046c2..2bb6137d1d94 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/constants.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/constants.ts @@ -14,7 +14,7 @@ export interface FieldRenderRow { export interface CellRenderRow { type: RenderRowType.Row; data: { - meta: RowMeta, + meta: RowMeta; }; } @@ -23,7 +23,7 @@ export interface NewRenderRow { data: { startRowId?: string; groupId?: string; - }, + }; } export interface CalculateRenderRow { @@ -37,7 +37,7 @@ export const rowMetasToRenderRow = (rowMetas: RowMeta[]): RenderRow[] => { { type: RenderRowType.Fields, }, - ...rowMetas.map<RenderRow>(rowMeta => ({ + ...rowMetas.map<RenderRow>((rowMeta) => ({ type: RenderRowType.Row, data: { meta: rowMeta, @@ -54,4 +54,3 @@ export const rowMetasToRenderRow = (rowMetas: RowMeta[]): RenderRow[] => { }, ]; }; - diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridTable/GridTable.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridTable/GridTable.tsx index 338370a121bc..84b8dcd513ef 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridTable/GridTable.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridTable/GridTable.tsx @@ -1,7 +1,7 @@ import { useVirtualizer } from '@tanstack/react-virtual'; import { FC, useMemo, useRef } from 'react'; import { RowMeta } from '../../application'; -import { useDatabase, useVerticalScrollElement } from '../../Database.hooks'; +import { useDatabase } from '../../Database.hooks'; import { VirtualizedList } from '../../_shared'; import { GridRow, RenderRow, RenderRowType, rowMetasToRenderRow } from '../GridRow'; @@ -13,44 +13,62 @@ const getRenderRowKey = (row: RenderRow) => { return row.type; }; -export const GridTable: FC = () => { - const verticalScrollElementRef = useVerticalScrollElement(); - const horizontalScrollElementRef = useRef<HTMLDivElement>(null); +export const GridTable: FC<{ tableHeight: number }> = ({ tableHeight }) => { + const verticalScrollElementRef = useRef<HTMLDivElement | null>(null); + const horizontalScrollElementRef = useRef<HTMLDivElement | null>(null); const { rowMetas, fields } = useDatabase(); - const renderRows = useMemo<RenderRow[]>(() => rowMetasToRenderRow(rowMetas as RowMeta[]), [rowMetas]); - const rowVirtualizer = useVirtualizer({ + const rowVirtualizer = useVirtualizer<HTMLDivElement, HTMLDivElement>({ count: renderRows.length, - overscan: 10, - getItemKey: i => getRenderRowKey(renderRows[i]), + overscan: 20, + getItemKey: (i) => getRenderRowKey(renderRows[i]), getScrollElement: () => verticalScrollElementRef.current, estimateSize: () => 37, }); - const columnVirtualizer = useVirtualizer<Element, Element>({ + const columnVirtualizer = useVirtualizer<HTMLDivElement, HTMLDivElement>({ horizontal: true, count: fields.length, overscan: 5, - getItemKey: i => fields[i].id, + getItemKey: (i) => fields[i].id, getScrollElement: () => horizontalScrollElementRef.current, estimateSize: (i) => fields[i].width ?? 201, }); + const getPrevRowId = (id: string) => { + const index = rowMetas.findIndex((rowMeta) => rowMeta.id === id); + + if (index === 0) { + return null; + } + + return rowMetas[index - 1].id; + }; + return ( <div - ref={horizontalScrollElementRef} - className="flex w-full overflow-x-auto px-16" - style={{ minHeight: 'calc(100% - 132px)' }} + style={{ + height: tableHeight, + }} + className={'flex w-full flex-col'} > - <VirtualizedList - className="flex flex-col basis-full" - virtualizer={rowVirtualizer} - itemClassName="flex" - renderItem={index => ( - <GridRow row={renderRows[index]} virtualizer={columnVirtualizer} /> - )} - /> + <div + className={'w-full flex-1 overflow-auto'} + ref={(e) => { + verticalScrollElementRef.current = e; + horizontalScrollElementRef.current = e; + }} + > + <VirtualizedList + className='flex w-fit basis-full flex-col' + virtualizer={rowVirtualizer} + itemClassName='flex' + renderItem={(index) => ( + <GridRow getPrevRowId={getPrevRowId} row={renderRows[index]} virtualizer={columnVirtualizer} /> + )} + /> + </div> </div> ); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/proxy/grid/ui_state/Provider.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/proxy/grid/ui_state/Provider.tsx new file mode 100644 index 000000000000..b46247649438 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/proxy/grid/ui_state/Provider.tsx @@ -0,0 +1,14 @@ +import React, { useEffect } from 'react'; +import { GridUIContext, useProxyGridUIState } from '$app/components/database/proxy/grid/ui_state/actions'; + +function GridUIProvider({ children, isActivated }: { children: React.ReactNode; isActivated: boolean }) { + const context = useProxyGridUIState(); + + useEffect(() => { + context.isActivated = isActivated; + }, [isActivated, context]); + + return <GridUIContext.Provider value={context}>{children}</GridUIContext.Provider>; +} + +export default GridUIProvider; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/proxy/grid/ui_state/actions.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/proxy/grid/ui_state/actions.ts new file mode 100644 index 000000000000..65ecee0570dd --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/proxy/grid/ui_state/actions.ts @@ -0,0 +1,46 @@ +import { useMemo, useContext, createContext, useCallback } from 'react'; +import { proxy, useSnapshot } from 'valtio'; + +export interface GridUIContextState { + hoverRowId: string | null; + isActivated: boolean; +} + +const initialUIState: GridUIContextState = { + hoverRowId: null, + isActivated: false, +}; + +function proxyGridUIState(state: GridUIContextState) { + return proxy<GridUIContextState>(state); +} + +export const GridUIContext = createContext<GridUIContextState>(proxyGridUIState(initialUIState)); + +export function useProxyGridUIState() { + const context = useMemo<GridUIContextState>(() => { + return proxyGridUIState({ + ...initialUIState, + }); + }, []); + + return context; +} + +export function useGridUIStateSelector() { + return useSnapshot(useContext(GridUIContext)); +} + +export function useGridUIStateDispatcher() { + const context = useContext(GridUIContext); + const setRowHover = useCallback( + (rowId: string | null) => { + context.hoverRowId = rowId; + }, + [context] + ); + + return { + setRowHover, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideToolbar/BlockSideToolbar.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideToolbar/BlockSideToolbar.hooks.tsx index 274ab960de8e..9ccd26389934 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideToolbar/BlockSideToolbar.hooks.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideToolbar/BlockSideToolbar.hooks.tsx @@ -39,6 +39,10 @@ export function useBlockSideToolbar(id: string) { return -6; } + if (block.type === BlockType.GridBlock) { + return 16; + } + return 0; }, [docId, id]); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideToolbar/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideToolbar/index.tsx index a086fed1dce3..ed7114f0d8c3 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideToolbar/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSideToolbar/index.tsx @@ -75,14 +75,13 @@ export default function BlockSideToolbar({ id }: { id: string }) { }} data-draggable-anchor={id} onClick={async (e: React.MouseEvent<HTMLButtonElement>) => { + handleOpen(e); await dispatch( setRectSelectionThunk({ docId, selection: [id], }) ); - - handleOpen(e); }} sx={{ height: 24, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSlash/BlockSlashMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSlash/BlockSlashMenu.tsx index 583d57594db3..fcee83dc258b 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSlash/BlockSlashMenu.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSlash/BlockSlashMenu.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useRef } from 'react'; +import React, { useCallback, useMemo } from 'react'; import MenuItem from '$app/components/document/_shared/MenuItem'; import { ArrowRight, @@ -13,6 +13,7 @@ import { SafetyDivider, Image, Functions, + BackupTableOutlined, } from '@mui/icons-material'; import { BlockData, @@ -23,29 +24,29 @@ import { } from '$app/interfaces/document'; import { useAppDispatch } from '$app/stores/store'; import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks'; -import { slashCommandActions } from '$app_reducers/document/slice'; -import { Keyboard } from '$app/constants/document/keyboard'; -import { selectOptionByUpDown } from '$app/utils/document/menu'; import { turnToBlockThunk } from '$app_reducers/document/async-actions'; import { useTranslation } from 'react-i18next'; +import { useKeyboardShortcut } from '$app/components/document/BlockSlash/index.hooks'; function BlockSlashMenu({ id, onClose, searchText, hoverOption, + onHoverOption, container, }: { id: string; onClose?: () => void; searchText?: string; hoverOption?: SlashCommandOption; + onHoverOption: (option: SlashCommandOption, target: HTMLElement) => void; container: HTMLDivElement; }) { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const ref = useRef<HTMLDivElement | null>(null); - const { docId, controller } = useSubscribeDocument(); + const { controller } = useSubscribeDocument(); + const handleInsert = useCallback( async (type: BlockType, data?: BlockData) => { if (!controller) return; @@ -72,14 +73,14 @@ function BlockSlashMenu({ { key: SlashCommandOptionKey.TEXT, type: BlockType.TextBlock, - title: 'Text', + title: t('editor.text'), icon: <TextFields />, group: SlashCommandGroup.BASIC, }, { key: SlashCommandOptionKey.HEADING_1, type: BlockType.HeadingBlock, - title: 'Heading 1', + title: t('editor.heading1'), icon: <Title />, data: { level: 1, @@ -89,7 +90,7 @@ function BlockSlashMenu({ { key: SlashCommandOptionKey.HEADING_2, type: BlockType.HeadingBlock, - title: 'Heading 2', + title: t('editor.heading2'), icon: <Title />, data: { level: 2, @@ -99,7 +100,7 @@ function BlockSlashMenu({ { key: SlashCommandOptionKey.HEADING_3, type: BlockType.HeadingBlock, - title: 'Heading 3', + title: t('editor.heading3'), icon: <Title />, data: { level: 3, @@ -109,35 +110,35 @@ function BlockSlashMenu({ { key: SlashCommandOptionKey.TODO, type: BlockType.TodoListBlock, - title: 'To-do list', + title: t('editor.checkbox'), icon: <Check />, group: SlashCommandGroup.BASIC, }, { key: SlashCommandOptionKey.BULLET, type: BlockType.BulletedListBlock, - title: 'Bulleted list', + title: t('editor.bulletedList'), icon: <FormatListBulleted />, group: SlashCommandGroup.BASIC, }, { key: SlashCommandOptionKey.NUMBER, type: BlockType.NumberedListBlock, - title: 'Numbered list', + title: t('editor.numberedList'), icon: <FormatListNumbered />, group: SlashCommandGroup.BASIC, }, { key: SlashCommandOptionKey.TOGGLE, type: BlockType.ToggleListBlock, - title: 'Toggle list', + title: t('document.plugins.toggleList'), icon: <ArrowRight />, group: SlashCommandGroup.BASIC, }, { key: SlashCommandOptionKey.QUOTE, type: BlockType.QuoteBlock, - title: 'Quote', + title: t('toolbar.quote'), icon: <FormatQuote />, group: SlashCommandGroup.BASIC, }, @@ -151,31 +152,41 @@ function BlockSlashMenu({ { key: SlashCommandOptionKey.DIVIDER, type: BlockType.DividerBlock, - title: 'Divider', + title: t('editor.divider'), icon: <SafetyDivider />, group: SlashCommandGroup.BASIC, }, { key: SlashCommandOptionKey.CODE, type: BlockType.CodeBlock, - title: 'Code', + title: t('document.selectionMenu.codeBlock'), icon: <DataObject />, group: SlashCommandGroup.MEDIA, }, { key: SlashCommandOptionKey.IMAGE, type: BlockType.ImageBlock, - title: 'Image', + title: t('editor.image'), icon: <Image />, group: SlashCommandGroup.MEDIA, }, { key: SlashCommandOptionKey.EQUATION, type: BlockType.EquationBlock, - title: 'Block equation', + title: t('document.plugins.mathEquation.addMathEquation'), icon: <Functions />, group: SlashCommandGroup.ADVANCED, }, + { + key: SlashCommandOptionKey.GRID_REFERENCE, + type: BlockType.GridBlock, + title: t('document.plugins.referencedGrid'), + icon: <BackupTableOutlined />, + group: SlashCommandGroup.ADVANCED, + onClick: () => { + // do nothing + }, + }, ].filter((option) => { if (!searchText) return true; const match = (text: string) => { @@ -184,9 +195,16 @@ function BlockSlashMenu({ return match(option.title) || match(option.type); }), - [searchText] + [searchText, t] ); + const { ref } = useKeyboardShortcut({ + container, + options, + handleInsert, + hoverOption, + }); + const optionsByGroup = useMemo(() => { return options.reduce((acc, option) => { if (!acc[option.group]) { @@ -198,89 +216,6 @@ function BlockSlashMenu({ }, {} as Record<SlashCommandGroup, typeof options>); }, [options]); - const scrollIntoOption = useCallback((option: SlashCommandOption) => { - if (!ref.current) return; - const containerRect = ref.current.getBoundingClientRect(); - const optionElement = document.querySelector(`#slash-item-${option.key}`); - - if (!optionElement) return; - const itemRect = optionElement?.getBoundingClientRect(); - - if (!itemRect) return; - - if (itemRect.top < containerRect.top || itemRect.bottom > containerRect.bottom) { - optionElement.scrollIntoView({ behavior: 'smooth' }); - } - }, []); - - const selectOptionByArrow = useCallback( - ({ isUp = false, isDown = false }: { isUp?: boolean; isDown?: boolean }) => { - if (!isUp && !isDown) return; - const optionsKeys = options.map((option) => String(option.key)); - const nextKey = selectOptionByUpDown(isUp, String(hoverOption?.key), optionsKeys); - const nextOption = options.find((option) => String(option.key) === nextKey); - - if (!nextOption) return; - - scrollIntoOption(nextOption); - dispatch( - slashCommandActions.setHoverOption({ - option: nextOption, - docId, - }) - ); - }, - [dispatch, docId, hoverOption?.key, options, scrollIntoOption] - ); - - useEffect(() => { - const handleKeyDownCapture = (e: KeyboardEvent) => { - const isUp = e.key === Keyboard.keys.UP; - const isDown = e.key === Keyboard.keys.DOWN; - const isEnter = e.key === Keyboard.keys.ENTER; - - // if any arrow key is pressed, prevent default behavior and stop propagation - if (isUp || isDown || isEnter) { - e.stopPropagation(); - e.preventDefault(); - if (isEnter) { - if (hoverOption) { - void handleInsert(hoverOption.type, hoverOption.data); - } - - return; - } - - selectOptionByArrow({ - isUp, - isDown, - }); - } - }; - - // intercept keydown event in capture phase before it reaches the editor - container.addEventListener('keydown', handleKeyDownCapture, true); - return () => { - container.removeEventListener('keydown', handleKeyDownCapture, true); - }; - }, [container, handleInsert, hoverOption, selectOptionByArrow]); - - const onHoverOption = useCallback( - (option: SlashCommandOption) => { - dispatch( - slashCommandActions.setHoverOption({ - option: { - key: option.key, - type: option.type, - data: option.data, - }, - docId, - }) - ); - }, - [dispatch, docId] - ); - const renderEmptyContent = useCallback(() => { return ( <div className={'m-5 flex items-center justify-center text-text-caption'}>{t('findAndReplace.noResult')}</div> @@ -309,12 +244,17 @@ function BlockSlashMenu({ key={option.key} title={option.title} icon={option.icon} - onHover={() => { - onHoverOption(option); + onHover={(e) => { + onHoverOption(option, e.currentTarget as HTMLElement); }} isHovered={hoverOption?.key === option.key} onClick={() => { - void handleInsert(option.type, option.data); + if (!option.onClick) { + void handleInsert(option.type, option.data); + return; + } + + option.onClick(); }} /> ); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSlash/index.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSlash/index.hooks.ts index 10fa7a0297f7..fc9ccd719ecc 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSlash/index.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSlash/index.hooks.ts @@ -1,9 +1,101 @@ import { useAppDispatch } from '$app/stores/store'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { slashCommandActions } from '$app_reducers/document/slice'; import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks'; import { useSubscribeSlashState } from '$app/components/document/_shared/SubscribeSlash.hooks'; import { useSubscribePanelSearchText } from '$app/components/document/_shared/usePanelSearchText'; +import { BlockData, BlockType, SlashCommandOption, SlashCommandOptionKey } from '$app/interfaces/document'; +import { selectOptionByUpDown } from '$app/utils/document/menu'; +import { Keyboard } from '$app/constants/document/keyboard'; + +export function useKeyboardShortcut({ + container, + options, + handleInsert, + hoverOption, +}: { + container: HTMLElement; + options: SlashCommandOption[]; + handleInsert: (type: BlockType, data?: BlockData) => Promise<void>; + hoverOption?: SlashCommandOption; +}) { + const ref = useRef<HTMLDivElement | null>(null); + const dispatch = useAppDispatch(); + const { docId } = useSubscribeDocument(); + const scrollIntoOption = useCallback( + (option: SlashCommandOption) => { + if (!ref.current) return; + const containerRect = ref.current.getBoundingClientRect(); + const optionElement = document.querySelector(`#slash-item-${option.key}`); + + if (!optionElement) return; + const itemRect = optionElement?.getBoundingClientRect(); + + if (!itemRect) return; + + if (itemRect.top < containerRect.top || itemRect.bottom > containerRect.bottom) { + optionElement.scrollIntoView({ behavior: 'smooth' }); + } + }, + [ref] + ); + + const selectOptionByArrow = useCallback( + ({ isUp = false, isDown = false }: { isUp?: boolean; isDown?: boolean }) => { + if (!isUp && !isDown) return; + const optionsKeys = options.map((option) => String(option.key)); + const nextKey = selectOptionByUpDown(isUp, String(hoverOption?.key), optionsKeys); + const nextOption = options.find((option) => String(option.key) === nextKey); + + if (!nextOption) return; + + scrollIntoOption(nextOption); + dispatch( + slashCommandActions.setHoverOption({ + option: nextOption, + docId, + }) + ); + }, + [dispatch, docId, hoverOption?.key, options, scrollIntoOption] + ); + + useEffect(() => { + const handleKeyDownCapture = (e: KeyboardEvent) => { + const isUp = e.key === Keyboard.keys.UP; + const isDown = e.key === Keyboard.keys.DOWN; + const isEnter = e.key === Keyboard.keys.ENTER; + + // if any arrow key is pressed, prevent default behavior and stop propagation + if (isUp || isDown || isEnter) { + e.stopPropagation(); + e.preventDefault(); + if (isEnter) { + if (hoverOption) { + void handleInsert(hoverOption.type, hoverOption.data); + } + + return; + } + + selectOptionByArrow({ + isUp, + isDown, + }); + } + }; + + // intercept keydown event in capture phase before it reaches the editor + container.addEventListener('keydown', handleKeyDownCapture, true); + return () => { + container.removeEventListener('keydown', handleKeyDownCapture, true); + }; + }, [container, handleInsert, hoverOption, selectOptionByArrow]); + + return { + ref, + }; +} export function useBlockSlash() { const dispatch = useAppDispatch(); @@ -13,6 +105,10 @@ export function useBlockSlash() { top: number; left: number; }>(); + const [subMenuAnchorPosition, setSubMenuAnchorPosition] = useState<{ + top: number; + left: number; + }>(); useEffect(() => { if (blockId && visible) { @@ -41,11 +137,42 @@ export function useBlockSlash() { }, [slashText]); const onClose = useCallback(() => { + setSubMenuAnchorPosition(undefined); dispatch(slashCommandActions.closeSlashCommand(docId)); }, [dispatch, docId]); const open = Boolean(anchorPosition); + const onHoverOption = useCallback( + (option: SlashCommandOption, target: HTMLElement) => { + setSubMenuAnchorPosition(undefined); + dispatch( + slashCommandActions.setHoverOption({ + option: { + key: option.key, + type: option.type, + data: option.data, + }, + docId, + }) + ); + + if (option.key === SlashCommandOptionKey.GRID_REFERENCE) { + const rect = target.getBoundingClientRect(); + + setSubMenuAnchorPosition({ + top: rect.top, + left: rect.right, + }); + } + }, + [dispatch, docId] + ); + + const onCloseSubMenu = useCallback(() => { + setSubMenuAnchorPosition(undefined); + }, []); + return { open, anchorPosition, @@ -53,6 +180,9 @@ export function useBlockSlash() { blockId, searchText, hoverOption, + onHoverOption, + onCloseSubMenu, + subMenuAnchorPosition, }; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSlash/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSlash/index.tsx index 9bdf418b2897..1566b1156b60 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSlash/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/BlockSlash/index.tsx @@ -1,10 +1,33 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import Popover from '@mui/material/Popover'; import BlockSlashMenu from '$app/components/document/BlockSlash/BlockSlashMenu'; import { useBlockSlash } from '$app/components/document/BlockSlash/index.hooks'; +import { SlashCommandOptionKey } from '$app/interfaces/document'; +import DatabaseList from '$app/components/document/_shared/DatabaseList'; +import { ViewLayoutPB } from '@/services/backend'; function BlockSlash({ container }: { container: HTMLDivElement }) { - const { blockId, open, onClose, anchorPosition, searchText, hoverOption } = useBlockSlash(); + const { + blockId, + open, + onClose, + anchorPosition, + searchText, + hoverOption, + onHoverOption, + subMenuAnchorPosition, + onCloseSubMenu, + } = useBlockSlash(); + + const renderSubMenu = useCallback(() => { + if (!blockId) return null; + switch (hoverOption?.key) { + case SlashCommandOptionKey.GRID_REFERENCE: + return <DatabaseList onClose={onClose} blockId={blockId} layout={ViewLayoutPB.Grid} searchText={searchText} />; + default: + return null; + } + }, [blockId, hoverOption?.key, onClose, searchText]); if (!blockId) return null; @@ -26,7 +49,29 @@ function BlockSlash({ container }: { container: HTMLDivElement }) { id={blockId} onClose={onClose} searchText={searchText} + onHoverOption={onHoverOption} /> + <Popover + transformOrigin={{ + vertical: 'top', + horizontal: 'left', + }} + disableAutoFocus + sx={{ + pointerEvents: 'none', + }} + PaperProps={{ + style: { + pointerEvents: 'auto', + }, + }} + open={!!subMenuAnchorPosition} + anchorReference={'anchorPosition'} + anchorPosition={subMenuAnchorPosition} + onClose={onCloseSubMenu} + > + {renderSubMenu()} + </Popover> </Popover> ); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/GridBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/GridBlock/index.tsx new file mode 100644 index 000000000000..1f689943b590 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/GridBlock/index.tsx @@ -0,0 +1,36 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { BlockType, NestedBlock } from '$app/interfaces/document'; +import { Database } from '$app/components/database'; +import { ViewIdProvider } from '@/appflowy_app/hooks'; + +function GridBlock({ node }: { node: NestedBlock<BlockType.GridBlock> }) { + const viewId = node.data.viewId; + const ref = useRef<HTMLDivElement>(null); + const [selectedViewId, onChangeSelectedViewId] = useState(viewId); + + useEffect(() => { + const element = ref.current; + + if (!element) return; + + const resizeObserver = new ResizeObserver(() => { + element.style.minHeight = `${element.clientHeight}px`; + }); + + resizeObserver.observe(element); + + return () => { + resizeObserver.disconnect(); + }; + }, []); + + return ( + <div className='flex h-[400px] overflow-hidden py-3 caret-text-title' ref={ref}> + <ViewIdProvider value={viewId}> + <Database selectedViewId={selectedViewId} setSelectedViewId={onChangeSelectedViewId} /> + </ViewIdProvider> + </div> + ); +} + +export default GridBlock; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx index f157619afe66..46f394a6be64 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx @@ -19,6 +19,8 @@ import CodeBlock from '$app/components/document/CodeBlock'; import { NodeIdContext } from '$app/components/document/_shared/SubscribeNode.hooks'; import EquationBlock from '$app/components/document/EquationBlock'; import ImageBlock from '$app/components/document/ImageBlock'; +import GridBlock from '$app/components/document/GridBlock'; + import { useTranslation } from 'react-i18next'; import BlockDraggable from '$app/components/_shared/BlockDraggable'; import { BlockDraggableType } from '$app_reducers/block-draggable/slice'; @@ -28,41 +30,31 @@ function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<H const renderBlock = useCallback(() => { switch (node.type) { - case BlockType.TextBlock: { + case BlockType.TextBlock: return <TextBlock node={node} childIds={childIds} />; - } - case BlockType.HeadingBlock: { + case BlockType.HeadingBlock: return <HeadingBlock node={node} />; - } - case BlockType.TodoListBlock: { + case BlockType.TodoListBlock: return <TodoListBlock node={node} childIds={childIds} />; - } - case BlockType.QuoteBlock: { + case BlockType.QuoteBlock: return <QuoteBlock node={node} childIds={childIds} />; - } - - case BlockType.BulletedListBlock: { + case BlockType.BulletedListBlock: return <BulletedListBlock node={node} childIds={childIds} />; - } - case BlockType.NumberedListBlock: { + case BlockType.NumberedListBlock: return <NumberedListBlock node={node} childIds={childIds} />; - } - case BlockType.ToggleListBlock: { + case BlockType.ToggleListBlock: return <ToggleListBlock node={node} childIds={childIds} />; - } - case BlockType.DividerBlock: { + case BlockType.DividerBlock: return <DividerBlock />; - } - case BlockType.CalloutBlock: { + case BlockType.CalloutBlock: return <CalloutBlock node={node} childIds={childIds} />; - } case BlockType.CodeBlock: return <CodeBlock node={node} />; @@ -70,6 +62,8 @@ function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<H return <EquationBlock node={node} />; case BlockType.ImageBlock: return <ImageBlock node={node} />; + case BlockType.GridBlock: + return <GridBlock node={node} />; default: return <UnSupportedBlock />; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/DatabaseList/index.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/DatabaseList/index.hooks.ts new file mode 100644 index 000000000000..e9af1503128a --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/DatabaseList/index.hooks.ts @@ -0,0 +1,26 @@ +import { useAppSelector } from '$app/stores/store'; +import { useEffect, useState } from 'react'; +import { Page } from '$app_reducers/pages/slice'; +import { ViewLayoutPB } from '@/services/backend'; + +export function useLoadDatabaseList({ searchText, layout }: { searchText: string; layout: ViewLayoutPB }) { + const [list, setList] = useState<Page[]>([]); + const pages = useAppSelector((state) => state.pages.pageMap); + + useEffect(() => { + const list = Object.values(pages) + .map((page) => { + return page; + }) + .filter((page) => { + if (page.layout !== layout) return false; + return page.name.toLowerCase().includes(searchText.toLowerCase()); + }); + + setList(list); + }, [layout, pages, searchText]); + + return { + list, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/DatabaseList/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/DatabaseList/index.tsx new file mode 100644 index 000000000000..355ec2216be4 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/_shared/DatabaseList/index.tsx @@ -0,0 +1,103 @@ +import React, { useMemo } from 'react'; +import { ViewLayoutPB } from '@/services/backend'; +import { useLoadDatabaseList } from '$app/components/document/_shared/DatabaseList/index.hooks'; +import { List } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import { BackupTableOutlined } from '@mui/icons-material'; +import MenuItem from '@mui/material/MenuItem'; +import { useAppDispatch } from '@/appflowy_app/stores/store'; +import { turnToBlockThunk } from '$app_reducers/document/async-actions'; +import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks'; +import { BlockType } from '$app/interfaces/document'; +import AddSvg from '$app/components/_shared/svg/AddSvg'; +import Button from '@mui/material/Button'; +import { PageController } from '$app/stores/effects/workspace/page/page_controller'; + +interface Props { + layout: ViewLayoutPB; + searchText?: string; + blockId: string; + onClose?: () => void; +} + +function DatabaseList({ layout, searchText, blockId, onClose }: Props) { + const { t } = useTranslation(); + const { docId } = useSubscribeDocument(); + const pageController = useMemo(() => new PageController(docId), [docId]); + const dispatch = useAppDispatch(); + const { controller } = useSubscribeDocument(); + const { list } = useLoadDatabaseList({ + searchText: searchText || '', + layout, + }); + + const renderEmpty = () => { + return <div className={'p-2 text-text-caption'}>No {layout === ViewLayoutPB.Grid ? 'grid' : 'list'} found</div>; + }; + + const handleReferenceDatabase = (viewId: string) => { + let blockType; + + switch (layout) { + case ViewLayoutPB.Grid: + blockType = BlockType.GridBlock; + break; + default: + break; + } + + if (blockType === undefined) return; + onClose?.(); + void dispatch( + turnToBlockThunk({ + id: blockId, + controller, + type: blockType, + data: { + viewId, + }, + }) + ); + }; + + const handleCreateNewGrid = async () => { + const newViewId = await pageController.createPage({ + layout, + name: t('editor.table'), + }); + + handleReferenceDatabase(newViewId); + }; + + return ( + <div className={'max-h-[360px] w-[200px] p-3'}> + <div className={'flex items-center justify-center'}> + <Button + color='inherit' + startIcon={ + <i className={'h-8 w-8'}> + <AddSvg /> + </i> + } + onClick={handleCreateNewGrid} + > + {t('document.slashMenu.grid.createANewGrid')} + </Button> + </div> + {list.length === 0 ? ( + renderEmpty() + ) : ( + <List> + {list.map((item) => ( + <MenuItem onClick={() => handleReferenceDatabase(item.id)} key={item.id}> + <div className={'mr-2'}>{item.icon?.value || <BackupTableOutlined />}</div> + {item.name || t('grid.title.placeholder')} + </MenuItem> + ))} + </List> + )} + </div> + ); +} + +export default DatabaseList; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/WorkspaceManager/Workspace.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/WorkspaceManager/Workspace.tsx index b30002777daf..b80b8c28e5d9 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/WorkspaceManager/Workspace.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/WorkspaceManager/Workspace.tsx @@ -1,8 +1,10 @@ import React from 'react'; import { WorkspaceItem } from '$app_reducers/workspace/slice'; import NestedViews from '$app/components/layout/WorkspaceManager/NestedPages'; +import { useLoadWorkspace } from '$app/components/layout/WorkspaceManager/Workspace.hooks'; function Workspace({ workspace, opened }: { workspace: WorkspaceItem; opened: boolean }) { + useLoadWorkspace(workspace); return ( <div className={'flex h-[100%] flex-col'}> <div diff --git a/frontend/appflowy_tauri/src/appflowy_app/constants/document/config.ts b/frontend/appflowy_tauri/src/appflowy_app/constants/document/config.ts index 4f8e54ad8901..fe6c432a550e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/constants/document/config.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/constants/document/config.ts @@ -90,6 +90,9 @@ export const blockConfig: Record<string, BlockConfig> = { [BlockType.DividerBlock]: { canAddChild: false, }, + [BlockType.GridBlock]: { + canAddChild: false, + }, [BlockType.EquationBlock]: { canAddChild: false, defaultData: { diff --git a/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts b/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts index e66edd77af7e..8e8e2f97bf7b 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/interfaces/document.ts @@ -23,6 +23,7 @@ export enum BlockType { CalloutBlock = 'callout', DividerBlock = 'divider', ImageBlock = 'image', + GridBlock = 'grid', } export interface EauqtionBlockData { @@ -83,6 +84,10 @@ export interface PageBlockData extends TextBlockData { // eslint-disable-next-line @typescript-eslint/no-explicit-any type Data = any; +export interface ReferenceBlockData { + viewId: string; +} + // eslint-disable-next-line @typescript-eslint/no-explicit-any export type BlockData<Type = any> = Type extends BlockType.HeadingBlock ? HeadingBlockData @@ -106,13 +111,15 @@ export type BlockData<Type = any> = Type extends BlockType.HeadingBlock ? ImageBlockData : Type extends BlockType.TextBlock ? TextBlockData + : Type extends BlockType.GridBlock + ? ReferenceBlockData : Data; // eslint-disable-next-line @typescript-eslint/no-explicit-any export interface NestedBlock<Type = any> { id: string; type: BlockType; - data: BlockData<Type> | Data; + data: BlockData<Type>; parent: string | null; children: string; externalId?: string; @@ -159,12 +166,14 @@ export enum SlashCommandOptionKey { HEADING_2, HEADING_3, IMAGE, + GRID_REFERENCE, } export interface SlashCommandOption { type: BlockType; data?: BlockData; key: SlashCommandOptionKey; + onClick?: () => void; } export enum SlashCommandGroup { diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/turn_to.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/turn_to.ts index dd66cfd2abe3..2c9e9939db28 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/turn_to.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/turn_to.ts @@ -44,52 +44,63 @@ export const turnToBlockThunk = createAsyncThunk( delta = new Delta([{ insert: node.data.formula }]); } - if (type === BlockType.EquationBlock) { - data.formula = deltaOperator.getDeltaText(delta); - const block = newBlock(type, parent.id, data); - - insertActions.push(controller.getInsertAction(block, node.id)); - caretId = block.id; - caretIndex = 0; - } else if (type === BlockType.DividerBlock) { - const block = newBlock(type, parent.id, data); - - insertActions.push(controller.getInsertAction(block, node.id)); - const nodeId = generateId(); - const actions = deltaOperator.getNewTextLineActions({ - blockId: nodeId, - parentId: parent.id, - prevId: block.id || null, - delta: delta ? delta : new Delta([{ insert: '' }]), - type: BlockType.TextBlock, - data, - }); + const block = newBlock(type, parent.id, data); - caretId = nodeId; - caretIndex = 0; - insertActions.push(...actions); - } else { - caretId = generateId(); + caretId = block.id; - const actions = deltaOperator.getNewTextLineActions({ - blockId: caretId, - parentId: parent.id, - prevId: node.id, - delta: delta, - type, - data, - }); - - insertActions.push(...actions); + switch (type) { + case BlockType.GridBlock: + insertActions.push(controller.getInsertAction(block, node.id)); + caretIndex = 0; + break; + case BlockType.EquationBlock: + data.formula = deltaOperator.getDeltaText(delta); + insertActions.push(controller.getInsertAction(block, node.id)); + caretIndex = 0; + break; + case BlockType.DividerBlock: { + insertActions.push(controller.getInsertAction(block, node.id)); + + const nodeId = generateId(); + + caretId = nodeId; + caretIndex = 0; + insertActions.push( + ...deltaOperator.getNewTextLineActions({ + blockId: nodeId, + parentId: parent.id, + prevId: block.id || null, + delta: delta ? delta : new Delta([{ insert: '' }]), + type: BlockType.TextBlock, + data, + }) + ); + break; + } + + default: + caretId = generateId(); + + insertActions.push( + ...deltaOperator.getNewTextLineActions({ + blockId: caretId, + parentId: parent.id, + prevId: node.id, + delta, + type, + data, + }) + ); + break; } if (!caretId) return; // check if prev node is allowed to have children const config = blockConfig[type]; // if new block is not allowed to have children, move children to parent - const newParentId = config.canAddChild ? caretId : parent.id; + const newParentId = config?.canAddChild ? caretId : parent.id; // if move children to parent, set prev to current block, otherwise the prev is empty - const newPrev = config.canAddChild ? null : caretId; + const newPrev = config?.canAddChild ? null : caretId; const moveChildrenActions = controller.getMoveChildrenAction(children, newParentId, newPrev); // delete current block diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/document/block.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/document/block.ts index 18734e47358c..9b231756a8c3 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/document/block.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/document/block.ts @@ -100,11 +100,13 @@ export function getPrevNodeId(state: DocumentState, id: string) { } export function newBlock<Type>(type: BlockType, parentId: string, data?: BlockData<Type>): NestedBlock<Type> { + const blockData = data || ({} as BlockData<Type>); + return { id: generateId(), type, parent: parentId, children: generateId(), - data: data ? data : {}, + data: blockData, }; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/views/DatabasePage.tsx b/frontend/appflowy_tauri/src/appflowy_app/views/DatabasePage.tsx index cb0dbd5ba060..1b14c939815c 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/views/DatabasePage.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/views/DatabasePage.tsx @@ -1,25 +1,24 @@ import { useParams } from 'react-router-dom'; import { ViewIdProvider } from '$app/hooks'; -import { Database, DatabaseTitle, VerticalScrollElementProvider } from '../components/database'; -import { useRef } from 'react'; +import { Database, DatabaseTitle, useSelectDatabaseView } from '../components/database'; export const DatabasePage = () => { const viewId = useParams().id; - const ref = useRef<HTMLDivElement>(null); + const { selectedViewId, onChange } = useSelectDatabaseView({ + viewId, + }); if (!viewId) { return null; } return ( - <div className="h-full overflow-y-auto" ref={ref}> - <VerticalScrollElementProvider value={ref}> - <ViewIdProvider value={viewId}> - <DatabaseTitle /> - <Database /> - </ViewIdProvider> - </VerticalScrollElementProvider> + <div className='flex h-full w-full flex-col overflow-hidden px-16 caret-text-title'> + <ViewIdProvider value={viewId}> + <DatabaseTitle /> + <Database selectedViewId={selectedViewId} setSelectedViewId={onChange} /> + </ViewIdProvider> </div> ); }; diff --git a/frontend/appflowy_tauri/src/styles/template.css b/frontend/appflowy_tauri/src/styles/template.css index c23eaad52b26..cabbdfb23166 100644 --- a/frontend/appflowy_tauri/src/styles/template.css +++ b/frontend/appflowy_tauri/src/styles/template.css @@ -23,6 +23,11 @@ body { @apply bg-content-blue-100; } +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + div[role="textbox"] ::selection { @apply bg-transparent; } diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 852098d8efc5..4d9c4647905b 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -531,7 +531,10 @@ "newRow": "New row", "action": "Action", "add": "Click add to below", - "drag": "Drag to move" + "drag": "Drag to move", + "dragAndClick": "Drag to move, click to open menu", + "insertRecordAbove": "Insert record above", + "insertRecordBelow": "Insert record below" }, "selectOption": { "create": "Create", diff --git a/shared-lib/flowy-codegen/src/ts_event/event_template.tera b/shared-lib/flowy-codegen/src/ts_event/event_template.tera index 2b23f787683a..46e79c0dfe1b 100644 --- a/shared-lib/flowy-codegen/src/ts_event/event_template.tera +++ b/shared-lib/flowy-codegen/src/ts_event/event_template.tera @@ -24,10 +24,8 @@ export async function {{ event_func_name }}(): Promise<Result<{{ output_deserial if (result.code == 0) { {%- if has_output %} let object = {{ output_deserializer }}.deserializeBinary(result.payload); - console.log({{ event_func_name }}.name, object); return Ok(object); {%- else %} - console.log({{ event_func_name }}.name); return Ok.EMPTY; {%- endif %} } else { From afc6473582d02fce03dde6f7d3b8ead79515390f Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" <lucas.xu@appflowy.io> Date: Wed, 8 Nov 2023 21:10:29 +0800 Subject: [PATCH 35/56] feat: adjust toggle list, callout, quote and divider on mobile (#3894) * feat: adjust toggle list block * feat: show block actions when tapping divider * feat: add toggle list and callout to toolbar * feat: refactor the emoji picker button * fix: toggle list integration tests --- .../document_with_toggle_list_test.dart | 23 +++- .../base/emoji/emoji_picker_screen.dart | 11 +- .../lib/plugins/base/icon/icon_picker.dart | 26 +++- .../plugins/base/icon/icon_picker_page.dart | 14 +- .../presentation/editor_configuration.dart | 16 ++- .../document/presentation/editor_page.dart | 4 +- .../actions/mobile_block_action_buttons.dart | 12 +- .../base/emoji_picker_button.dart | 75 +++++++---- .../callout/callout_block_component.dart | 2 +- .../error/error_block_component_builder.dart | 24 +++- .../header/document_header_node_widget.dart | 44 +++---- ...=> mobile_math_equation_toolbar_item.dart} | 0 .../list_mobile_toolbar_item.dart | 121 ++++++++++++++++++ .../presentation/editor_plugins/plugins.dart | 3 +- .../toggle/toggle_block_component.dart | 16 ++- .../lib/startup/tasks/generate_router.dart | 7 +- .../home/menu/view/view_item.dart | 4 +- frontend/appflowy_flutter/pubspec.lock | 4 +- frontend/appflowy_flutter/pubspec.yaml | 3 +- frontend/resources/translations/en.json | 7 +- 20 files changed, 308 insertions(+), 108 deletions(-) rename frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/{mobile_math_eqaution_toolbar_item.dart => mobile_math_equation_toolbar_item.dart} (100%) create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/list_mobile_toolbar_item.dart diff --git a/frontend/appflowy_flutter/integration_test/document/document_with_toggle_list_test.dart b/frontend/appflowy_flutter/integration_test/document/document_with_toggle_list_test.dart index 50b84ed521cc..80bdcbe98303 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_with_toggle_list_test.dart +++ b/frontend/appflowy_flutter/integration_test/document/document_with_toggle_list_test.dart @@ -15,14 +15,23 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('toggle list in document', () { + Finder findToggleListIcon({ + required bool isExpanded, + }) { + final turns = isExpanded ? 0.25 : 0.0; + return find.byWidgetPredicate( + (widget) => widget is AnimatedRotation && widget.turns == turns, + ); + } + void expectToggleListOpened() { - expect(find.byIcon(Icons.arrow_drop_down), findsOneWidget); - expect(find.byIcon(Icons.arrow_right), findsNothing); + expect(findToggleListIcon(isExpanded: true), findsOneWidget); + expect(findToggleListIcon(isExpanded: false), findsNothing); } void expectToggleListClosed() { - expect(find.byIcon(Icons.arrow_drop_down), findsNothing); - expect(find.byIcon(Icons.arrow_right), findsOneWidget); + expect(findToggleListIcon(isExpanded: false), findsOneWidget); + expect(findToggleListIcon(isExpanded: true), findsNothing); } testWidgets('convert > to toggle list, and click the icon to close it', @@ -63,7 +72,7 @@ void main() { expect(find.text(text2, findRichText: true), findsOneWidget); // Click the toggle list icon to close it - final toggleListIcon = find.byIcon(Icons.arrow_drop_down); + final toggleListIcon = find.byIcon(Icons.arrow_right); await tester.tapButton(toggleListIcon); // expect the toggle list to be closed @@ -88,7 +97,7 @@ void main() { await tester.ime.insertText('> $text'); // Click the toggle list icon to close it - final toggleListIcon = find.byIcon(Icons.arrow_drop_down); + final toggleListIcon = find.byIcon(Icons.arrow_right); await tester.tapButton(toggleListIcon); // Press the enter key @@ -164,7 +173,7 @@ void main() { // Press the enter key // Click the toggle list icon to close it - final toggleListIcon = find.byIcon(Icons.arrow_drop_down); + final toggleListIcon = find.byIcon(Icons.arrow_right); await tester.tapButton(toggleListIcon); await tester.editor.updateSelection( diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_screen.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_screen.dart index fa578bb132ae..6aaa307ef5bb 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_screen.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_screen.dart @@ -1,22 +1,21 @@ +import 'package:appflowy/plugins/base/icon/icon_picker.dart'; import 'package:appflowy/plugins/base/icon/icon_picker_page.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; class MobileEmojiPickerScreen extends StatelessWidget { static const routeName = '/emoji_picker'; - static const viewId = 'id'; const MobileEmojiPickerScreen({ super.key, - required this.id, }); - /// view id - final String id; - @override Widget build(BuildContext context) { return IconPickerPage( - id: id, + onSelected: (result) { + context.pop<EmojiPickerResult>(result); + }, ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart index e60555a1eff8..6b27e5a86560 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart @@ -12,13 +12,23 @@ enum FlowyIconType { custom; } +class EmojiPickerResult { + const EmojiPickerResult( + this.type, + this.emoji, + ); + + final FlowyIconType type; + final String emoji; +} + class FlowyIconPicker extends StatefulWidget { const FlowyIconPicker({ super.key, required this.onSelected, }); - final void Function(FlowyIconType type, String value) onSelected; + final void Function(EmojiPickerResult result) onSelected; @override State<FlowyIconPicker> createState() => _FlowyIconPickerState(); @@ -45,7 +55,12 @@ class _FlowyIconPickerState extends State<FlowyIconPicker> const Spacer(), _RemoveIconButton( onTap: () { - widget.onSelected(FlowyIconType.icon, ''); + widget.onSelected( + const EmojiPickerResult( + FlowyIconType.icon, + '', + ), + ); }, ), ], @@ -58,7 +73,12 @@ class _FlowyIconPickerState extends State<FlowyIconPicker> children: [ FlowyEmojiPicker( onEmojiSelected: (_, emoji) { - widget.onSelected(FlowyIconType.emoji, emoji); + widget.onSelected( + EmojiPickerResult( + FlowyIconType.emoji, + emoji, + ), + ); }, ), ], diff --git a/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker_page.dart b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker_page.dart index 43cc4c67de87..292910b6f4ef 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker_page.dart @@ -1,6 +1,5 @@ import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart'; import 'package:appflowy/plugins/base/icon/icon_picker.dart'; -import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -8,11 +7,10 @@ import 'package:go_router/go_router.dart'; class IconPickerPage extends StatefulWidget { const IconPickerPage({ super.key, - required this.id, + required this.onSelected, }); - /// view id - final String id; + final void Function(EmojiPickerResult) onSelected; @override State<IconPickerPage> createState() => _IconPickerPageState(); @@ -34,13 +32,7 @@ class _IconPickerPageState extends State<IconPickerPage> { ), body: SafeArea( child: FlowyIconPicker( - onSelected: (_, emoji) { - ViewBackendService.updateViewIcon( - viewId: widget.id, - viewIcon: emoji, - ); - context.pop(); - }, + onSelected: widget.onSelected, ), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart index 5618431420c2..74c83cfdaf86 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; @@ -26,6 +27,9 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({ final configuration = BlockComponentConfiguration( padding: (_) => const EdgeInsets.symmetric(vertical: 5.0), + indentPadding: (node, textDirection) => textDirection == TextDirection.ltr + ? const EdgeInsets.only(left: 26.0) + : const EdgeInsets.only(right: 26.0), ); final customBlockComponentBuilderMap = { @@ -119,6 +123,14 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({ DividerBlockKeys.type: DividerBlockComponentBuilder( configuration: configuration, height: 28.0, + wrapper: (context, node, child) { + return MobileBlockActionButtons( + showThreeDots: false, + node: node, + editorState: editorState, + child: child, + ); + }, ), MathEquationBlockKeys.type: MathEquationBlockComponentBuilder( configuration: configuration, @@ -146,9 +158,7 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({ ), ), errorBlockComponentBuilderKey: ErrorBlockComponentBuilder( - configuration: configuration.copyWith( - padding: (_) => const EdgeInsets.symmetric(vertical: 10), - ), + configuration: configuration, ), }; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index 542ff548eaaf..be877ea1ca0e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -286,10 +286,8 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> { textDecorationMobileToolbarItem, buildTextAndBackgroundColorMobileToolbarItem(), headingMobileToolbarItem, - todoListMobileToolbarItem, - listMobileToolbarItem, + customListMobileToolbarItem, linkMobileToolbarItem, - quoteMobileToolbarItem, dividerMobileToolbarItem, imageMobileToolbarItem, mathEquationMobileToolbarItem, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart index b5fab176944c..6738e8d028cf 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart @@ -21,6 +21,7 @@ class MobileBlockActionButtons extends StatelessWidget { const MobileBlockActionButtons({ super.key, this.extendActionWidgets = const [], + this.showThreeDots = true, required this.node, required this.editorState, required this.child, @@ -30,6 +31,7 @@ class MobileBlockActionButtons extends StatelessWidget { final EditorState editorState; final List<Widget> extendActionWidgets; final Widget child; + final bool showThreeDots; @override Widget build(BuildContext context) { @@ -37,7 +39,15 @@ class MobileBlockActionButtons extends StatelessWidget { return child; } - const padding = 5.0; + if (!showThreeDots) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => _showBottomSheet(context), + child: child, + ); + } + + const padding = 10.0; return Stack( children: [ child, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart index 2d4e48379be5..61e505cf6143 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart @@ -1,7 +1,11 @@ +import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart'; +import 'package:appflowy/plugins/base/icon/icon_picker.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; class EmojiPickerButton extends StatelessWidget { EmojiPickerButton({ @@ -15,20 +19,43 @@ class EmojiPickerButton extends StatelessWidget { final String emoji; final double emojiSize; final Size emojiPickerSize; - final void Function(String emoji, PopoverController controller) onSubmitted; + final void Function(String emoji, PopoverController? controller) onSubmitted; final PopoverController popoverController = PopoverController(); @override Widget build(BuildContext context) { - return AppFlowyPopover( - controller: popoverController, - triggerActions: PopoverTriggerFlags.click, - constraints: BoxConstraints.expand( - width: emojiPickerSize.width, - height: emojiPickerSize.height, - ), - popupBuilder: (context) => _buildEmojiPicker(), - child: FlowyTextButton( + if (PlatformExtension.isDesktopOrWeb) { + return AppFlowyPopover( + controller: popoverController, + triggerActions: PopoverTriggerFlags.click, + constraints: BoxConstraints.expand( + width: emojiPickerSize.width, + height: emojiPickerSize.height, + ), + popupBuilder: (context) => Container( + width: emojiPickerSize.width, + height: emojiPickerSize.height, + padding: const EdgeInsets.all(4.0), + child: EmojiSelectionMenu( + onSubmitted: (emoji) => onSubmitted(emoji, popoverController), + onExit: () {}, + ), + ), + child: FlowyTextButton( + emoji, + overflow: TextOverflow.visible, + fontSize: emojiSize, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(minWidth: 35.0), + fillColor: Colors.transparent, + mainAxisAlignment: MainAxisAlignment.center, + onPressed: () { + popoverController.show(); + }, + ), + ); + } else { + return FlowyTextButton( emoji, overflow: TextOverflow.visible, fontSize: emojiSize, @@ -36,22 +63,18 @@ class EmojiPickerButton extends StatelessWidget { constraints: const BoxConstraints(minWidth: 35.0), fillColor: Colors.transparent, mainAxisAlignment: MainAxisAlignment.center, - onPressed: () { - popoverController.show(); + onPressed: () async { + final result = await context.push<EmojiPickerResult>( + MobileEmojiPickerScreen.routeName, + ); + if (result != null) { + onSubmitted( + result.emoji, + null, + ); + } }, - ), - ); - } - - Widget _buildEmojiPicker() { - return Container( - width: emojiPickerSize.width, - height: emojiPickerSize.height, - padding: const EdgeInsets.all(4.0), - child: EmojiSelectionMenu( - onSubmitted: (emoji) => onSubmitted(emoji, popoverController), - onExit: () {}, - ), - ); + ); + } } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart index 7ede37eb7299..b8e48c8c300f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart @@ -187,7 +187,7 @@ class _CalloutBlockComponentWidgetState emoji: emoji, onSubmitted: (emoji, controller) { setEmoji(emoji); - controller.close(); + controller?.close(); }, ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart index a50895d298e3..914a4bef572c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; @@ -8,6 +9,7 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class ErrorBlockComponentBuilder extends BlockComponentBuilder { ErrorBlockComponentBuilder({ @@ -72,11 +74,15 @@ class _DividerBlockComponentWidgetState extends State<ErrorBlockComponentWidget> ClipboardServiceData(plainText: jsonEncode(node.toJson())), ); }, - text: Container( - height: 48, - alignment: Alignment.center, - child: FlowyText( - LocaleKeys.document_errorBlock_theBlockIsNotSupported.tr(), + text: SizedBox( + height: 52, + child: Row( + children: [ + const HSpace(4), + FlowyText( + LocaleKeys.document_errorBlock_theBlockIsNotSupported.tr(), + ), + ], ), ), ), @@ -95,6 +101,14 @@ class _DividerBlockComponentWidgetState extends State<ErrorBlockComponentWidget> ); } + if (PlatformExtension.isMobile) { + child = MobileBlockActionButtons( + node: node, + editorState: context.read<EditorState>(), + child: child, + ); + } + return child; } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart index 358c85ff2dc5..59355d25c19c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart @@ -6,7 +6,6 @@ import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart'; import 'package:appflowy/plugins/base/icon/icon_picker.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_listener.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -16,7 +15,6 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/rounded_button.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:provider/provider.dart'; import 'cover_editor.dart'; @@ -303,15 +301,14 @@ class _DocumentHeaderToolbarState extends State<DocumentHeaderToolbar> { ), onTap: PlatformExtension.isDesktop ? null - : () => context.push( - Uri( - path: MobileEmojiPickerScreen.routeName, - queryParameters: { - MobileEmojiPickerScreen.viewId: - context.read<ViewBloc>().state.view.id, - }, - ).toString(), - ), + : () async { + final result = await context.push<EmojiPickerResult>( + MobileEmojiPickerScreen.routeName, + ); + if (result != null) { + widget.onCoverChanged(icon: result.emoji); + } + }, ); if (PlatformExtension.isDesktop) { @@ -325,8 +322,8 @@ class _DocumentHeaderToolbarState extends State<DocumentHeaderToolbar> { popupBuilder: (BuildContext popoverContext) { isPopoverOpen = true; return FlowyIconPicker( - onSelected: (type, value) { - widget.onCoverChanged(icon: value); + onSelected: (result) { + widget.onCoverChanged(icon: result.emoji); _popoverController.close(); }, ); @@ -532,8 +529,8 @@ class _DocumentIconState extends State<DocumentIcon> { child: child, popupBuilder: (BuildContext popoverContext) { return FlowyIconPicker( - onSelected: (type, value) { - widget.onIconChanged(value); + onSelected: (result) { + widget.onIconChanged(result.emoji); _popoverController.close(); }, ); @@ -542,15 +539,14 @@ class _DocumentIconState extends State<DocumentIcon> { } else { child = GestureDetector( child: child, - onTap: () => context.push( - Uri( - path: MobileEmojiPickerScreen.routeName, - queryParameters: { - MobileEmojiPickerScreen.viewId: - context.read<ViewBloc>().state.view.id, - }, - ).toString(), - ), + onTap: () async { + final result = await context.push<EmojiPickerResult>( + MobileEmojiPickerScreen.routeName, + ); + if (result != null) { + widget.onIconChanged(result.emoji); + } + }, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/mobile_math_eqaution_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/mobile_math_equation_toolbar_item.dart similarity index 100% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/mobile_math_eqaution_toolbar_item.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/mobile_math_equation_toolbar_item.dart diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/list_mobile_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/list_mobile_toolbar_item.dart new file mode 100644 index 000000000000..d1f633eb6044 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/list_mobile_toolbar_item.dart @@ -0,0 +1,121 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +final customListMobileToolbarItem = MobileToolbarItem.withMenu( + itemIcon: const AFMobileIcon(afMobileIcons: AFMobileIcons.list), + itemMenuBuilder: (editorState, selection, _) { + return _MobileListMenu( + editorState: editorState, + selection: selection, + ); + }, +); + +class _MobileListMenu extends StatelessWidget { + const _MobileListMenu({ + required this.editorState, + required this.selection, + }); + + final Selection selection; + final EditorState editorState; + + @override + Widget build(BuildContext context) { + return GridView.count( + crossAxisCount: 2, + mainAxisSpacing: 8, + crossAxisSpacing: 8, + childAspectRatio: 5, + shrinkWrap: true, + children: [ + // bulleted list, numbered list + _buildListButton( + context, + BulletedListBlockKeys.type, + const AFMobileIcon(afMobileIcons: AFMobileIcons.bulletedList), + LocaleKeys.document_plugins_bulletedList.tr(), + ), + _buildListButton( + context, + NumberedListBlockKeys.type, + const AFMobileIcon(afMobileIcons: AFMobileIcons.numberedList), + LocaleKeys.document_plugins_numberedList.tr(), + ), + + // todo list, quote list + _buildListButton( + context, + TodoListBlockKeys.type, + const AFMobileIcon(afMobileIcons: AFMobileIcons.checkbox), + LocaleKeys.document_plugins_todoList.tr(), + ), + _buildListButton( + context, + QuoteBlockKeys.type, + const AFMobileIcon(afMobileIcons: AFMobileIcons.quote), + LocaleKeys.document_plugins_quoteList.tr(), + ), + + // toggle list, callout + _buildListButton( + context, + ToggleListBlockKeys.type, + const FlowySvg( + FlowySvgs.toggle_list_s, + size: Size.square(24), + ), + LocaleKeys.document_plugins_toggleList.tr(), + ), + _buildListButton( + context, + CalloutBlockKeys.type, + const Icon(Icons.note_rounded), + LocaleKeys.document_plugins_callout.tr(), + ), + ], + ); + } + + Widget _buildListButton( + BuildContext context, + String listBlockType, + Widget icon, + String label, + ) { + final node = editorState.getNodeAtPath(selection.start.path); + final type = node?.type; + if (node == null || type == null) { + const SizedBox.shrink(); + } + final isSelected = type == listBlockType; + return MobileToolbarItemMenuBtn( + icon: icon, + label: FlowyText(label), + isSelected: isSelected, + onPressed: () async { + await editorState.formatNode( + selection, + (node) { + final attributes = { + ParagraphBlockKeys.delta: (node.delta ?? Delta()).toJson(), + if (listBlockType == TodoListBlockKeys.type) + TodoListBlockKeys.checked: false, + if (listBlockType == CalloutBlockKeys.type) + CalloutBlockKeys.icon: '📌', + }; + return node.copyWith( + type: isSelected ? ParagraphBlockKeys.type : listBlockType, + attributes: attributes, + ); + }, + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart index 49b212c40e0c..f1f816f8a01b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart @@ -25,7 +25,8 @@ export 'image/mobile_image_toolbar_item.dart'; export 'inline_math_equation/inline_math_equation.dart'; export 'inline_math_equation/inline_math_equation_toolbar_item.dart'; export 'math_equation/math_equation_block_component.dart'; -export 'math_equation/mobile_math_eqaution_toolbar_item.dart'; +export 'math_equation/mobile_math_equation_toolbar_item.dart'; +export 'mobile_toolbar_item/list_mobile_toolbar_item.dart'; export 'openai/widgets/auto_completion_node_widget.dart'; export 'openai/widgets/smart_edit_node_widget.dart'; export 'openai/widgets/smart_edit_toolbar_item.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart index a9ff605b038e..003ae01d7b2b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart @@ -166,13 +166,17 @@ class _ToggleListBlockComponentWidgetState Container( constraints: const BoxConstraints(minWidth: 26, minHeight: 22), padding: const EdgeInsets.only(right: 4.0), - child: FlowyIconButton( - width: 18.0, - icon: Icon( - collapsed ? Icons.arrow_right : Icons.arrow_drop_down, - size: 18.0, + child: AnimatedRotation( + turns: collapsed ? 0.0 : 0.25, + duration: const Duration(milliseconds: 200), + child: FlowyIconButton( + width: 18.0, + icon: const Icon( + Icons.arrow_right, + size: 18.0, + ), + onPressed: onCollapsed, ), - onPressed: onCollapsed, ), ), diff --git a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart index a9415f14d6fe..bdb70ab5c498 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart @@ -209,11 +209,8 @@ GoRoute _mobileEmojiPickerPageRoute() { parentNavigatorKey: AppGlobals.rootNavKey, path: MobileEmojiPickerScreen.routeName, pageBuilder: (context, state) { - final id = state.uri.queryParameters[MobileEmojiPickerScreen.viewId]!; - return MaterialPage( - child: MobileEmojiPickerScreen( - id: id, - ), + return const MaterialPage( + child: MobileEmojiPickerScreen(), ); }, ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart index 05ce8902e238..dac51e723229 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart @@ -361,10 +361,10 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> { popupBuilder: (context) { isIconPickerOpened = true; return FlowyIconPicker( - onSelected: (_, emoji) { + onSelected: (result) { ViewBackendService.updateViewIcon( viewId: widget.view.id, - viewIcon: emoji, + viewIcon: result.emoji, ); controller.close(); }, diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 9efa52c66dff..7c005d9ab54b 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -54,8 +54,8 @@ packages: dependency: "direct main" description: path: "." - ref: a47fc6f - resolved-ref: a47fc6fc712b06991f578ae2ab314cbe23034e96 + ref: "50117b6" + resolved-ref: "50117b6900e4b239603ee48f6f3e7b7bc603c865" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "1.5.2" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 862d1cbea986..2bb798ad59ca 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -47,7 +47,8 @@ dependencies: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: a47fc6f + ref: 50117b6 + appflowy_popover: path: packages/appflowy_popover diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 4d9c4647905b..f504956308dc 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -616,7 +616,12 @@ "smartEditDisabled": "Connect OpenAI in Settings", "discardResponse": "Do you want to discard the AI responses?", "createInlineMathEquation": "Create equation", - "toggleList": "Toggle List", + "toggleList": "Toggle list", + "quoteList":"Quote list", + "numberedList":"Numbered list", + "bulletedList":"Bulleted list", + "todoList": "Todo List", + "callout": "Callout", "cover": { "changeCover": "Change Cover", "colors": "Colors", From 73f1c211c2aec601ec6572713e4316c5a4a180ed Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Wed, 8 Nov 2023 21:48:17 +0800 Subject: [PATCH 36/56] fix: Invalid refresh token (#3879) * chore: update client api * chore: update client api * chore: update client api * chore: fix clippy * chore: fix clippy * ci: fix * chore: update client api --- frontend/appflowy_flutter/dev.env | 6 +- frontend/appflowy_tauri/src-tauri/Cargo.lock | 117 +++++++++++++--- frontend/appflowy_tauri/src-tauri/Cargo.toml | 18 +-- frontend/rust-lib/Cargo.lock | 131 ++++++++++++++---- frontend/rust-lib/Cargo.toml | 19 +-- .../flowy-core/src/integrate/trait_impls.rs | 1 + .../flowy-error/src/impl_from/cloud.rs | 3 +- .../rust-lib/flowy-folder2/src/manager.rs | 7 +- .../flowy-folder2/src/user_default.rs | 10 +- .../flowy-folder2/src/view_operation.rs | 26 ++-- .../flowy-server/src/af_cloud/server.rs | 40 +++--- .../src/local_server/impls/folder.rs | 14 +- frontend/rust-lib/flowy-server/src/server.rs | 12 +- .../flowy-server/src/supabase/api/folder.rs | 7 + .../flowy-server/src/supabase/api/user.rs | 2 +- frontend/rust-lib/flowy-sqlite/src/kv/kv.rs | 2 +- frontend/rust-lib/flowy-sqlite/src/lib.rs | 8 +- .../src/{sqlite => sqlite_impl}/conn_ext.rs | 3 +- .../src/{sqlite => sqlite_impl}/database.rs | 8 +- .../src/{sqlite => sqlite_impl}/errors.rs | 0 .../src/{sqlite => sqlite_impl}/mod.rs | 0 .../src/{sqlite => sqlite_impl}/pool.rs | 6 +- .../src/{sqlite => sqlite_impl}/pragma.rs | 16 ++- 23 files changed, 332 insertions(+), 124 deletions(-) rename frontend/rust-lib/flowy-sqlite/src/{sqlite => sqlite_impl}/conn_ext.rs (94%) rename frontend/rust-lib/flowy-sqlite/src/{sqlite => sqlite_impl}/database.rs (97%) rename frontend/rust-lib/flowy-sqlite/src/{sqlite => sqlite_impl}/errors.rs (100%) rename frontend/rust-lib/flowy-sqlite/src/{sqlite => sqlite_impl}/mod.rs (100%) rename frontend/rust-lib/flowy-sqlite/src/{sqlite => sqlite_impl}/pool.rs (95%) rename frontend/rust-lib/flowy-sqlite/src/{sqlite => sqlite_impl}/pragma.rs (97%) diff --git a/frontend/appflowy_flutter/dev.env b/frontend/appflowy_flutter/dev.env index 109aa7c5cc5d..190b166547cf 100644 --- a/frontend/appflowy_flutter/dev.env +++ b/frontend/appflowy_flutter/dev.env @@ -26,8 +26,8 @@ SUPABASE_ANON_KEY=replace-with-your-supabase-key # AppFlowy Cloud Configuration # If using AppFlowy Cloud (CLOUD_TYPE=2), provide the following details: -APPFLOWY_CLOUD_BASE_URL=replace-with-your-appflowy-cloud-url -APPFLOWY_CLOUD_WS_BASE_URL=replace-with-your-appflowy-cloud-ws-url -APPFLOWY_CLOUD_GOTRUE_URL=replace-with-your-appflowy-cloud-gotrue-url +APPFLOWY_CLOUD_BASE_URL=https://xxxxxxxxx +APPFLOWY_CLOUD_WS_BASE_URL=wss://xxxxxxxxx +APPFLOWY_CLOUD_GOTRUE_URL=https://xxxxxxxxx diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 1c1c96466cbf..987a734a6fcc 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -138,7 +138,7 @@ checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7752ab68a91ff742fae595c1b45babdc17927fea#7752ab68a91ff742fae595c1b45babdc17927fea" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d37fbbf486dc44336c87acd59cf8b6feff57b330#d37fbbf486dc44336c87acd59cf8b6feff57b330" dependencies = [ "anyhow", "reqwest", @@ -768,10 +768,11 @@ dependencies = [ [[package]] name = "client-api" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7752ab68a91ff742fae595c1b45babdc17927fea#7752ab68a91ff742fae595c1b45babdc17927fea" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d37fbbf486dc44336c87acd59cf8b6feff57b330#d37fbbf486dc44336c87acd59cf8b6feff57b330" dependencies = [ "anyhow", "app-error", + "async-trait", "bytes", "collab", "collab-entity", @@ -784,6 +785,7 @@ dependencies = [ "mime", "mime_guess", "parking_lot", + "prost", "realtime-entity", "reqwest", "scraper 0.17.1", @@ -861,7 +863,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" dependencies = [ "anyhow", "async-trait", @@ -881,7 +883,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" dependencies = [ "anyhow", "async-trait", @@ -911,7 +913,7 @@ dependencies = [ [[package]] name = "collab-derive" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" dependencies = [ "proc-macro2", "quote", @@ -923,7 +925,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" dependencies = [ "anyhow", "collab", @@ -943,7 +945,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" dependencies = [ "anyhow", "bytes", @@ -957,7 +959,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" dependencies = [ "anyhow", "chrono", @@ -999,7 +1001,7 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" dependencies = [ "async-trait", "bincode", @@ -1020,7 +1022,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" dependencies = [ "anyhow", "async-trait", @@ -1047,7 +1049,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" dependencies = [ "anyhow", "collab", @@ -1446,7 +1448,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7752ab68a91ff742fae595c1b45babdc17927fea#7752ab68a91ff742fae595c1b45babdc17927fea" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d37fbbf486dc44336c87acd59cf8b6feff57b330#d37fbbf486dc44336c87acd59cf8b6feff57b330" dependencies = [ "anyhow", "app-error", @@ -1848,6 +1850,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flate2" version = "1.0.26" @@ -2796,7 +2804,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7752ab68a91ff742fae595c1b45babdc17927fea#7752ab68a91ff742fae595c1b45babdc17927fea" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d37fbbf486dc44336c87acd59cf8b6feff57b330#d37fbbf486dc44336c87acd59cf8b6feff57b330" dependencies = [ "anyhow", "futures-util", @@ -2812,7 +2820,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7752ab68a91ff742fae595c1b45babdc17927fea#7752ab68a91ff742fae595c1b45babdc17927fea" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d37fbbf486dc44336c87acd59cf8b6feff57b330#d37fbbf486dc44336c87acd59cf8b6feff57b330" dependencies = [ "anyhow", "app-error", @@ -3248,7 +3256,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7752ab68a91ff742fae595c1b45babdc17927fea#7752ab68a91ff742fae595c1b45babdc17927fea" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d37fbbf486dc44336c87acd59cf8b6feff57b330#d37fbbf486dc44336c87acd59cf8b6feff57b330" dependencies = [ "anyhow", "reqwest", @@ -3813,6 +3821,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "multimap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" + [[package]] name = "nanoid" version = "0.4.0" @@ -4286,6 +4300,16 @@ dependencies = [ "sha2", ] +[[package]] +name = "petgraph" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +dependencies = [ + "fixedbitset", + "indexmap 2.0.0", +] + [[package]] name = "phf" version = "0.8.0" @@ -4642,6 +4666,60 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fdd22f3b9c31b53c060df4a0613a1c7f062d4115a2b984dd15b1858f7e340d" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bdf592881d821b83d471f8af290226c8d51402259e9bb5be7f9f8bdebbb11ac" +dependencies = [ + "bytes", + "heck 0.4.1", + "itertools 0.11.0", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 2.0.29", + "tempfile", + "which", +] + +[[package]] +name = "prost-derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "265baba7fabd416cf5078179f7d2cbeca4ce7a9041111900675ea7c4cb8a4c32" +dependencies = [ + "anyhow", + "itertools 0.11.0", + "proc-macro2", + "quote", + "syn 2.0.29", +] + +[[package]] +name = "prost-types" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e081b29f63d83a4bc75cfc9f3fe424f9156cf92d8a4f0c9407cce9a1b67327cf" +dependencies = [ + "prost", +] + [[package]] name = "protobuf" version = "2.28.0" @@ -4922,14 +5000,19 @@ dependencies = [ [[package]] name = "realtime-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7752ab68a91ff742fae595c1b45babdc17927fea#7752ab68a91ff742fae595c1b45babdc17927fea" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d37fbbf486dc44336c87acd59cf8b6feff57b330#d37fbbf486dc44336c87acd59cf8b6feff57b330" dependencies = [ "anyhow", + "bincode", "bytes", "collab", "collab-entity", + "prost", + "prost-build", + "protoc-bin-vendored", "serde", "serde_json", + "tokio-tungstenite", ] [[package]] @@ -5661,7 +5744,7 @@ dependencies = [ [[package]] name = "shared_entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7752ab68a91ff742fae595c1b45babdc17927fea#7752ab68a91ff742fae595c1b45babdc17927fea" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d37fbbf486dc44336c87acd59cf8b6feff57b330#d37fbbf486dc44336c87acd59cf8b6feff57b330" dependencies = [ "anyhow", "app-error", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index 2f6f8e7ff05d..7a2ada383ad1 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -38,7 +38,7 @@ custom-protocol = ["tauri/custom-protocol"] # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "7752ab68a91ff742fae595c1b45babdc17927fea" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d37fbbf486dc44336c87acd59cf8b6feff57b330" } # Please use the following script to update collab. # Working directory: frontend # @@ -48,14 +48,14 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "775 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "faf17b54" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "faf17b54" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "faf17b54" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "faf17b54" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "faf17b54" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "faf17b54" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "faf17b54" } -collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "faf17b54" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68459c6c501cd6d69deda04605839db1b442bb01" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68459c6c501cd6d69deda04605839db1b442bb01" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68459c6c501cd6d69deda04605839db1b442bb01" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68459c6c501cd6d69deda04605839db1b442bb01" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68459c6c501cd6d69deda04605839db1b442bb01" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68459c6c501cd6d69deda04605839db1b442bb01" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68459c6c501cd6d69deda04605839db1b442bb01" } +collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68459c6c501cd6d69deda04605839db1b442bb01" } diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index bf77c2651639..bb15bbd3ac6d 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -124,7 +124,7 @@ checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7752ab68a91ff742fae595c1b45babdc17927fea#7752ab68a91ff742fae595c1b45babdc17927fea" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d37fbbf486dc44336c87acd59cf8b6feff57b330#d37fbbf486dc44336c87acd59cf8b6feff57b330" dependencies = [ "anyhow", "reqwest", @@ -467,7 +467,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" dependencies = [ "borsh-derive", - "hashbrown 0.12.3", + "hashbrown 0.13.2", ] [[package]] @@ -666,10 +666,11 @@ dependencies = [ [[package]] name = "client-api" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7752ab68a91ff742fae595c1b45babdc17927fea#7752ab68a91ff742fae595c1b45babdc17927fea" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d37fbbf486dc44336c87acd59cf8b6feff57b330#d37fbbf486dc44336c87acd59cf8b6feff57b330" dependencies = [ "anyhow", "app-error", + "async-trait", "bytes", "collab", "collab-entity", @@ -682,6 +683,7 @@ dependencies = [ "mime", "mime_guess", "parking_lot", + "prost 0.12.1", "realtime-entity", "reqwest", "scraper 0.17.1", @@ -728,7 +730,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" dependencies = [ "anyhow", "async-trait", @@ -748,7 +750,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" dependencies = [ "anyhow", "async-trait", @@ -778,7 +780,7 @@ dependencies = [ [[package]] name = "collab-derive" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" dependencies = [ "proc-macro2", "quote", @@ -790,7 +792,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" dependencies = [ "anyhow", "collab", @@ -810,7 +812,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" dependencies = [ "anyhow", "bytes", @@ -824,7 +826,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" dependencies = [ "anyhow", "chrono", @@ -866,7 +868,7 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" dependencies = [ "async-trait", "bincode", @@ -887,7 +889,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" dependencies = [ "anyhow", "async-trait", @@ -914,7 +916,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=faf17b54#faf17b54ead0c1286e4ba8332d25e42bf1e1d627" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" dependencies = [ "anyhow", "collab", @@ -960,8 +962,8 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2895653b4d9f1538a83970077cb01dfc77a4810524e51a110944688e916b18e" dependencies = [ - "prost", - "prost-types", + "prost 0.11.9", + "prost-types 0.11.9", "tonic", "tracing-core", ] @@ -978,7 +980,7 @@ dependencies = [ "futures", "hdrhistogram", "humantime", - "prost-types", + "prost-types 0.11.9", "serde", "serde_json", "thread_local", @@ -1273,7 +1275,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7752ab68a91ff742fae595c1b45babdc17927fea#7752ab68a91ff742fae595c1b45babdc17927fea" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d37fbbf486dc44336c87acd59cf8b6feff57b330#d37fbbf486dc44336c87acd59cf8b6feff57b330" dependencies = [ "anyhow", "app-error", @@ -1667,6 +1669,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flate2" version = "1.0.27" @@ -2455,7 +2463,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7752ab68a91ff742fae595c1b45babdc17927fea#7752ab68a91ff742fae595c1b45babdc17927fea" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d37fbbf486dc44336c87acd59cf8b6feff57b330#d37fbbf486dc44336c87acd59cf8b6feff57b330" dependencies = [ "anyhow", "futures-util", @@ -2471,7 +2479,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7752ab68a91ff742fae595c1b45babdc17927fea#7752ab68a91ff742fae595c1b45babdc17927fea" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d37fbbf486dc44336c87acd59cf8b6feff57b330#d37fbbf486dc44336c87acd59cf8b6feff57b330" dependencies = [ "anyhow", "app-error", @@ -2832,7 +2840,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7752ab68a91ff742fae595c1b45babdc17927fea#7752ab68a91ff742fae595c1b45babdc17927fea" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d37fbbf486dc44336c87acd59cf8b6feff57b330#d37fbbf486dc44336c87acd59cf8b6feff57b330" dependencies = [ "anyhow", "reqwest", @@ -3262,6 +3270,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "multimap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" + [[package]] name = "nanoid" version = "0.4.0" @@ -3624,6 +3638,16 @@ dependencies = [ "sha2", ] +[[package]] +name = "petgraph" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +dependencies = [ + "fixedbitset", + "indexmap 2.0.0", +] + [[package]] name = "phf" version = "0.8.0" @@ -3923,7 +3947,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" dependencies = [ "bytes", - "prost-derive", + "prost-derive 0.11.9", +] + +[[package]] +name = "prost" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fdd22f3b9c31b53c060df4a0613a1c7f062d4115a2b984dd15b1858f7e340d" +dependencies = [ + "bytes", + "prost-derive 0.12.1", +] + +[[package]] +name = "prost-build" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bdf592881d821b83d471f8af290226c8d51402259e9bb5be7f9f8bdebbb11ac" +dependencies = [ + "bytes", + "heck 0.4.1", + "itertools 0.11.0", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost 0.12.1", + "prost-types 0.12.1", + "regex", + "syn 2.0.31", + "tempfile", + "which", ] [[package]] @@ -3939,13 +3995,35 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "prost-derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "265baba7fabd416cf5078179f7d2cbeca4ce7a9041111900675ea7c4cb8a4c32" +dependencies = [ + "anyhow", + "itertools 0.11.0", + "proc-macro2", + "quote", + "syn 2.0.31", +] + [[package]] name = "prost-types" version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13" dependencies = [ - "prost", + "prost 0.11.9", +] + +[[package]] +name = "prost-types" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e081b29f63d83a4bc75cfc9f3fe424f9156cf92d8a4f0c9407cce9a1b67327cf" +dependencies = [ + "prost 0.12.1", ] [[package]] @@ -4272,14 +4350,19 @@ dependencies = [ [[package]] name = "realtime-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7752ab68a91ff742fae595c1b45babdc17927fea#7752ab68a91ff742fae595c1b45babdc17927fea" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d37fbbf486dc44336c87acd59cf8b6feff57b330#d37fbbf486dc44336c87acd59cf8b6feff57b330" dependencies = [ "anyhow", + "bincode", "bytes", "collab", "collab-entity", + "prost 0.12.1", + "prost-build", + "protoc-bin-vendored", "serde", "serde_json", + "tokio-tungstenite", ] [[package]] @@ -4910,7 +4993,7 @@ dependencies = [ [[package]] name = "shared_entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=7752ab68a91ff742fae595c1b45babdc17927fea#7752ab68a91ff742fae595c1b45babdc17927fea" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d37fbbf486dc44336c87acd59cf8b6feff57b330#d37fbbf486dc44336c87acd59cf8b6feff57b330" dependencies = [ "anyhow", "app-error", @@ -5584,7 +5667,7 @@ dependencies = [ "hyper-timeout", "percent-encoding", "pin-project", - "prost", + "prost 0.11.9", "tokio", "tokio-stream", "tower", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index ffcc51d1862a..20685c0171c1 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -26,6 +26,7 @@ members = [ "flowy-ai", "flowy-date", ] +resolver = "2" [workspace.dependencies] lib-dispatch = { workspace = true, path = "lib-dispatch" } @@ -82,7 +83,7 @@ incremental = false # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "7752ab68a91ff742fae595c1b45babdc17927fea" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d37fbbf486dc44336c87acd59cf8b6feff57b330" } # Please use the following script to update collab. # Working directory: frontend # @@ -92,11 +93,11 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "775 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "faf17b54" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "faf17b54" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "faf17b54" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "faf17b54" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "faf17b54" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "faf17b54" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "faf17b54" } -collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "faf17b54" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68459c6c501cd6d69deda04605839db1b442bb01" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68459c6c501cd6d69deda04605839db1b442bb01" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68459c6c501cd6d69deda04605839db1b442bb01" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68459c6c501cd6d69deda04605839db1b442bb01" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68459c6c501cd6d69deda04605839db1b442bb01" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68459c6c501cd6d69deda04605839db1b442bb01" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68459c6c501cd6d69deda04605839db1b442bb01" } +collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68459c6c501cd6d69deda04605839db1b442bb01" } diff --git a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs index 84f4ed6614a6..936b1d78084f 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs @@ -330,6 +330,7 @@ impl CollabStorageProvider for ServerProvider { let (sink, stream) = (channel.sink(), channel.stream()); let sink_config = SinkConfig::new() .send_timeout(8) + .with_max_payload_size(1024 * 10) .with_strategy(SinkStrategy::FixInterval(Duration::from_secs(2))); let sync_plugin = SyncPlugin::new( origin, diff --git a/frontend/rust-lib/flowy-error/src/impl_from/cloud.rs b/frontend/rust-lib/flowy-error/src/impl_from/cloud.rs index 6f15465a9cfc..65e3d073a016 100644 --- a/frontend/rust-lib/flowy-error/src/impl_from/cloud.rs +++ b/frontend/rust-lib/flowy-error/src/impl_from/cloud.rs @@ -15,8 +15,7 @@ impl From<AppResponseError> for FlowyError { AppErrorCode::MissingPayload => ErrorCode::MissingPayload, AppErrorCode::OpenError => ErrorCode::Internal, AppErrorCode::InvalidUrl => ErrorCode::InvalidURL, - AppErrorCode::InvalidRequestParams => ErrorCode::InvalidParams, - AppErrorCode::UrlMissingParameter => ErrorCode::InvalidParams, + AppErrorCode::InvalidRequest => ErrorCode::InvalidParams, AppErrorCode::InvalidOAuthProvider => ErrorCode::InvalidAuthConfig, AppErrorCode::NotLoggedIn => ErrorCode::UserUnauthorized, AppErrorCode::NotEnoughPermissions => ErrorCode::NotEnoughPermissions, diff --git a/frontend/rust-lib/flowy-folder2/src/manager.rs b/frontend/rust-lib/flowy-folder2/src/manager.rs index 4e70300ed2ac..8762299a29ff 100644 --- a/frontend/rust-lib/flowy-folder2/src/manager.rs +++ b/frontend/rust-lib/flowy-folder2/src/manager.rs @@ -450,7 +450,7 @@ impl FolderManager { } let index = params.index; - let view = create_view(params, view_layout); + let view = create_view(self.user.user_id()?, params, view_layout); self.with_folder( || (), |folder| { @@ -474,7 +474,7 @@ impl FolderManager { handler .create_built_in_view(user_id, ¶ms.view_id, ¶ms.name, view_layout.clone()) .await?; - let view = create_view(params, view_layout); + let view = create_view(self.user.user_id()?, params, view_layout); self.with_folder( || (), |folder| { @@ -915,7 +915,7 @@ impl FolderManager { index: None, }; - let view = create_view(params, import_data.view_layout); + let view = create_view(self.user.user_id()?, params, import_data.view_layout); self.with_folder( || (), |folder| { @@ -1278,6 +1278,7 @@ impl Deref for MutexFolder { unsafe impl Sync for MutexFolder {} unsafe impl Send for MutexFolder {} +#[allow(clippy::large_enum_variant)] pub enum FolderInitDataSource { /// It means using the data stored on local disk to initialize the folder LocalDisk { create_if_not_exist: bool }, diff --git a/frontend/rust-lib/flowy-folder2/src/user_default.rs b/frontend/rust-lib/flowy-folder2/src/user_default.rs index d12c1ae596d8..910273a60b60 100644 --- a/frontend/rust-lib/flowy-folder2/src/user_default.rs +++ b/frontend/rust-lib/flowy-folder2/src/user_default.rs @@ -17,8 +17,10 @@ impl DefaultFolderBuilder { workspace_id: String, handlers: &FolderOperationHandlers, ) -> FolderData { - let workspace_view_builder = - Arc::new(RwLock::new(WorkspaceViewBuilder::new(workspace_id.clone()))); + let workspace_view_builder = Arc::new(RwLock::new(WorkspaceViewBuilder::new( + workspace_id.clone(), + uid, + ))); for handler in handlers.values() { let _ = handler .create_workspace_view(uid, workspace_view_builder.clone()) @@ -41,6 +43,9 @@ impl DefaultFolderBuilder { name: "Workspace".to_string(), child_views: RepeatedViewIdentifier::new(first_level_views), created_at: timestamp(), + created_by: Some(uid), + last_edited_time: timestamp(), + last_edited_by: Some(uid), }; FolderData { @@ -48,6 +53,7 @@ impl DefaultFolderBuilder { current_view: first_view.id, views: FlattedViews::flatten_views(views), favorites: Default::default(), + recent: Default::default(), } } } diff --git a/frontend/rust-lib/flowy-folder2/src/view_operation.rs b/frontend/rust-lib/flowy-folder2/src/view_operation.rs index d9ccfab1fb6b..20e43ffa5288 100644 --- a/frontend/rust-lib/flowy-folder2/src/view_operation.rs +++ b/frontend/rust-lib/flowy-folder2/src/view_operation.rs @@ -20,13 +20,15 @@ pub type ViewData = Bytes; /// A builder for creating a view for a workspace. /// The views created by this builder will be the first level views of the workspace. pub struct WorkspaceViewBuilder { + pub uid: i64, pub workspace_id: String, pub views: Vec<ParentChildViews>, } impl WorkspaceViewBuilder { - pub fn new(workspace_id: String) -> Self { + pub fn new(workspace_id: String, uid: i64) -> Self { Self { + uid, workspace_id, views: vec![], } @@ -37,7 +39,7 @@ impl WorkspaceViewBuilder { F: Fn(ViewBuilder) -> O, O: Future<Output = ParentChildViews>, { - let builder = ViewBuilder::new(self.workspace_id.clone()); + let builder = ViewBuilder::new(self.uid, self.workspace_id.clone()); self.views.push(view_builder(builder).await); } @@ -49,6 +51,7 @@ impl WorkspaceViewBuilder { /// A builder for creating a view. /// The default layout of the view is [ViewLayout::Document] pub struct ViewBuilder { + uid: i64, parent_view_id: String, view_id: String, name: String, @@ -60,8 +63,9 @@ pub struct ViewBuilder { } impl ViewBuilder { - pub fn new(parent_view_id: String) -> Self { + pub fn new(uid: i64, parent_view_id: String) -> Self { Self { + uid, parent_view_id, view_id: gen_view_id().to_string(), name: Default::default(), @@ -99,7 +103,7 @@ impl ViewBuilder { F: Fn(ViewBuilder) -> O, O: Future<Output = ParentChildViews>, { - let builder = ViewBuilder::new(self.view_id.clone()); + let builder = ViewBuilder::new(self.uid, self.view_id.clone()); self.child_views.push(child_view_builder(builder).await); self } @@ -114,6 +118,8 @@ impl ViewBuilder { is_favorite: self.is_favorite, layout: self.layout, icon: self.icon, + created_by: Some(self.uid), + last_edited_time: 0, children: RepeatedViewIdentifier::new( self .child_views @@ -123,6 +129,7 @@ impl ViewBuilder { }) .collect(), ), + last_edited_by: Some(self.uid), }; ParentChildViews { parent_view: view, @@ -246,7 +253,7 @@ impl From<ViewLayoutPB> for ViewLayout { } } -pub(crate) fn create_view(params: CreateViewParams, layout: ViewLayout) -> View { +pub(crate) fn create_view(uid: i64, params: CreateViewParams, layout: ViewLayout) -> View { let time = timestamp(); View { id: params.view_id, @@ -258,6 +265,9 @@ pub(crate) fn create_view(params: CreateViewParams, layout: ViewLayout) -> View is_favorite: false, layout, icon: None, + created_by: Some(uid), + last_edited_time: 0, + last_edited_by: Some(uid), } } @@ -268,7 +278,7 @@ mod tests { #[tokio::test] async fn create_first_level_views_test() { let workspace_id = "w1".to_string(); - let mut builder = WorkspaceViewBuilder::new(workspace_id); + let mut builder = WorkspaceViewBuilder::new(workspace_id, 1); builder .with_view_builder(|view_builder| async { view_builder.with_name("1").build() }) .await; @@ -288,7 +298,7 @@ mod tests { #[tokio::test] async fn create_view_with_child_views_test() { let workspace_id = "w1".to_string(); - let mut builder = WorkspaceViewBuilder::new(workspace_id); + let mut builder = WorkspaceViewBuilder::new(workspace_id, 1); builder .with_view_builder(|view_builder| async { view_builder @@ -331,7 +341,7 @@ mod tests { #[tokio::test] async fn create_three_level_view_test() { let workspace_id = "w1".to_string(); - let mut builder = WorkspaceViewBuilder::new(workspace_id); + let mut builder = WorkspaceViewBuilder::new(workspace_id, 1); builder .with_view_builder(|view_builder| async { view_builder diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs index 740ac3c8a58b..de1f8f3f55d0 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs @@ -2,9 +2,10 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use anyhow::Error; +use client_api::collab_sync::collab_msg::CollabMessage; use client_api::notify::{TokenState, TokenStateReceiver}; use client_api::ws::{ - BusinessID, ConnectState, WSClient, WSClientConfig, WSConnectStateReceiver, WebSocketChannel, + ConnectState, WSClient, WSClientConfig, WSConnectStateReceiver, WebSocketChannel, }; use client_api::Client; use tokio::sync::watch; @@ -50,11 +51,7 @@ impl AFCloudServer { let token_state_rx = api_client.subscribe_token_state(); let enable_sync = Arc::new(AtomicBool::new(enable_sync)); - let ws_client = WSClient::new(WSClientConfig { - buffer_capacity: 100, - ping_per_secs: 8, - retry_connect_per_pings: 6, - }); + let ws_client = WSClient::new(WSClientConfig::default(), api_client.clone()); let ws_client = Arc::new(ws_client); let api_client = Arc::new(api_client); @@ -99,7 +96,7 @@ impl AppFlowyServer for AFCloudServer { while let Ok(token_state) = token_state_rx.recv().await { if let Some(client) = weak_client.upgrade() { match token_state { - TokenState::Refresh => match client.get_token() { + TokenState::Revoked => match client.get_token() { Ok(token) => { let _ = watch_tx.send(UserTokenState::Refresh { token }); }, @@ -110,6 +107,7 @@ impl AppFlowyServer for AFCloudServer { TokenState::Invalid => { let _ = watch_tx.send(UserTokenState::Invalid); }, + TokenState::DidRefresh => {}, } } } @@ -142,19 +140,26 @@ impl AppFlowyServer for AFCloudServer { Arc::new(AFCloudDocumentCloudServiceImpl(server)) } + #[allow(clippy::type_complexity)] fn collab_ws_channel( &self, - object_id: &str, - ) -> FutureResult<Option<(Arc<WebSocketChannel>, WSConnectStateReceiver, bool)>, anyhow::Error> - { + _object_id: &str, + ) -> FutureResult< + Option<( + Arc<WebSocketChannel<CollabMessage>>, + WSConnectStateReceiver, + bool, + )>, + anyhow::Error, + > { if self.enable_sync.load(Ordering::SeqCst) { - let object_id = object_id.to_string(); + let object_id = _object_id.to_string(); let weak_ws_client = Arc::downgrade(&self.ws_client); FutureResult::new(async move { match weak_ws_client.upgrade() { None => Ok(None), Some(ws_client) => { - let channel = ws_client.subscribe(BusinessID::CollabId, object_id).ok(); + let channel = ws_client.subscribe(object_id).ok(); let connect_state_recv = ws_client.subscribe_connect_state(); Ok(channel.map(|c| (c, connect_state_recv, ws_client.is_connected()))) }, @@ -203,7 +208,7 @@ fn spawn_ws_conn( match api_client.ws_url(&device_id) { Ok(ws_addr) => { event!(tracing::Level::INFO, "🟢reconnecting websocket"); - let _ = ws_client.connect(ws_addr).await; + let _ = ws_client.connect(ws_addr, &device_id).await; }, Err(err) => error!("Failed to get ws url: {}", err), } @@ -212,8 +217,8 @@ fn spawn_ws_conn( }, ConnectState::Unauthorized => { if let Some(api_client) = weak_api_client.upgrade() { - if enable_sync.load(Ordering::SeqCst) { - let _ = api_client.refresh().await; + if let Err(err) = api_client.refresh_token().await { + error!("Failed to refresh token: {}", err); } } }, @@ -229,7 +234,7 @@ fn spawn_ws_conn( af_spawn(async move { while let Ok(token_state) = token_state_rx.recv().await { match token_state { - TokenState::Refresh => { + TokenState::Revoked => { if let (Some(api_client), Some(ws_client), Some(device_id)) = ( weak_api_client.upgrade(), weak_ws_client.upgrade(), @@ -239,7 +244,7 @@ fn spawn_ws_conn( match api_client.ws_url(&device_id) { Ok(ws_addr) => { info!("🟢token state: {:?}, reconnecting websocket", token_state); - let _ = ws_client.connect(ws_addr).await; + let _ = ws_client.connect(ws_addr, &device_id).await; }, Err(err) => error!("Failed to get ws url: {}", err), } @@ -251,6 +256,7 @@ fn spawn_ws_conn( ws_client.disconnect().await; } }, + TokenState::DidRefresh => {}, } } }); diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs index 7f84264150b1..99f00e2d2709 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs @@ -6,7 +6,6 @@ use flowy_folder_deps::cloud::{ gen_workspace_id, FolderCloudService, FolderData, FolderSnapshot, Workspace, WorkspaceRecord, }; use lib_infra::future::FutureResult; -use lib_infra::util::timestamp; use crate::local_server::LocalServerDB; @@ -16,15 +15,14 @@ pub(crate) struct LocalServerFolderCloudServiceImpl { } impl FolderCloudService for LocalServerFolderCloudServiceImpl { - fn create_workspace(&self, _uid: i64, name: &str) -> FutureResult<Workspace, Error> { + fn create_workspace(&self, uid: i64, name: &str) -> FutureResult<Workspace, Error> { let name = name.to_string(); FutureResult::new(async move { - Ok(Workspace { - id: gen_workspace_id().to_string(), - name: name.to_string(), - child_views: Default::default(), - created_at: timestamp(), - }) + Ok(Workspace::new( + gen_workspace_id().to_string(), + name.to_string(), + uid, + )) }) } diff --git a/frontend/rust-lib/flowy-server/src/server.rs b/frontend/rust-lib/flowy-server/src/server.rs index d6699b687b95..945dbd9e9f41 100644 --- a/frontend/rust-lib/flowy-server/src/server.rs +++ b/frontend/rust-lib/flowy-server/src/server.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use anyhow::Error; +use client_api::collab_sync::collab_msg::CollabMessage; use client_api::ws::{WSConnectStateReceiver, WebSocketChannel}; use collab_entity::CollabObject; use collab_plugins::cloud_storage::RemoteCollabStorage; @@ -101,11 +102,18 @@ pub trait AppFlowyServer: Send + Sync + 'static { None } + #[allow(clippy::type_complexity)] fn collab_ws_channel( &self, _object_id: &str, - ) -> FutureResult<Option<(Arc<WebSocketChannel>, WSConnectStateReceiver, bool)>, anyhow::Error> - { + ) -> FutureResult< + Option<( + Arc<WebSocketChannel<CollabMessage>>, + WSConnectStateReceiver, + bool, + )>, + anyhow::Error, + > { FutureResult::new(async { Ok(None) }) } diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs b/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs index 55c199512edc..6efe86d1d5c7 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs @@ -13,6 +13,7 @@ use flowy_folder_deps::cloud::{ }; use lib_dispatch::prelude::af_spawn; use lib_infra::future::FutureResult; +use lib_infra::util::timestamp; use crate::response::ExtendedResponse; use crate::supabase::api::request::{ @@ -170,5 +171,11 @@ fn workspace_from_json_value(value: Value) -> Result<Workspace, Error> { .and_then(|s| DateTime::<Utc>::from_str(s).ok()) .map(|date| date.timestamp()) .unwrap_or_default(), + created_by: json.get("created_by").and_then(|value| value.as_i64()), + last_edited_time: json + .get("last_edited_time") + .and_then(|value| value.as_i64()) + .unwrap_or(timestamp()), + last_edited_by: json.get("last_edited_by").and_then(|value| value.as_i64()), }) } diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs index a76e980c4620..ede45bb1c494 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs @@ -614,7 +614,7 @@ fn default_workspace_doc_state(collab_object: &CollabObject) -> Vec<u8> { &collab_object.object_id, vec![], )); - let workspace = Workspace::new(workspace_id, "My workspace".to_string()); + let workspace = Workspace::new(workspace_id, "My workspace".to_string(), collab_object.uid); let folder = Folder::create(collab_object.uid, collab, None, FolderData::new(workspace)); folder.encode_collab_v1().doc_state.to_vec() } diff --git a/frontend/rust-lib/flowy-sqlite/src/kv/kv.rs b/frontend/rust-lib/flowy-sqlite/src/kv/kv.rs index 6348ebd536c5..423ad5db92c9 100644 --- a/frontend/rust-lib/flowy-sqlite/src/kv/kv.rs +++ b/frontend/rust-lib/flowy-sqlite/src/kv/kv.rs @@ -7,7 +7,7 @@ use serde::de::DeserializeOwned; use serde::Serialize; use crate::kv::schema::{kv_table, kv_table::dsl, KV_SQL}; -use crate::sqlite::{Database, PoolConfig}; +use crate::sqlite_impl::{Database, PoolConfig}; const DB_NAME: &str = "cache.db"; diff --git a/frontend/rust-lib/flowy-sqlite/src/lib.rs b/frontend/rust-lib/flowy-sqlite/src/lib.rs index fbdc5431d148..85982bb5e429 100644 --- a/frontend/rust-lib/flowy-sqlite/src/lib.rs +++ b/frontend/rust-lib/flowy-sqlite/src/lib.rs @@ -10,11 +10,11 @@ use std::{fmt::Debug, io, path::Path}; pub use diesel::*; pub use diesel_derives::*; -use crate::sqlite::PoolConfig; -pub use crate::sqlite::{ConnectionPool, DBConnection, Database}; +use crate::sqlite_impl::PoolConfig; +pub use crate::sqlite_impl::{ConnectionPool, DBConnection, Database}; pub mod kv; -mod sqlite; +mod sqlite_impl; pub mod schema; @@ -46,7 +46,7 @@ pub fn init<P: AsRef<Path>>(storage_path: P) -> Result<Database, io::Error> { fn as_io_error<E>(e: E) -> io::Error where - E: Into<crate::sqlite::Error> + Debug, + E: Into<crate::sqlite_impl::Error> + Debug, { let msg = format!("{:?}", e); io::Error::new(io::ErrorKind::NotConnected, msg) diff --git a/frontend/rust-lib/flowy-sqlite/src/sqlite/conn_ext.rs b/frontend/rust-lib/flowy-sqlite/src/sqlite_impl/conn_ext.rs similarity index 94% rename from frontend/rust-lib/flowy-sqlite/src/sqlite/conn_ext.rs rename to frontend/rust-lib/flowy-sqlite/src/sqlite_impl/conn_ext.rs index 31bfd109a11c..90a55b82aad3 100644 --- a/frontend/rust-lib/flowy-sqlite/src/sqlite/conn_ext.rs +++ b/frontend/rust-lib/flowy-sqlite/src/sqlite_impl/conn_ext.rs @@ -1,8 +1,9 @@ -use crate::sqlite::errors::*; use diesel::{ dsl::sql, expression::SqlLiteral, query_dsl::LoadQuery, Connection, RunQueryDsl, SqliteConnection, }; +use crate::sqlite_impl::errors::*; + pub trait ConnectionExtension: Connection { fn query<ST, T>(&self, query: &str) -> Result<T> where diff --git a/frontend/rust-lib/flowy-sqlite/src/sqlite/database.rs b/frontend/rust-lib/flowy-sqlite/src/sqlite_impl/database.rs similarity index 97% rename from frontend/rust-lib/flowy-sqlite/src/sqlite/database.rs rename to frontend/rust-lib/flowy-sqlite/src/sqlite_impl/database.rs index a78e8a48b5a9..cc5300b335eb 100644 --- a/frontend/rust-lib/flowy-sqlite/src/sqlite/database.rs +++ b/frontend/rust-lib/flowy-sqlite/src/sqlite_impl/database.rs @@ -1,9 +1,11 @@ -use crate::sqlite::{ +use std::sync::Arc; + +use r2d2::PooledConnection; + +use crate::sqlite_impl::{ errors::*, pool::{ConnectionManager, ConnectionPool, PoolConfig}, }; -use r2d2::PooledConnection; -use std::sync::Arc; pub struct Database { uri: String, diff --git a/frontend/rust-lib/flowy-sqlite/src/sqlite/errors.rs b/frontend/rust-lib/flowy-sqlite/src/sqlite_impl/errors.rs similarity index 100% rename from frontend/rust-lib/flowy-sqlite/src/sqlite/errors.rs rename to frontend/rust-lib/flowy-sqlite/src/sqlite_impl/errors.rs diff --git a/frontend/rust-lib/flowy-sqlite/src/sqlite/mod.rs b/frontend/rust-lib/flowy-sqlite/src/sqlite_impl/mod.rs similarity index 100% rename from frontend/rust-lib/flowy-sqlite/src/sqlite/mod.rs rename to frontend/rust-lib/flowy-sqlite/src/sqlite_impl/mod.rs diff --git a/frontend/rust-lib/flowy-sqlite/src/sqlite/pool.rs b/frontend/rust-lib/flowy-sqlite/src/sqlite_impl/pool.rs similarity index 95% rename from frontend/rust-lib/flowy-sqlite/src/sqlite/pool.rs rename to frontend/rust-lib/flowy-sqlite/src/sqlite_impl/pool.rs index e14ea9d7a820..8dc0eb3e6db1 100644 --- a/frontend/rust-lib/flowy-sqlite/src/sqlite/pool.rs +++ b/frontend/rust-lib/flowy-sqlite/src/sqlite_impl/pool.rs @@ -4,7 +4,7 @@ use diesel::{connection::Connection, SqliteConnection}; use r2d2::{CustomizeConnection, ManageConnection, Pool}; use scheduled_thread_pool::ScheduledThreadPool; -use crate::sqlite::{errors::*, pragma::*}; +use crate::sqlite_impl::{errors::*, pragma::*}; pub struct ConnectionPool { pub(crate) inner: Pool<ConnectionManager>, @@ -87,7 +87,7 @@ pub struct ConnectionManager { impl ManageConnection for ConnectionManager { type Connection = SqliteConnection; - type Error = crate::sqlite::Error; + type Error = crate::sqlite_impl::Error; fn connect(&self) -> Result<Self::Connection> { Ok(SqliteConnection::establish(&self.db_uri)?) @@ -142,7 +142,7 @@ impl DatabaseCustomizer { } } -impl CustomizeConnection<SqliteConnection, crate::sqlite::Error> for DatabaseCustomizer { +impl CustomizeConnection<SqliteConnection, crate::sqlite_impl::Error> for DatabaseCustomizer { fn on_acquire(&self, conn: &mut SqliteConnection) -> Result<()> { conn.pragma_set_busy_timeout(self.config.busy_timeout)?; if self.config.journal_mode != SQLiteJournalMode::WAL { diff --git a/frontend/rust-lib/flowy-sqlite/src/sqlite/pragma.rs b/frontend/rust-lib/flowy-sqlite/src/sqlite_impl/pragma.rs similarity index 97% rename from frontend/rust-lib/flowy-sqlite/src/sqlite/pragma.rs rename to frontend/rust-lib/flowy-sqlite/src/sqlite_impl/pragma.rs index d773d2d25e0b..51830197c12d 100644 --- a/frontend/rust-lib/flowy-sqlite/src/sqlite/pragma.rs +++ b/frontend/rust-lib/flowy-sqlite/src/sqlite_impl/pragma.rs @@ -1,5 +1,11 @@ #![allow(clippy::upper_case_acronyms)] -use crate::sqlite::errors::{Error, Result}; + +use std::{ + convert::{TryFrom, TryInto}, + fmt, + str::FromStr, +}; + use diesel::{ expression::SqlLiteral, query_dsl::load_dsl::LoadQuery, @@ -7,12 +13,8 @@ use diesel::{ SqliteConnection, }; -use crate::sqlite::conn_ext::ConnectionExtension; -use std::{ - convert::{TryFrom, TryInto}, - fmt, - str::FromStr, -}; +use crate::sqlite_impl::conn_ext::ConnectionExtension; +use crate::sqlite_impl::errors::{Error, Result}; pub trait PragmaExtension: ConnectionExtension { fn pragma<D: std::fmt::Display>(&self, key: &str, val: D, schema: Option<&str>) -> Result<()> { From 17651bf64c799f3b61b74a86481f291bd14e32a7 Mon Sep 17 00:00:00 2001 From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Date: Thu, 9 Nov 2023 00:30:50 +0100 Subject: [PATCH 37/56] feat: show notes icon when notes is not empty (#3893) * feat: show notes icon when notes is not empty * fix: redundant clone * chore: update collab and fix after merging main --- .../application/row/row_service.dart | 5 + .../board/presentation/board_page.dart | 5 +- .../presentation/ungrouped_items_button.dart | 1 + .../application/row/row_document_bloc.dart | 11 ++ .../database_view/widgets/card/card.dart | 74 ++++++------- .../database_view/widgets/card/card_bloc.dart | 27 ++++- .../widgets/card/card_cell_builder.dart | 8 +- .../widgets/card/cells/text_card_cell.dart | 47 ++++---- .../database_view/widgets/row/row_detail.dart | 4 +- .../widgets/row/row_document.dart | 100 ++++++++++-------- .../document/application/doc_bloc.dart | 22 ++-- frontend/appflowy_tauri/src-tauri/Cargo.lock | 24 ++--- frontend/appflowy_tauri/src-tauri/Cargo.toml | 16 +-- frontend/resources/flowy_icons/16x/notes.svg | 11 ++ frontend/rust-lib/Cargo.lock | 20 ++-- frontend/rust-lib/Cargo.toml | 16 +-- .../tests/database/local_test/test.rs | 2 + .../src/entities/row_entities.rs | 10 ++ .../src/services/database/database_editor.rs | 4 +- 19 files changed, 250 insertions(+), 157 deletions(-) create mode 100644 frontend/resources/flowy_icons/16x/notes.svg diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_service.dart index 8668341f7863..aa63d30747ff 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_service.dart @@ -40,6 +40,7 @@ class RowBackendService { required String rowId, String? iconURL, String? coverURL, + bool? isDocumentEmpty, }) { final payload = UpdateRowMetaChangesetPB.create() ..viewId = viewId @@ -52,6 +53,10 @@ class RowBackendService { payload.coverUrl = coverURL; } + if (isDocumentEmpty != null) { + payload.isDocumentEmpty = isDocumentEmpty; + } + return DatabaseEventUpdateRowMeta(payload).send(); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart index c2260a2ff093..3c1371d8bb71 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart @@ -115,9 +115,9 @@ class BoardPage extends StatelessWidget { class BoardContent extends StatefulWidget { const BoardContent({ - Key? key, + super.key, this.onEditStateChanged, - }) : super(key: key); + }); final VoidCallback? onEditStateChanged; @@ -275,6 +275,7 @@ class _BoardContentState extends State<BoardContent> { boardBloc.state.editingRow?.row.id == groupItem.row.id; final groupItemId = groupItem.row.id + groupData.group.groupId; + return AppFlowyGroupCard( key: ValueKey(groupItemId), margin: config.cardPadding, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/ungrouped_items_button.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/ungrouped_items_button.dart index dc1370832186..c1582d29f752 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/ungrouped_items_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/ungrouped_items_button.dart @@ -228,6 +228,7 @@ class UngroupedItem extends StatelessWidget { text: cellBuilder.buildCell( cellContext: cellContext, renderHook: renderHook, + hasNotes: false, ), onTap: onPressed, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_document_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_document_bloc.dart index 0e64ba597c71..9eedbbc7e909 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_document_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_document_bloc.dart @@ -5,6 +5,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; @@ -43,6 +44,14 @@ class RowDocumentBloc extends Bloc<RowDocumentEvent, RowDocumentState> { ), ); }, + updateIsEmpty: (isEmpty) async { + final unitOrFailure = await _rowBackendSvc.updateMeta( + rowId: rowId, + isDocumentEmpty: isEmpty, + ); + + unitOrFailure.fold((l) => null, (err) => Log.error(err)); + }, ); }, ); @@ -104,6 +113,8 @@ class RowDocumentEvent with _$RowDocumentEvent { _DidReceiveRowDocument; const factory RowDocumentEvent.didReceiveError(FlowyError error) = _DidReceiveError; + const factory RowDocumentEvent.updateIsEmpty(bool isDocumentEmpty) = + _UpdateIsEmpty; } @freezed diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart index b399abc45115..b8d4d09f90d4 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart @@ -48,23 +48,23 @@ class RowCard<CustomCardData> extends StatefulWidget { final RowCardStyleConfiguration styleConfiguration; const RowCard({ + super.key, required this.rowMeta, required this.viewId, - this.groupingFieldId, - this.groupId, required this.isEditing, required this.rowCache, required this.cellBuilder, required this.openCard, required this.onStartEditing, required this.onEndEditing, + this.groupingFieldId, + this.groupId, this.cardData, this.styleConfiguration = const RowCardStyleConfiguration( showAccessory: true, ), this.renderHook, - Key? key, - }) : super(key: key); + }); @override State<RowCard<CustomCardData>> createState() => @@ -79,6 +79,7 @@ class _RowCardState<T> extends State<RowCard<T>> { @override void initState() { + super.initState(); rowNotifier = EditableRowNotifier(isEditing: widget.isEditing); _cardBloc = CardBloc( viewId: widget.viewId, @@ -100,7 +101,6 @@ class _RowCardState<T> extends State<RowCard<T>> { }); popoverController = PopoverController(); - super.initState(); } @override @@ -197,21 +197,22 @@ class _RowCardState<T> extends State<RowCard<T>> { } class _CardContent<CustomCardData> extends StatelessWidget { - final CardCellBuilder<CustomCardData> cellBuilder; - final EditableRowNotifier rowNotifier; - final List<DatabaseCellContext> cells; - final RowCardRenderHook<CustomCardData>? renderHook; - final CustomCardData? cardData; - final RowCardStyleConfiguration styleConfiguration; const _CardContent({ + super.key, required this.rowNotifier, required this.cellBuilder, required this.cells, required this.cardData, required this.styleConfiguration, this.renderHook, - Key? key, - }) : super(key: key); + }); + + final CardCellBuilder<CustomCardData> cellBuilder; + final EditableRowNotifier rowNotifier; + final List<DatabaseCellContext> cells; + final RowCardRenderHook<CustomCardData>? renderHook; + final CustomCardData? cardData; + final RowCardStyleConfiguration styleConfiguration; @override Widget build(BuildContext context) { @@ -244,31 +245,30 @@ class _CardContent<CustomCardData> extends StatelessWidget { // Remove all the cell listeners. rowNotifier.unbind(); - cells.asMap().forEach( - (int index, DatabaseCellContext cellContext) { - final isEditing = index == 0 ? rowNotifier.isEditing.value : false; - final cellNotifier = EditableCardNotifier(isEditing: isEditing); - - if (index == 0) { - // Only use the first cell to receive user's input when click the edit - // button - rowNotifier.bindCell(cellContext, cellNotifier); - } - - final child = Padding( - key: cellContext.key(), - padding: styleConfiguration.cellPadding, - child: cellBuilder.buildCell( - cellContext: cellContext, - cellNotifier: cellNotifier, - renderHook: renderHook, - cardData: cardData, - ), - ); + cells.asMap().forEach((int index, DatabaseCellContext cellContext) { + final isEditing = index == 0 ? rowNotifier.isEditing.value : false; + final cellNotifier = EditableCardNotifier(isEditing: isEditing); - children.add(child); - }, - ); + if (index == 0) { + // Only use the first cell to receive user's input when click the edit + // button + rowNotifier.bindCell(cellContext, cellNotifier); + } + + final child = Padding( + key: cellContext.key(), + padding: styleConfiguration.cellPadding, + child: cellBuilder.buildCell( + cellContext: cellContext, + cellNotifier: cellNotifier, + renderHook: renderHook, + cardData: cardData, + hasNotes: !cellContext.rowMeta.isDocumentEmpty, + ), + ); + + children.add(child); + }); return children; } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart index 296a94a66172..06e873b86582 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart @@ -1,4 +1,5 @@ import 'dart:collection'; +import 'package:appflowy/plugins/database_view/application/row/row_listener.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -16,8 +17,10 @@ class CardBloc extends Bloc<RowCardEvent, RowCardState> { final String? groupFieldId; final RowBackendService _rowBackendSvc; final RowCache _rowCache; - VoidCallback? _rowCallback; final String viewId; + final RowListener _rowListener; + + VoidCallback? _rowCallback; CardBloc({ required this.rowMeta, @@ -26,6 +29,7 @@ class CardBloc extends Bloc<RowCardEvent, RowCardState> { required RowCache rowCache, required bool isEditing, }) : _rowBackendSvc = RowBackendService(viewId: viewId), + _rowListener = RowListener(rowMeta.id), _rowCache = rowCache, super( RowCardState.initial( @@ -50,6 +54,16 @@ class CardBloc extends Bloc<RowCardEvent, RowCardState> { setIsEditing: (bool isEditing) { emit(state.copyWith(isEditing: isEditing)); }, + didReceiveRowMeta: (rowMeta) { + final cells = state.cells + .map( + (cell) => cell.rowMeta.id == rowMeta.id + ? cell.copyWith(rowMeta: rowMeta) + : cell, + ) + .toList(); + emit(state.copyWith(cells: cells)); + }, ); }, ); @@ -85,6 +99,14 @@ class CardBloc extends Bloc<RowCardEvent, RowCardState> { } }, ); + + _rowListener.start( + onMetaChanged: (meta) { + if (!isClosed) { + add(RowCardEvent.didReceiveRowMeta(meta)); + } + }, + ); } } @@ -116,6 +138,9 @@ class RowCardEvent with _$RowCardEvent { List<DatabaseCellContext> cells, ChangedReason reason, ) = _DidReceiveCells; + const factory RowCardEvent.didReceiveRowMeta( + RowMetaPB meta, + ) = _DidReceiveRowMeta; } @freezed diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart index 3ab828b1efa9..695244eb2eaa 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart @@ -25,6 +25,7 @@ class CardCellBuilder<CustomCardData> { required DatabaseCellContext cellContext, EditableCardNotifier? cellNotifier, RowCardRenderHook<CustomCardData>? renderHook, + required bool hasNotes, }) { final cellControllerBuilder = CellControllerBuilder( cellContext: cellContext, @@ -86,12 +87,13 @@ class CardCellBuilder<CustomCardData> { ); case FieldType.RichText: return TextCardCell<CustomCardData>( + key: key, + style: isStyleOrNull<TextCardCellStyle>(style), + cardData: cardData, renderHook: renderHook?.renderHook[FieldType.RichText], cellControllerBuilder: cellControllerBuilder, editableNotifier: cellNotifier, - cardData: cardData, - style: isStyleOrNull<TextCardCellStyle>(style), - key: key, + showNotes: cellContext.fieldInfo.isPrimary && hasNotes, ); case FieldType.URL: return URLCardCell<CustomCardData>( diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/text_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/text_card_cell.dart index f2f99be92817..7f23f0e3745f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/text_card_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/text_card_cell.dart @@ -1,6 +1,8 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../row/cell_builder.dart'; @@ -15,19 +17,21 @@ class TextCardCellStyle extends CardCellStyle { class TextCardCell<CustomCardData> extends CardCell<CustomCardData, TextCardCellStyle> with EditableCell { - @override - final EditableCardNotifier? editableNotifier; - final CellControllerBuilder cellControllerBuilder; - final CellRenderHook<String, CustomCardData>? renderHook; - const TextCardCell({ + super.key, + super.cardData, + super.style, required this.cellControllerBuilder, - required CustomCardData? cardData, this.editableNotifier, this.renderHook, - TextCardCellStyle? style, - Key? key, - }) : super(key: key, style: style, cardData: cardData); + this.showNotes = false, + }); + + @override + final EditableCardNotifier? editableNotifier; + final CellControllerBuilder cellControllerBuilder; + final CellRenderHook<String, CustomCardData>? renderHook; + final bool showNotes; @override State<TextCardCell> createState() => _TextCellState(); @@ -122,14 +126,19 @@ class _TextCellState extends State<TextCardCell> { return const SizedBox(); } - // - Widget child; - if (state.enableEdit || focusWhenInit) { - child = _buildTextField(); - } else { - child = _buildText(state); - } - return Align(alignment: Alignment.centerLeft, child: child); + final child = state.enableEdit || focusWhenInit + ? _buildTextField() + : _buildText(state); + + return Row( + children: [ + if (widget.showNotes) ...[ + const FlowySvg(FlowySvgs.notes_s), + const HSpace(4), + ], + Expanded(child: child), + ], + ); }, ), ), @@ -151,9 +160,9 @@ class _TextCellState extends State<TextCardCell> { double _fontSize() { if (widget.style != null) { return widget.style!.fontSize; - } else { - return 14; } + + return 14; } Widget _buildText(TextCellState state) { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart index 4ab52f899e0b..c773fe5fc0e1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart @@ -15,10 +15,10 @@ class RowDetailPage extends StatefulWidget with FlowyOverlayDelegate { final GridCellBuilder cellBuilder; const RowDetailPage({ + super.key, required this.rowController, required this.cellBuilder, - Key? key, - }) : super(key: key); + }); @override State<RowDetailPage> createState() => _RowDetailPageState(); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_document.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_document.dart index 22e8ba09799a..795c6c6818ae 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_document.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_document.dart @@ -24,12 +24,8 @@ class RowDocument extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider<RowDocumentBloc>( - create: (context) => RowDocumentBloc( - viewId: viewId, - rowId: rowId, - )..add( - const RowDocumentEvent.initial(), - ), + create: (context) => RowDocumentBloc(viewId: viewId, rowId: rowId) + ..add(const RowDocumentEvent.initial()), child: BlocBuilder<RowDocumentBloc, RowDocumentState>( builder: (context, state) { return state.loadingState.when( @@ -43,6 +39,9 @@ class RowDocument extends StatelessWidget { finish: () => RowEditor( viewPB: state.viewPB!, scrollController: scrollController, + onIsEmptyChanged: (isEmpty) => context + .read<RowDocumentBloc>() + .add(RowDocumentEvent.updateIsEmpty(isEmpty)), ), ); }, @@ -56,10 +55,12 @@ class RowEditor extends StatefulWidget { super.key, required this.viewPB, required this.scrollController, + this.onIsEmptyChanged, }); final ViewPB viewPB; final ScrollController scrollController; + final void Function(bool)? onIsEmptyChanged; @override State<RowEditor> createState() => _RowEditorState(); @@ -87,47 +88,56 @@ class _RowEditorState extends State<RowEditor> { providers: [ BlocProvider.value(value: documentBloc), ], - child: BlocBuilder<DocumentBloc, DocumentState>( - builder: (context, state) { - return state.loadingState.when( - loading: () => const Center( - child: CircularProgressIndicator.adaptive(), - ), - finish: (result) { - return result.fold( - (error) => FlowyErrorPage.message( - error.toString(), - howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), - ), - (_) { - final editorState = documentBloc.editorState; - if (editorState == null) { - return const SizedBox.shrink(); - } - return IntrinsicHeight( - child: Container( - constraints: const BoxConstraints(minHeight: 300), - child: AppFlowyEditorPage( - shrinkWrap: true, - autoFocus: false, - editorState: editorState, - scrollController: widget.scrollController, - styleCustomizer: EditorStyleCustomizer( - context: context, - padding: const EdgeInsets.symmetric(horizontal: 10), + child: BlocListener<DocumentBloc, DocumentState>( + listenWhen: (previous, current) => + previous.isDocumentEmpty != current.isDocumentEmpty, + listener: (context, state) { + if (state.isDocumentEmpty != null) { + widget.onIsEmptyChanged?.call(state.isDocumentEmpty!); + } + }, + child: BlocBuilder<DocumentBloc, DocumentState>( + builder: (context, state) { + return state.loadingState.when( + loading: () => const Center( + child: CircularProgressIndicator.adaptive(), + ), + finish: (result) { + return result.fold( + (error) => FlowyErrorPage.message( + error.toString(), + howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), + ), + (_) { + final editorState = documentBloc.editorState; + if (editorState == null) { + return const SizedBox.shrink(); + } + return IntrinsicHeight( + child: Container( + constraints: const BoxConstraints(minHeight: 300), + child: AppFlowyEditorPage( + shrinkWrap: true, + autoFocus: false, + editorState: editorState, + scrollController: widget.scrollController, + styleCustomizer: EditorStyleCustomizer( + context: context, + padding: const EdgeInsets.symmetric(horizontal: 10), + ), + showParagraphPlaceholder: (editorState, node) => + editorState.document.isEmpty, + placeholderText: (node) => + LocaleKeys.cardDetails_notesPlaceholder.tr(), ), - showParagraphPlaceholder: (editorState, node) => - editorState.document.isEmpty, - placeholderText: (node) => - LocaleKeys.cardDetails_notesPlaceholder.tr(), ), - ), - ); - }, - ); - }, - ); - }, + ); + }, + ); + }, + ); + }, + ), ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart index 7ae081ba8aeb..a41057ae0c27 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart @@ -31,13 +31,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> { required this.view, }) : _documentListener = DocumentListener(id: view.id), _viewListener = ViewListener(viewId: view.id), - _documentService = DocumentService(), - _trashService = TrashService(), super(DocumentState.initial()) { - _transactionAdapter = TransactionAdapter( - documentId: view.id, - documentService: _documentService, - ); on<DocumentEvent>(_onDocumentEvent); } @@ -46,10 +40,13 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> { final DocumentListener _documentListener; final ViewListener _viewListener; - final DocumentService _documentService; - final TrashService _trashService; + final DocumentService _documentService = DocumentService(); + final TrashService _trashService = TrashService(); - late final TransactionAdapter _transactionAdapter; + late final TransactionAdapter _transactionAdapter = TransactionAdapter( + documentId: view.id, + documentService: _documentService, + ); EditorState? editorState; StreamSubscription? _subscription; @@ -158,6 +155,11 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> { // check if the document is empty. applyRules(); + + if (!isClosed) { + // ignore: invalid_use_of_visible_for_testing_member + emit(state.copyWith(isDocumentEmpty: editorState.document.isEmpty)); + } }); // output the log from the editor when debug mode @@ -240,6 +242,7 @@ class DocumentState with _$DocumentState { required DocumentLoadingState loadingState, required bool isDeleted, required bool forceClose, + bool? isDocumentEmpty, UserProfilePB? userProfilePB, }) = _DocumentState; @@ -247,6 +250,7 @@ class DocumentState with _$DocumentState { loadingState: _Loading(), isDeleted: false, forceClose: false, + isDocumentEmpty: null, userProfilePB: null, ); } diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 987a734a6fcc..678c563bd4fc 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -460,7 +460,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" dependencies = [ "borsh-derive", - "hashbrown 0.13.2", + "hashbrown 0.12.3", ] [[package]] @@ -863,7 +863,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" dependencies = [ "anyhow", "async-trait", @@ -883,7 +883,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" dependencies = [ "anyhow", "async-trait", @@ -913,7 +913,7 @@ dependencies = [ [[package]] name = "collab-derive" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" dependencies = [ "proc-macro2", "quote", @@ -925,7 +925,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" dependencies = [ "anyhow", "collab", @@ -945,7 +945,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" dependencies = [ "anyhow", "bytes", @@ -959,7 +959,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" dependencies = [ "anyhow", "chrono", @@ -1001,7 +1001,7 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" dependencies = [ "async-trait", "bincode", @@ -1022,7 +1022,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" dependencies = [ "anyhow", "async-trait", @@ -1049,7 +1049,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" dependencies = [ "anyhow", "collab", @@ -5594,9 +5594,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.107" +version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ "itoa 1.0.6", "ryu", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index 7a2ada383ad1..c15c31ae1b6a 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -48,14 +48,14 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d37 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68459c6c501cd6d69deda04605839db1b442bb01" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68459c6c501cd6d69deda04605839db1b442bb01" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68459c6c501cd6d69deda04605839db1b442bb01" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68459c6c501cd6d69deda04605839db1b442bb01" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68459c6c501cd6d69deda04605839db1b442bb01" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68459c6c501cd6d69deda04605839db1b442bb01" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68459c6c501cd6d69deda04605839db1b442bb01" } -collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68459c6c501cd6d69deda04605839db1b442bb01" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6e30a5b9" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6e30a5b9" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6e30a5b9" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6e30a5b9" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6e30a5b9" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6e30a5b9" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6e30a5b9" } +collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6e30a5b9" } diff --git a/frontend/resources/flowy_icons/16x/notes.svg b/frontend/resources/flowy_icons/16x/notes.svg new file mode 100644 index 000000000000..a6096ef23814 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/notes.svg @@ -0,0 +1,11 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g id="Icons 16 / docs"> +<g id="Group 1321314145"> +<path id="Vector" d="M9.08889 2V5.81818C9.08889 6.42067 9.5764 6.90909 10.1778 6.90909H12.3556M4.18889 14H11.8111C12.4125 14 12.9 13.5116 12.9 12.9091V7.09091C12.9 6.61883 12.7472 6.15948 12.4644 5.78182L10.2867 2.87273C9.87538 2.32333 9.22991 2 8.54444 2H4.18889C3.58751 2 3.1 2.48842 3.1 3.09091V12.9091C3.1 13.5116 3.58751 14 4.18889 14Z" stroke="#747B84" stroke-width="1.09091"/> +<g id="Vector_2"> +<path d="M5.27777 10.7273C4.97708 10.7273 4.73332 10.9715 4.73332 11.2727C4.73332 11.574 4.97708 11.8182 5.27777 11.8182H7.45554C7.75623 11.8182 7.99999 11.574 7.99999 11.2727C7.99999 10.9715 7.75623 10.7273 7.45554 10.7273H5.27777Z" fill="#747B84"/> +<path d="M4.73332 9.09091C4.73332 8.78966 4.97708 8.54546 5.27777 8.54546H9.63332C9.93401 8.54546 10.1778 8.78966 10.1778 9.09091C10.1778 9.39216 9.93401 9.63637 9.63332 9.63637H5.27777C4.97708 9.63637 4.73332 9.39216 4.73332 9.09091Z" fill="#747B84"/> +</g> +</g> +</g> +</svg> diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index bb15bbd3ac6d..44912063d1ef 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -467,7 +467,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" dependencies = [ "borsh-derive", - "hashbrown 0.13.2", + "hashbrown 0.12.3", ] [[package]] @@ -730,7 +730,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" dependencies = [ "anyhow", "async-trait", @@ -750,7 +750,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" dependencies = [ "anyhow", "async-trait", @@ -780,7 +780,7 @@ dependencies = [ [[package]] name = "collab-derive" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" dependencies = [ "proc-macro2", "quote", @@ -792,7 +792,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" dependencies = [ "anyhow", "collab", @@ -812,7 +812,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" dependencies = [ "anyhow", "bytes", @@ -826,7 +826,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" dependencies = [ "anyhow", "chrono", @@ -868,7 +868,7 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" dependencies = [ "async-trait", "bincode", @@ -889,7 +889,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" dependencies = [ "anyhow", "async-trait", @@ -916,7 +916,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=68459c6c501cd6d69deda04605839db1b442bb01#68459c6c501cd6d69deda04605839db1b442bb01" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" dependencies = [ "anyhow", "collab", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 20685c0171c1..f46c56695e50 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -93,11 +93,11 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d37 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68459c6c501cd6d69deda04605839db1b442bb01" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68459c6c501cd6d69deda04605839db1b442bb01" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68459c6c501cd6d69deda04605839db1b442bb01" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68459c6c501cd6d69deda04605839db1b442bb01" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68459c6c501cd6d69deda04605839db1b442bb01" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68459c6c501cd6d69deda04605839db1b442bb01" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68459c6c501cd6d69deda04605839db1b442bb01" } -collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "68459c6c501cd6d69deda04605839db1b442bb01" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6e30a5b9" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6e30a5b9" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6e30a5b9" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6e30a5b9" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6e30a5b9" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6e30a5b9" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6e30a5b9" } +collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6e30a5b9" } diff --git a/frontend/rust-lib/event-integration/tests/database/local_test/test.rs b/frontend/rust-lib/event-integration/tests/database/local_test/test.rs index 0d4b944ff520..c2775b70653c 100644 --- a/frontend/rust-lib/event-integration/tests/database/local_test/test.rs +++ b/frontend/rust-lib/event-integration/tests/database/local_test/test.rs @@ -265,6 +265,7 @@ async fn update_row_meta_event_with_url_test() { view_id: grid_view.id.clone(), icon_url: Some("icon_url".to_owned()), cover_url: None, + is_document_empty: None, }; let error = test.update_row_meta(changeset).await; assert!(error.is_none()); @@ -293,6 +294,7 @@ async fn update_row_meta_event_with_cover_test() { view_id: grid_view.id.clone(), cover_url: Some("cover url".to_owned()), icon_url: None, + is_document_empty: None, }; let error = test.update_row_meta(changeset).await; assert!(error.is_none()); diff --git a/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs index 2e9e4859e50a..69ba007cf4e8 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs @@ -59,6 +59,9 @@ pub struct RowMetaPB { #[pb(index = 4, one_of)] pub cover: Option<String>, + + #[pb(index = 5)] + pub is_document_empty: bool, } impl std::convert::From<&RowDetail> for RowMetaPB { @@ -68,6 +71,7 @@ impl std::convert::From<&RowDetail> for RowMetaPB { document_id: row_detail.document_id.clone(), icon: row_detail.meta.icon_url.clone(), cover: row_detail.meta.cover_url.clone(), + is_document_empty: row_detail.meta.is_document_empty.clone(), } } } @@ -78,6 +82,7 @@ impl std::convert::From<RowDetail> for RowMetaPB { document_id: row_detail.document_id, icon: row_detail.meta.icon_url, cover: row_detail.meta.cover_url, + is_document_empty: row_detail.meta.is_document_empty, } } } @@ -96,6 +101,9 @@ pub struct UpdateRowMetaChangesetPB { #[pb(index = 4, one_of)] pub cover_url: Option<String>, + + #[pb(index = 5, one_of)] + pub is_document_empty: Option<bool>, } #[derive(Debug)] @@ -104,6 +112,7 @@ pub struct UpdateRowMetaParams { pub view_id: String, pub icon_url: Option<String>, pub cover_url: Option<String>, + pub is_document_empty: Option<bool>, } impl TryInto<UpdateRowMetaParams> for UpdateRowMetaChangesetPB { @@ -122,6 +131,7 @@ impl TryInto<UpdateRowMetaParams> for UpdateRowMetaChangesetPB { view_id, icon_url: self.icon_url, cover_url: self.cover_url, + is_document_empty: self.is_document_empty, }) } } diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index 1ff4e8d5526c..d92a36723040 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -546,6 +546,7 @@ impl DatabaseEditor { document_id: row_document_id, icon: row_meta.icon_url, cover: row_meta.cover_url, + is_document_empty: row_meta.is_document_empty, }) } else { warn!("the row:{} is exist in view:{}", row_id.as_str(), view_id); @@ -577,7 +578,8 @@ impl DatabaseEditor { self.database.lock().update_row_meta(row_id, |meta_update| { meta_update .insert_cover_if_not_none(changeset.cover_url) - .insert_icon_if_not_none(changeset.icon_url); + .insert_icon_if_not_none(changeset.icon_url) + .update_is_document_empty_if_not_none(changeset.is_document_empty); }); // Use the temporary row meta to get rid of the lock that not implement the `Send` or 'Sync' trait. From 42e7317cd4bc17672ba215c12a7ac128ff201caf Mon Sep 17 00:00:00 2001 From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Date: Thu, 9 Nov 2023 00:32:10 +0100 Subject: [PATCH 38/56] fix: notifications setting (#3903) * fix: notifications setting * fix: remove dependency in reminder bloc * test: remove redundant lines --- .../integration_test/runner.dart | 6 +-- .../settings/notifications_settings_test.dart | 41 +++++++++++++++++++ .../settings/settings_runner.dart | 13 ++++++ .../lib/startup/deps_resolver.dart | 9 +--- .../lib/startup/tasks/app_widget.dart | 2 +- .../application/reminder/reminder_bloc.dart | 15 +++---- 6 files changed, 64 insertions(+), 22 deletions(-) create mode 100644 frontend/appflowy_flutter/integration_test/settings/notifications_settings_test.dart create mode 100644 frontend/appflowy_flutter/integration_test/settings/settings_runner.dart diff --git a/frontend/appflowy_flutter/integration_test/runner.dart b/frontend/appflowy_flutter/integration_test/runner.dart index fb1d3248cd99..700e500938d3 100644 --- a/frontend/appflowy_flutter/integration_test/runner.dart +++ b/frontend/appflowy_flutter/integration_test/runner.dart @@ -19,8 +19,7 @@ import 'document/document_test_runner.dart' as document_test_runner; import 'empty_test.dart' as first_test; import 'hotkeys_test.dart' as hotkeys_test; import 'import_files_test.dart' as import_files_test; -import 'settings/user_icon_test.dart' as user_icon_test; -import 'settings/user_language_test.dart' as user_language_test; +import 'settings/settings_runner.dart' as settings_test_runner; import 'share_markdown_test.dart' as share_markdown_test; import 'sidebar/sidebar_test_runner.dart' as sidebar_test_runner; import 'switch_folder_test.dart' as switch_folder_test; @@ -75,8 +74,7 @@ void main() { appearance_test_runner.main(); // User settings - user_icon_test.main(); - user_language_test.main(); + settings_test_runner.main(); if (isCloudEnabled) { auth_test_runner.main(); diff --git a/frontend/appflowy_flutter/integration_test/settings/notifications_settings_test.dart b/frontend/appflowy_flutter/integration_test/settings/notifications_settings_test.dart new file mode 100644 index 000000000000..ab7458b2501f --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/settings/notifications_settings_test.dart @@ -0,0 +1,41 @@ +import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../util/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('board add row test', () { + testWidgets('Add card from header', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.openSettings(); + await tester.openSettingsPage(SettingsPage.notifications); + await tester.pumpAndSettle(); + + final switchFinder = find.byType(Switch); + + // Defaults to enabled + Switch switchWidget = tester.widget(switchFinder); + expect(switchWidget.value, true); + + // Disable + await tester.tap(switchFinder); + await tester.pumpAndSettle(); + + switchWidget = tester.widget(switchFinder); + expect(switchWidget.value, false); + + // Enable again + await tester.tap(switchFinder); + await tester.pumpAndSettle(); + + switchWidget = tester.widget(switchFinder); + expect(switchWidget.value, true); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/settings/settings_runner.dart b/frontend/appflowy_flutter/integration_test/settings/settings_runner.dart new file mode 100644 index 000000000000..2b5691569037 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/settings/settings_runner.dart @@ -0,0 +1,13 @@ +import 'package:integration_test/integration_test.dart'; + +import 'notifications_settings_test.dart' as notifications_settings_test; +import 'user_icon_test.dart' as user_icon_test; +import 'user_language_test.dart' as user_language_test; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + notifications_settings_test.main(); + user_icon_test.main(); + user_language_test.main(); +} diff --git a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart index bc66aaf878e3..a8d8bd8faae4 100644 --- a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart +++ b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart @@ -19,7 +19,6 @@ import 'package:appflowy/user/presentation/router.dart'; import 'package:appflowy/workspace/application/edit_panel/edit_panel_bloc.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart'; -import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/user/prelude.dart'; @@ -164,13 +163,7 @@ void _resolveHomeDeps(GetIt getIt) { getIt.registerLazySingleton<TabsBloc>(() => TabsBloc()); - getIt.registerSingleton<NotificationSettingsCubit>( - NotificationSettingsCubit(), - ); - - getIt.registerSingleton<ReminderBloc>( - ReminderBloc(notificationSettings: getIt<NotificationSettingsCubit>()), - ); + getIt.registerSingleton<ReminderBloc>(ReminderBloc()); } void _resolveFolderDeps(GetIt getIt) { diff --git a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart index 265f36c3ec9e..01733a7e835f 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart @@ -131,7 +131,7 @@ class _ApplicationWidgetState extends State<ApplicationWidget> { )..readLocaleWhenAppLaunch(context), ), BlocProvider<NotificationSettingsCubit>( - create: (_) => getIt<NotificationSettingsCubit>(), + create: (_) => NotificationSettingsCubit(), ), BlocProvider<DocumentAppearanceCubit>( create: (_) => DocumentAppearanceCubit()..fetch(), diff --git a/frontend/appflowy_flutter/lib/user/application/reminder/reminder_bloc.dart b/frontend/appflowy_flutter/lib/user/application/reminder/reminder_bloc.dart index 4a34a6d00454..4110131814d0 100644 --- a/frontend/appflowy_flutter/lib/user/application/reminder/reminder_bloc.dart +++ b/frontend/appflowy_flutter/lib/user/application/reminder/reminder_bloc.dart @@ -4,10 +4,10 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/reminder/reminder_extension.dart'; import 'package:appflowy/user/application/reminder/reminder_service.dart'; +import 'package:appflowy/user/application/user_settings_service.dart'; import 'package:appflowy/workspace/application/notifications/notification_action.dart'; import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart'; import 'package:appflowy/workspace/application/notifications/notification_service.dart'; -import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:bloc/bloc.dart'; @@ -20,16 +20,11 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'reminder_bloc.freezed.dart'; class ReminderBloc extends Bloc<ReminderEvent, ReminderState> { - final NotificationSettingsCubit _notificationSettings; - late final NotificationActionBloc actionBloc; late final ReminderService reminderService; late final Timer timer; - ReminderBloc({ - required NotificationSettingsCubit notificationSettings, - }) : _notificationSettings = notificationSettings, - super(ReminderState()) { + ReminderBloc() : super(ReminderState()) { actionBloc = getIt<NotificationActionBloc>(); reminderService = const ReminderService(); timer = _periodicCheck(); @@ -146,7 +141,7 @@ class ReminderBloc extends Bloc<ReminderEvent, ReminderState> { Timer _periodicCheck() { return Timer.periodic( const Duration(minutes: 1), - (_) { + (_) async { final now = DateTime.now(); for (final reminder in state.upcomingReminders) { @@ -159,7 +154,9 @@ class ReminderBloc extends Bloc<ReminderEvent, ReminderState> { ); if (scheduledAt.isBefore(now)) { - if (_notificationSettings.state.isNotificationsEnabled) { + final notificationSettings = + await UserSettingsBackendService().getNotificationSettings(); + if (notificationSettings.notificationsEnabled) { NotificationMessage( identifier: reminder.id, title: LocaleKeys.reminderNotification_title.tr(), From 9586ea0e6fab60bc365b36c91ca42ae5e15f8873 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" <lucas.xu@appflowy.io> Date: Thu, 9 Nov 2023 13:11:13 +0800 Subject: [PATCH 39/56] feat: display the titles of a view's ancestors and the view's title on the title bar. (#3898) * feat: add no pages inside tips * feat: show view's ancestors (include itself) title on bar * feat: show view's ancestors (include itself) title on bar * test: add integration tests * fix: integration tests --- .../document_with_cover_image_test.dart | 7 +- .../sidebar/sidebar_favorites_test.dart | 24 +- .../sidebar/sidebar_icon_test.dart | 76 ++++++ .../sidebar/sidebar_test_runner.dart | 4 +- .../util/common_operations.dart | 44 ++++ .../integration_test/util/emoji.dart | 6 +- .../integration_test/util/expectation.dart | 27 +- .../database_view/tar_bar/tab_bar_view.dart | 4 +- .../lib/plugins/document/document.dart | 6 +- .../base/emoji_picker_button.dart | 40 ++- .../workspace/application/view/view_ext.dart | 19 ++ .../home/menu/view/view_item.dart | 68 +++-- .../presentation/widgets/left_bar_item.dart | 8 +- .../presentation/widgets/view_title_bar.dart | 236 ++++++++++++++++++ .../lib/style_widget/text_field.dart | 8 +- 15 files changed, 507 insertions(+), 70 deletions(-) create mode 100644 frontend/appflowy_flutter/integration_test/sidebar/sidebar_icon_test.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart diff --git a/frontend/appflowy_flutter/integration_test/document/document_with_cover_image_test.dart b/frontend/appflowy_flutter/integration_test/document/document_with_cover_image_test.dart index 360202e0164b..b2a8d2a69047 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_with_cover_image_test.dart +++ b/frontend/appflowy_flutter/integration_test/document/document_with_cover_image_test.dart @@ -1,5 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:emoji_mart/emoji_mart.dart'; import 'package:flutter/material.dart'; @@ -155,7 +156,11 @@ void main() { const hand = '👋🏿'; await tester.tapEmoji(hand); tester.expectToSeeDocumentIcon(hand); - tester.isPageWithIcon(gettingStarted, hand); + tester.expectViewHasIcon( + gettingStarted, + ViewLayoutPB.Document, + hand, + ); }); }); } diff --git a/frontend/appflowy_flutter/integration_test/sidebar/sidebar_favorites_test.dart b/frontend/appflowy_flutter/integration_test/sidebar/sidebar_favorites_test.dart index b0af036eeadd..4aff4ca836b5 100644 --- a/frontend/appflowy_flutter/integration_test/sidebar/sidebar_favorites_test.dart +++ b/frontend/appflowy_flutter/integration_test/sidebar/sidebar_favorites_test.dart @@ -53,7 +53,7 @@ void main() { await tester.favoriteViewByName(names[1]); expect( tester.findFavoritePageName(names[1]), - findsNWidgets(1), + findsNWidgets(2), ); await tester.unfavoriteViewByName(gettingStarted); @@ -99,7 +99,7 @@ void main() { ); expect( tester.findFavoritePageName(name), - findsNothing, + findsOneWidget, ); }, ); @@ -127,11 +127,11 @@ void main() { expect( find.byWidgetPredicate( (widget) => - widget is ViewItem && + widget is SingleInnerViewItem && widget.view.isFavorite && widget.categoryType == FolderCategoryType.favorite, ), - findsNWidgets(3), + findsNWidgets(6), ); await tester.hoverOnPageName( @@ -144,13 +144,8 @@ void main() { ); expect( - find.byWidgetPredicate( - (widget) => - widget is ViewItem && - widget.view.isFavorite && - widget.categoryType == FolderCategoryType.favorite, - ), - findsNWidgets(2), + tester.findAllFavoritePages(), + findsNWidgets(3), ); await tester.hoverOnPageName( @@ -163,12 +158,7 @@ void main() { ); expect( - find.byWidgetPredicate( - (widget) => - widget is ViewItem && - widget.view.isFavorite && - widget.categoryType == FolderCategoryType.favorite, - ), + tester.findAllFavoritePages(), findsNothing, ); }, diff --git a/frontend/appflowy_flutter/integration_test/sidebar/sidebar_icon_test.dart b/frontend/appflowy_flutter/integration_test/sidebar/sidebar_icon_test.dart new file mode 100644 index 000000000000..e2681c2e9533 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/sidebar/sidebar_icon_test.dart @@ -0,0 +1,76 @@ +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../util/base.dart'; +import '../util/common_operations.dart'; +import '../util/expectation.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + const emoji = '😁'; + + group('Icon', () { + testWidgets('Update page icon in sidebar', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + // create document, board, grid and calendar views + for (final value in ViewLayoutPB.values) { + await tester.createNewPageWithName( + name: value.name, + parentName: gettingStarted, + layout: value, + ); + + // update its icon + await tester.updatePageIconInSidebarByName( + name: value.name, + parentName: gettingStarted, + layout: value, + icon: emoji, + ); + + tester.expectViewHasIcon( + value.name, + value, + emoji, + ); + } + }); + + testWidgets('Update page icon in title bar', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + // create document, board, grid and calendar views + for (final value in ViewLayoutPB.values) { + await tester.createNewPageWithName( + name: value.name, + parentName: gettingStarted, + layout: value, + ); + + // update its icon + await tester.updatePageIconInTitleBarByName( + name: value.name, + layout: value, + icon: emoji, + ); + + tester.expectViewHasIcon( + value.name, + value, + emoji, + ); + + tester.expectViewTitleHasIcon( + value.name, + value, + emoji, + ); + } + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/sidebar/sidebar_test_runner.dart b/frontend/appflowy_flutter/integration_test/sidebar/sidebar_test_runner.dart index f4d54a2160c0..bf199036a83e 100644 --- a/frontend/appflowy_flutter/integration_test/sidebar/sidebar_test_runner.dart +++ b/frontend/appflowy_flutter/integration_test/sidebar/sidebar_test_runner.dart @@ -1,8 +1,9 @@ import 'package:integration_test/integration_test.dart'; -import 'sidebar_test.dart' as sidebar_test; import 'sidebar_expand_test.dart' as sidebar_expanded_test; import 'sidebar_favorites_test.dart' as sidebar_favorite_test; +import 'sidebar_icon_test.dart' as sidebar_icon_test; +import 'sidebar_test.dart' as sidebar_test; void startTesting() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -11,4 +12,5 @@ void startTesting() { sidebar_test.main(); sidebar_expanded_test.main(); sidebar_favorite_test.main(); + sidebar_icon_test.main(); } diff --git a/frontend/appflowy_flutter/integration_test/util/common_operations.dart b/frontend/appflowy_flutter/integration_test/util/common_operations.dart index 7094f4a333be..ea7232a0fb2f 100644 --- a/frontend/appflowy_flutter/integration_test/util/common_operations.dart +++ b/frontend/appflowy_flutter/integration_test/util/common_operations.dart @@ -4,6 +4,7 @@ import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/config/kv_keys.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart'; import 'package:appflowy/plugins/document/presentation/share/share_button.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/presentation/screens/screens.dart'; @@ -14,6 +15,7 @@ import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type. import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart'; +import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -23,6 +25,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'emoji.dart'; import 'util.dart'; extension CommonOperations on WidgetTester { @@ -442,6 +445,47 @@ extension CommonOperations on WidgetTester { ); await tapButton(button); } + + // update the page icon in the sidebar + Future<void> updatePageIconInSidebarByName({ + required String name, + required String parentName, + required ViewLayoutPB layout, + required String icon, + }) async { + final iconButton = find.descendant( + of: findPageName( + name, + layout: layout, + parentName: parentName, + ), + matching: + find.byTooltip(LocaleKeys.document_plugins_cover_changeIcon.tr()), + ); + await tapButton(iconButton); + await tapEmoji(icon); + await pumpAndSettle(); + } + + // update the page icon in the sidebar + Future<void> updatePageIconInTitleBarByName({ + required String name, + required ViewLayoutPB layout, + required String icon, + }) async { + await openPage( + name, + layout: layout, + ); + final title = find.descendant( + of: find.byType(ViewTitleBar), + matching: find.text(name), + ); + await tapButton(title); + await tapButton(find.byType(EmojiPickerButton)); + await tapEmoji(icon); + await pumpAndSettle(); + } } extension ViewLayoutPBTest on ViewLayoutPB { diff --git a/frontend/appflowy_flutter/integration_test/util/emoji.dart b/frontend/appflowy_flutter/integration_test/util/emoji.dart index a3bfbc02f6e5..f0e5c693a615 100644 --- a/frontend/appflowy_flutter/integration_test/util/emoji.dart +++ b/frontend/appflowy_flutter/integration_test/util/emoji.dart @@ -1,10 +1,14 @@ +import 'package:emoji_mart/emoji_mart.dart'; import 'package:flutter_test/flutter_test.dart'; import 'base.dart'; extension EmojiTestExtension on WidgetTester { Future<void> tapEmoji(String emoji) async { - final emojiWidget = find.text(emoji); + final emojiWidget = find.descendant( + of: find.byType(EmojiPicker), + matching: find.text(emoji), + ); await tapButton(emojiWidget); } } diff --git a/frontend/appflowy_flutter/integration_test/util/expectation.dart b/frontend/appflowy_flutter/integration_test/util/expectation.dart index ec2f63d30c4c..582cd85ae3de 100644 --- a/frontend/appflowy_flutter/integration_test/util/expectation.dart +++ b/frontend/appflowy_flutter/integration_test/util/expectation.dart @@ -5,6 +5,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emo import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/presentation/home/home_stack.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; +import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -164,7 +165,7 @@ extension Expectation on WidgetTester { }) { return find.byWidgetPredicate( (widget) => - widget is ViewItem && + widget is SingleInnerViewItem && widget.view.isFavorite && widget.categoryType == FolderCategoryType.favorite && widget.view.name == name && @@ -173,6 +174,15 @@ extension Expectation on WidgetTester { ); } + Finder findAllFavoritePages() { + return find.byWidgetPredicate( + (widget) => + widget is SingleInnerViewItem && + widget.view.isFavorite && + widget.categoryType == FolderCategoryType.favorite, + ); + } + Finder findPageName( String name, { ViewLayoutPB layout = ViewLayoutPB.Document, @@ -201,12 +211,23 @@ extension Expectation on WidgetTester { ); } - void isPageWithIcon(String name, String emoji) { - final pageName = findPageName(name); + void expectViewHasIcon(String name, ViewLayoutPB layout, String emoji) { + final pageName = findPageName( + name, + layout: layout, + ); final icon = find.descendant( of: pageName, matching: find.text(emoji), ); expect(icon, findsOneWidget); } + + void expectViewTitleHasIcon(String name, ViewLayoutPB layout, String emoji) { + final icon = find.descendant( + of: find.byType(ViewTitleBar), + matching: find.text(emoji), + ); + expect(icon, findsOneWidget); + } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tab_bar_view.dart b/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tab_bar_view.dart index 54d244dc2a94..87355800c84a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tab_bar_view.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tab_bar_view.dart @@ -3,8 +3,8 @@ import 'package:appflowy/plugins/database_view/widgets/share_button.dart'; import 'package:appflowy/plugins/util.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/workspace/presentation/home/home_stack.dart'; -import 'package:appflowy/workspace/presentation/widgets/left_bar_item.dart'; import 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart'; +import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -190,7 +190,7 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder { }); @override - Widget get leftBarItem => ViewLeftBarItem(view: notifier.view); + Widget get leftBarItem => ViewTitleBar(view: notifier.view); @override Widget tabBarItem(String pluginId) => ViewTabBarItem(view: notifier.view); diff --git a/frontend/appflowy_flutter/lib/plugins/document/document.dart b/frontend/appflowy_flutter/lib/plugins/document/document.dart index 56e6cfe016a1..23b4ae88b8bd 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document.dart @@ -9,10 +9,10 @@ import 'package:appflowy/plugins/document/presentation/share/share_button.dart'; import 'package:appflowy/plugins/util.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/workspace/presentation/home/home_stack.dart'; -import 'package:appflowy/workspace/presentation/widgets/left_bar_item.dart'; import 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart'; -import 'package:easy_localization/easy_localization.dart'; +import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -104,7 +104,7 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder } @override - Widget get leftBarItem => ViewLeftBarItem(view: view); + Widget get leftBarItem => ViewTitleBar(view: view); @override Widget tabBarItem(String pluginId) => ViewTabBarItem(view: notifier.view); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart index 61e505cf6143..cc81c1e7f843 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart @@ -12,8 +12,11 @@ class EmojiPickerButton extends StatelessWidget { super.key, required this.emoji, required this.onSubmitted, - this.emojiPickerSize = const Size(300, 250), + this.emojiPickerSize = const Size(360, 380), this.emojiSize = 18.0, + this.defaultIcon, + this.offset, + this.direction, }); final String emoji; @@ -21,6 +24,9 @@ class EmojiPickerButton extends StatelessWidget { final Size emojiPickerSize; final void Function(String emoji, PopoverController? controller) onSubmitted; final PopoverController popoverController = PopoverController(); + final Widget? defaultIcon; + final Offset? offset; + final PopoverDirection? direction; @override Widget build(BuildContext context) { @@ -32,6 +38,8 @@ class EmojiPickerButton extends StatelessWidget { width: emojiPickerSize.width, height: emojiPickerSize.height, ), + offset: offset, + direction: direction ?? PopoverDirection.rightWithTopAligned, popupBuilder: (context) => Container( width: emojiPickerSize.width, height: emojiPickerSize.height, @@ -41,18 +49,24 @@ class EmojiPickerButton extends StatelessWidget { onExit: () {}, ), ), - child: FlowyTextButton( - emoji, - overflow: TextOverflow.visible, - fontSize: emojiSize, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(minWidth: 35.0), - fillColor: Colors.transparent, - mainAxisAlignment: MainAxisAlignment.center, - onPressed: () { - popoverController.show(); - }, - ), + child: emoji.isEmpty && defaultIcon != null + ? FlowyButton( + useIntrinsicWidth: true, + text: defaultIcon!, + onTap: () => popoverController.show(), + ) + : FlowyTextButton( + emoji, + overflow: TextOverflow.visible, + fontSize: emojiSize, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(minWidth: 35.0), + fillColor: Colors.transparent, + mainAxisAlignment: MainAxisAlignment.center, + onPressed: () { + popoverController.show(); + }, + ), ); } else { return FlowyTextButton( diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart index 8e4490b9ce90..4b8a15590148 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart @@ -5,6 +5,7 @@ import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart' import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart'; import 'package:appflowy/plugins/document/document.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; +import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:flutter/material.dart'; @@ -104,6 +105,24 @@ extension ViewExtension on ViewPB { } FlowySvgData get iconData => layout.icon; + + Future<List<ViewPB>> getAncestors({bool includeSelf = false}) async { + final ancestors = <ViewPB>[]; + if (includeSelf) { + ancestors.add(this); + } + var parent = await ViewBackendService.getView(parentViewId); + while (parent.isLeft()) { + // parent is not null + final view = parent.getLeftOrNull<ViewPB>(); + if (view == null) { + break; + } + ancestors.add(view); + parent = await ViewBackendService.getView(view.parentViewId); + } + return ancestors.reversed.toList(); + } } extension ViewLayoutExtension on ViewLayoutPB { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart index dac51e723229..6a49b6a5bdd7 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart @@ -167,30 +167,52 @@ class InnerViewItem extends StatelessWidget { ); // if the view is expanded and has child views, render its child views - if (isExpanded && childViews.isNotEmpty) { - final children = childViews.map((childView) { - return ViewItem( - key: ValueKey('${categoryType.name} ${childView.id}'), - parentView: view, - categoryType: categoryType, - isFirstChild: childView.id == childViews.first.id, - view: childView, - level: level + 1, - onSelected: onSelected, - onTertiarySelected: onTertiarySelected, - isDraggable: isDraggable, - leftPadding: leftPadding, - isFeedback: isFeedback, + if (isExpanded) { + if (childViews.isNotEmpty) { + final children = childViews.map((childView) { + return ViewItem( + key: ValueKey('${categoryType.name} ${childView.id}'), + parentView: view, + categoryType: categoryType, + isFirstChild: childView.id == childViews.first.id, + view: childView, + level: level + 1, + onSelected: onSelected, + onTertiarySelected: onTertiarySelected, + isDraggable: isDraggable, + leftPadding: leftPadding, + isFeedback: isFeedback, + ); + }).toList(); + + child = Column( + mainAxisSize: MainAxisSize.min, + children: [ + child, + ...children, + ], ); - }).toList(); - - child = Column( - mainAxisSize: MainAxisSize.min, - children: [ - child, - ...children, - ], - ); + } else { + child = Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + child, + Container( + height: height, + alignment: Alignment.centerLeft, + child: Padding( + // add 2px to make the text align with the view item + padding: EdgeInsets.only(left: (level + 1) * leftPadding + 2), + child: FlowyText.medium( + LocaleKeys.noPagesInside.tr(), + color: Theme.of(context).hintColor, + ), + ), + ), + ], + ); + } } // wrap the child with DraggableItem if isDraggable is true diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/left_bar_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/left_bar_item.dart index 7aeed651afe5..b30e5ef931bc 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/left_bar_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/left_bar_item.dart @@ -3,11 +3,13 @@ import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:flutter/material.dart'; +// TODO: Remove this file after the migration is done. class ViewLeftBarItem extends StatefulWidget { - final ViewPB view; + ViewLeftBarItem({ + required this.view, + }) : super(key: ValueKey(view.id)); - ViewLeftBarItem({required this.view, Key? key}) - : super(key: ValueKey(view.hashCode)); + final ViewPB view; @override State<ViewLeftBarItem> createState() => _ViewLeftBarItemState(); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart new file mode 100644 index 000000000000..4f36c5cb4c3b --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart @@ -0,0 +1,236 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart'; +import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy/workspace/application/view/view_listener.dart'; +import 'package:appflowy/workspace/application/view/view_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +// workspaces / ... / view_title +class ViewTitleBar extends StatefulWidget { + const ViewTitleBar({ + super.key, + required this.view, + }); + + final ViewPB view; + + @override + State<ViewTitleBar> createState() => _ViewTitleBarState(); +} + +class _ViewTitleBarState extends State<ViewTitleBar> { + late Future<List<ViewPB>> ancestors; + + @override + void initState() { + super.initState(); + + ancestors = widget.view.getAncestors( + includeSelf: true, + ); + } + + @override + void didUpdateWidget(covariant ViewTitleBar oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.view.id != widget.view.id) { + ancestors = widget.view.getAncestors( + includeSelf: true, + ); + } + } + + @override + Widget build(BuildContext context) { + return FutureBuilder<List<ViewPB>>( + future: ancestors, + builder: ((context, snapshot) { + final ancestors = snapshot.data; + if (ancestors == null || + snapshot.connectionState != ConnectionState.done) { + return const SizedBox.shrink(); + } + return Row( + children: _buildViewTitles(ancestors), + ); + }), + ); + } + + List<Widget> _buildViewTitles(List<ViewPB> views) { + final children = <Widget>[]; + for (var i = 0; i < views.length; i++) { + final view = views[i]; + children.add( + _ViewTitle( + view: view, + behavior: i == views.length - 1 + ? _ViewTitleBehavior.editable // only the last one is editable + : _ViewTitleBehavior.uneditable, // others are not editable + ), + ); + if (i != views.length - 1) { + // if not the last one, add a divider + children.add(const FlowyText.regular('/')); + } + } + return children; + } +} + +enum _ViewTitleBehavior { + editable, + uneditable, +} + +class _ViewTitle extends StatefulWidget { + const _ViewTitle({ + required this.view, + this.behavior = _ViewTitleBehavior.editable, + }); + + final ViewPB view; + final _ViewTitleBehavior behavior; + + @override + State<_ViewTitle> createState() => _ViewTitleState(); +} + +class _ViewTitleState extends State<_ViewTitle> { + final popoverController = PopoverController(); + final textEditingController = TextEditingController(); + late final viewListener = ViewListener(viewId: widget.view.id); + + String name = ''; + String icon = ''; + + @override + void initState() { + super.initState(); + + name = widget.view.name; + icon = widget.view.icon.value; + + _resetTextEditingController(); + viewListener.start( + onViewUpdated: (view) { + setState(() { + name = view.name; + icon = view.icon.value; + _resetTextEditingController(); + }); + }, + ); + } + + @override + void dispose() { + textEditingController.dispose(); + popoverController.close(); + viewListener.stop(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + // root view + if (widget.view.parentViewId.isEmpty) { + return Row( + children: [ + FlowyText.regular(name), + const HSpace(4.0), + ], + ); + } + + final child = Row( + children: [ + FlowyText.regular( + icon, + fontSize: 18.0, + ), + const HSpace(2.0), + FlowyText.regular(name), + ], + ); + + if (widget.behavior == _ViewTitleBehavior.uneditable) { + return FlowyButton( + useIntrinsicWidth: true, + onTap: () { + context.read<TabsBloc>().openPlugin(widget.view); + }, + text: child, + ); + } + + return AppFlowyPopover( + constraints: const BoxConstraints( + maxWidth: 300, + maxHeight: 44, + ), + controller: popoverController, + direction: PopoverDirection.bottomWithCenterAligned, + offset: const Offset(0, 18), + popupBuilder: (context) { + // icon + textfield + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + EmojiPickerButton( + emoji: icon, + defaultIcon: widget.view.defaultIcon(), + direction: PopoverDirection.bottomWithCenterAligned, + offset: const Offset(0, 18), + onSubmitted: (emoji, _) { + ViewBackendService.updateViewIcon( + viewId: widget.view.id, + viewIcon: emoji, + ); + popoverController.close(); + }, + ), + const HSpace(4.0), + SizedBox( + height: 36.0, + width: 220, + child: FlowyTextField( + autoFocus: true, + controller: textEditingController, + onSubmitted: (text) { + if (text.isNotEmpty && text != name) { + ViewBackendService.updateView( + viewId: widget.view.id, + name: text, + ); + popoverController.close(); + } + }, + ), + ), + const HSpace(4.0), + ], + ); + }, + child: FlowyButton( + useIntrinsicWidth: true, + text: child, + ), + ); + } + + void _resetTextEditingController() { + textEditingController + ..text = name + ..selection = TextSelection( + baseOffset: 0, + extentOffset: name.length, + ); + } +} diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart index efad4a6f304a..2765c58a9fa1 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart @@ -76,9 +76,11 @@ class FlowyTextFieldState extends State<FlowyTextField> { if (widget.autoFocus) { WidgetsBinding.instance.addPostFrameCallback((_) { focusNode.requestFocus(); - controller.selection = TextSelection.fromPosition( - TextPosition(offset: controller.text.length), - ); + if (widget.controller == null) { + controller.selection = TextSelection.fromPosition( + TextPosition(offset: controller.text.length), + ); + } }); } } From 889a313cc2ba3aaa98347888154647bdec45aa06 Mon Sep 17 00:00:00 2001 From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Date: Thu, 9 Nov 2023 06:11:33 +0100 Subject: [PATCH 40/56] chore: update to version 0.3.8 (#3902) * chore: update to version 0.3.8 * chore: Update CHANGELOG.md * chore: Update CHANGELOG.md --- CHANGELOG.md | 19 +++++++++++++++++++ frontend/Makefile.toml | 2 +- frontend/appflowy_flutter/pubspec.yaml | 3 +-- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index baa764c29f57..3de85ea06062 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Release Notes +## Version 0.3.8 - 11/13/2023 + +### New Features +- Support hiding any stack in a board +- Support customizing page icons in menu +- Display visual hint when card contains notes +- Quick action for adding new stack to a board +- Support more ways of inserting page references in documents +- Shift + click on a checkbox to power toggle its children + +### Bug fixes +- Improved color of the "Share"-button text +- Text overflow issue in Calendar properties +- Default font (Roboto) added to application +- Placeholder added for the editor inside a Card +- Toggle notifications in settings have been fixed +- Dialog for linking board/grid/calendar opens in correct position +- Quick add Card in Board at top, correctly adds a new Card at the top + ## Version 0.3.7 - 10/30/2023 ### New Features diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml index 14e62b157d74..052ca05c4934 100644 --- a/frontend/Makefile.toml +++ b/frontend/Makefile.toml @@ -25,7 +25,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.3.7" +CURRENT_APP_VERSION = "0.3.8" FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite" PRODUCT_NAME = "AppFlowy" # CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 2bb798ad59ca..ab901ec6f18d 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.3.7 +version: 0.3.8 environment: sdk: ">=3.0.0 <4.0.0" @@ -208,7 +208,6 @@ flutter: - asset: assets/google_fonts/Roboto_Mono/RobotoMono-Italic.ttf style: italic - # To add assets to your application, add an assets section, like this: assets: - assets/images/ From 3c7e636b651afea5f023c413283cee56f6abbe06 Mon Sep 17 00:00:00 2001 From: Kritarth Sharma <42487588+Kritarthsharma@users.noreply.github.com> Date: Fri, 10 Nov 2023 13:12:40 +0530 Subject: [PATCH 41/56] fix: added missing hover tooltip to toolbar item (#3786) --- .../align_toolbar_item/align_toolbar_item.dart | 11 +++++++---- .../font/customize_font_toolbar_item.dart | 18 ++++++++++++------ frontend/resources/translations/en.json | 1 + 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/align_toolbar_item/align_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/align_toolbar_item/align_toolbar_item.dart index bd88f2fa8a6c..b2ac8d189b7b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/align_toolbar_item/align_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/align_toolbar_item/align_toolbar_item.dart @@ -36,10 +36,13 @@ final alignToolbarItem = ToolbarItem( final child = MouseRegion( cursor: SystemMouseCursors.click, - child: FlowySvg( - data, - size: const Size.square(20), - color: isHighlight ? highlightColor : Colors.white, + child: FlowyTooltip( + message: LocaleKeys.document_plugins_optionAction_align.tr(), + child: FlowySvg( + data, + size: const Size.square(16), + color: isHighlight ? highlightColor : Colors.white, + ), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/font/customize_font_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/font/customize_font_toolbar_item.dart index ea041a6cdd87..3c7fae3aa0c1 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/font/customize_font_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/font/customize_font_toolbar_item.dart @@ -1,8 +1,11 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/font_family_setting.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; final customizeFontToolbarItem = ToolbarItem( @@ -34,12 +37,15 @@ final customizeFontToolbarItem = ToolbarItem( onResetFont: () async => await editorState.formatDelta(selection, { AppFlowyRichTextKeys.fontFamily: null, }), - child: const Padding( - padding: EdgeInsets.symmetric(horizontal: 4.0), - child: FlowySvg( - FlowySvgs.font_family_s, - size: Size.square(16.0), - color: Colors.white, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: FlowyTooltip( + message: LocaleKeys.document_plugins_fonts.tr(), + child: const FlowySvg( + FlowySvgs.font_family_s, + size: Size.square(16.0), + color: Colors.white, + ), ), ), ), diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index f504956308dc..41a9e9a713ec 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -616,6 +616,7 @@ "smartEditDisabled": "Connect OpenAI in Settings", "discardResponse": "Do you want to discard the AI responses?", "createInlineMathEquation": "Create equation", + "fonts": "Fonts", "toggleList": "Toggle list", "quoteList":"Quote list", "numberedList":"Numbered list", From 7eb20b232a994779300f3c8d0cc0de8f0a334631 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Sun, 12 Nov 2023 18:00:07 +0800 Subject: [PATCH 42/56] feat: adding suffix for user data folder when current cloud type is appflowy cloud (#3918) * fix: load database fail caused by spawning long run task * chore: yield long run task * chore: fmt * chore: update client api * feat: copy data between server * ci: fix af cloud test --- frontend/.vscode/tasks.json | 8 -- frontend/appflowy_flutter/dev.env | 22 ++- frontend/appflowy_flutter/lib/env/env.dart | 14 +- .../lib/startup/tasks/rust_sdk.dart | 4 +- .../lib/appflowy_backend.dart | 7 +- frontend/appflowy_tauri/src-tauri/Cargo.lock | 89 ++++++------ frontend/appflowy_tauri/src-tauri/Cargo.toml | 18 +-- frontend/rust-lib/Cargo.lock | 129 +++++++----------- frontend/rust-lib/Cargo.toml | 18 +-- .../collab-integrate/src/collab_builder.rs | 3 + frontend/rust-lib/dart-ffi/Cargo.toml | 3 +- frontend/rust-lib/dart-ffi/src/env_serde.rs | 2 +- frontend/rust-lib/dart-ffi/src/lib.rs | 2 +- .../tests/database/local_test/group_test.rs | 1 + .../rust-lib/event-integration/tests/util.rs | 18 ++- frontend/rust-lib/flowy-core/Cargo.toml | 4 +- .../src/deps_resolve/collab_deps.rs | 4 +- .../rust-lib/flowy-core/src/integrate/log.rs | 2 +- .../rust-lib/flowy-core/src/integrate/mod.rs | 1 + .../flowy-core/src/integrate/server.rs | 32 +++-- .../flowy-core/src/integrate/trait_impls.rs | 21 ++- .../rust-lib/flowy-core/src/integrate/user.rs | 4 +- .../rust-lib/flowy-core/src/integrate/util.rs | 20 +++ frontend/rust-lib/flowy-core/src/lib.rs | 81 ++++++++--- .../src/entities/row_entities.rs | 2 +- .../src/services/database/database_editor.rs | 42 +++++- .../src/services/database_view/view_editor.rs | 2 + .../field_settings/field_settings_builder.rs | 3 +- .../src/services/group/configuration.rs | 16 ++- .../rust-lib/flowy-document2/src/manager.rs | 1 + .../src/af_cloud_config.rs | 21 +++ frontend/rust-lib/flowy-server/Cargo.toml | 2 +- .../af_cloud/impls/user/cloud_service_impl.rs | 28 +++- .../src/af_cloud/impls/user/dto.rs | 6 +- .../flowy-server/src/af_cloud/server.rs | 8 +- .../flowy-server/src/supabase/api/user.rs | 2 +- .../flowy-server/tests/af_cloud_test/util.rs | 26 +++- .../rust-lib/flowy-user-deps/src/cloud.rs | 1 + .../rust-lib/flowy-user-deps/src/entities.rs | 32 ++--- .../src/anon_user_upgrade/anon_user_data.rs | 9 +- .../src/anon_user_upgrade/sync_new_user.rs | 1 + .../flowy-user/src/entities/user_profile.rs | 2 +- .../rust-lib/flowy-user/src/event_handler.rs | 8 +- frontend/rust-lib/flowy-user/src/event_map.rs | 23 ++-- frontend/rust-lib/flowy-user/src/manager.rs | 105 +++++++++----- .../flowy-user/src/services/database.rs | 49 +++---- .../flowy-user/src/services/entities.rs | 26 ++-- .../src/services/historical_user.rs | 12 +- .../flowy-user/src/services/user_sql.rs | 6 +- frontend/rust-lib/lib-log/src/lib.rs | 5 +- frontend/scripts/makefile/desktop.toml | 2 +- 51 files changed, 560 insertions(+), 387 deletions(-) create mode 100644 frontend/rust-lib/flowy-core/src/integrate/util.rs diff --git a/frontend/.vscode/tasks.json b/frontend/.vscode/tasks.json index 3c0301838479..a6027d9d1769 100644 --- a/frontend/.vscode/tasks.json +++ b/frontend/.vscode/tasks.json @@ -314,13 +314,5 @@ "cwd": "${workspaceFolder}/appflowy_flutter" } }, - { - "label": "AF: Generate AppFlowyEnv", - "type": "shell", - "command": "dart run build_runner build --delete-conflicting-outputs", - "options": { - "cwd": "${workspaceFolder}/appflowy_flutter/packages/appflowy_backend" - } - } ] } diff --git a/frontend/appflowy_flutter/dev.env b/frontend/appflowy_flutter/dev.env index 190b166547cf..c3a62060bd91 100644 --- a/frontend/appflowy_flutter/dev.env +++ b/frontend/appflowy_flutter/dev.env @@ -21,13 +21,21 @@ CLOUD_TYPE=0 # Supabase Configuration # If using Supabase (CLOUD_TYPE=1), provide the following details: -SUPABASE_URL=replace-with-your-supabase-url -SUPABASE_ANON_KEY=replace-with-your-supabase-key +SUPABASE_URL= +SUPABASE_ANON_KEY= # AppFlowy Cloud Configuration # If using AppFlowy Cloud (CLOUD_TYPE=2), provide the following details: -APPFLOWY_CLOUD_BASE_URL=https://xxxxxxxxx -APPFLOWY_CLOUD_WS_BASE_URL=wss://xxxxxxxxx -APPFLOWY_CLOUD_GOTRUE_URL=https://xxxxxxxxx - - +# For instance: +# APPFLOWY_CLOUD_BASE_URL=https://xxxxxxxxx +# APPFLOWY_CLOUD_WS_BASE_URL=wss://xxxxxxxxx +# APPFLOWY_CLOUD_GOTRUE_URL=https://xxxxxxxxx +# +# Local host machine(For local develop) +# APPFLOWY_CLOUD_BASE_URL=http://localhost:8000 +# APPFLOWY_CLOUD_WS_BASE_URL=ws://localhost:8000/ws +# APPFLOWY_CLOUD_GOTRUE_URL=http://localhost:9998 + +APPFLOWY_CLOUD_BASE_URL= +APPFLOWY_CLOUD_WS_BASE_URL= +APPFLOWY_CLOUD_GOTRUE_URL= diff --git a/frontend/appflowy_flutter/lib/env/env.dart b/frontend/appflowy_flutter/lib/env/env.dart index 0c3dc86e2773..3cb2493926ea 100644 --- a/frontend/appflowy_flutter/lib/env/env.dart +++ b/frontend/appflowy_flutter/lib/env/env.dart @@ -101,7 +101,10 @@ CloudType currentCloudType() { final value = Env.cloudType; if (value == 1) { if (Env.supabaseUrl.isEmpty || Env.supabaseAnonKey.isEmpty) { - Log.error("Supabase is not configured"); + Log.error( + "Supabase is not configured correctly. The values are: " + "url: ${Env.supabaseUrl}, anonKey: ${Env.supabaseAnonKey}", + ); return CloudType.unknown; } else { return CloudType.supabase; @@ -109,8 +112,13 @@ CloudType currentCloudType() { } if (value == 2) { - if (Env.afCloudBaseUrl.isEmpty || Env.afCloudWSBaseUrl.isEmpty) { - Log.error("AppFlowy cloud is not configured"); + if (Env.afCloudBaseUrl.isEmpty || + Env.afCloudWSBaseUrl.isEmpty || + Env.afCloudGoTrueUrl.isEmpty) { + Log.error( + "AppFlowy cloud is not configured correctly. The values are: " + "baseUrl: ${Env.afCloudBaseUrl}, wsBaseUrl: ${Env.afCloudWSBaseUrl}, gotrueUrl: ${Env.afCloudGoTrueUrl}", + ); return CloudType.unknown; } else { return CloudType.appflowyCloud; diff --git a/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart b/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart index 551e7c77dad9..c27ad3455e45 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart @@ -26,9 +26,7 @@ class InitRustSDKTask extends LaunchTask { // Pass the environment variables to the Rust SDK final env = getAppFlowyEnv(); - context.getIt<FlowySDK>().setEnv(jsonEncode(env.toJson())); - - await context.getIt<FlowySDK>().init(dir); + await context.getIt<FlowySDK>().init(dir, jsonEncode(env.toJson())); } @override diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/lib/appflowy_backend.dart b/frontend/appflowy_flutter/packages/appflowy_backend/lib/appflowy_backend.dart index 91fb5c16ec3d..2489a7c647d0 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/lib/appflowy_backend.dart +++ b/frontend/appflowy_flutter/packages/appflowy_backend/lib/appflowy_backend.dart @@ -27,15 +27,12 @@ class FlowySDK { void dispose() {} - Future<void> init(Directory sdkDir) async { + Future<void> init(Directory sdkDir, String env) async { final port = RustStreamReceiver.shared.port; ffi.set_stream_port(port); ffi.store_dart_post_cobject(NativeApi.postCObject); + ffi.set_env(env.toNativeUtf8()); ffi.init_sdk(sdkDir.path.toNativeUtf8()); } - - void setEnv(String envStr) { - ffi.set_env(envStr.toNativeUtf8()); - } } diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 678c563bd4fc..02448abe3a73 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -138,7 +138,7 @@ checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d37fbbf486dc44336c87acd59cf8b6feff57b330#d37fbbf486dc44336c87acd59cf8b6feff57b330" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0873b582d61d787e20c6a7193fdcbad2ea90de0e#0873b582d61d787e20c6a7193fdcbad2ea90de0e" dependencies = [ "anyhow", "reqwest", @@ -195,7 +195,7 @@ checksum = "7150fb5d9cc4eb0184af43ce75a89620dc3747d3c816e8b0ba200682d0155c05" dependencies = [ "async-convert", "backoff", - "base64 0.21.2", + "base64 0.21.5", "derive_builder", "futures", "rand 0.8.5", @@ -365,9 +365,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.2" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" [[package]] name = "bincode" @@ -460,7 +460,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" dependencies = [ "borsh-derive", - "hashbrown 0.12.3", + "hashbrown 0.13.2", ] [[package]] @@ -768,7 +768,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d37fbbf486dc44336c87acd59cf8b6feff57b330#d37fbbf486dc44336c87acd59cf8b6feff57b330" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0873b582d61d787e20c6a7193fdcbad2ea90de0e#0873b582d61d787e20c6a7193fdcbad2ea90de0e" dependencies = [ "anyhow", "app-error", @@ -863,7 +863,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" dependencies = [ "anyhow", "async-trait", @@ -883,11 +883,11 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" dependencies = [ "anyhow", "async-trait", - "base64 0.21.2", + "base64 0.21.5", "chrono", "collab", "collab-derive", @@ -913,7 +913,7 @@ dependencies = [ [[package]] name = "collab-derive" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" dependencies = [ "proc-macro2", "quote", @@ -925,7 +925,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" dependencies = [ "anyhow", "collab", @@ -945,7 +945,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" dependencies = [ "anyhow", "bytes", @@ -959,7 +959,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" dependencies = [ "anyhow", "chrono", @@ -1001,8 +1001,9 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" dependencies = [ + "anyhow", "async-trait", "bincode", "chrono", @@ -1022,7 +1023,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" dependencies = [ "anyhow", "async-trait", @@ -1049,7 +1050,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" dependencies = [ "anyhow", "collab", @@ -1302,7 +1303,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa 1.0.6", - "phf 0.11.2", + "phf 0.8.0", "smallvec", ] @@ -1448,7 +1449,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d37fbbf486dc44336c87acd59cf8b6feff57b330#d37fbbf486dc44336c87acd59cf8b6feff57b330" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0873b582d61d787e20c6a7193fdcbad2ea90de0e#0873b582d61d787e20c6a7193fdcbad2ea90de0e" dependencies = [ "anyhow", "app-error", @@ -1932,6 +1933,7 @@ name = "flowy-core" version = "0.1.0" dependencies = [ "anyhow", + "base64 0.21.5", "bytes", "client-api", "collab", @@ -1969,6 +1971,7 @@ dependencies = [ "tokio-stream", "tracing", "uuid", + "walkdir", ] [[package]] @@ -2107,7 +2110,7 @@ version = "0.1.0" dependencies = [ "aes-gcm", "anyhow", - "base64 0.21.2", + "base64 0.21.5", "hmac", "pbkdf2", "rand 0.8.5", @@ -2298,7 +2301,7 @@ name = "flowy-user" version = "0.1.0" dependencies = [ "anyhow", - "base64 0.21.2", + "base64 0.21.5", "bytes", "chrono", "collab", @@ -2804,7 +2807,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d37fbbf486dc44336c87acd59cf8b6feff57b330#d37fbbf486dc44336c87acd59cf8b6feff57b330" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0873b582d61d787e20c6a7193fdcbad2ea90de0e#0873b582d61d787e20c6a7193fdcbad2ea90de0e" dependencies = [ "anyhow", "futures-util", @@ -2820,7 +2823,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d37fbbf486dc44336c87acd59cf8b6feff57b330#d37fbbf486dc44336c87acd59cf8b6feff57b330" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0873b582d61d787e20c6a7193fdcbad2ea90de0e#0873b582d61d787e20c6a7193fdcbad2ea90de0e" dependencies = [ "anyhow", "app-error", @@ -3256,7 +3259,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d37fbbf486dc44336c87acd59cf8b6feff57b330#d37fbbf486dc44336c87acd59cf8b6feff57b330" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0873b582d61d787e20c6a7193fdcbad2ea90de0e#0873b582d61d787e20c6a7193fdcbad2ea90de0e" dependencies = [ "anyhow", "reqwest", @@ -3408,7 +3411,7 @@ version = "8.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" dependencies = [ - "base64 0.21.2", + "base64 0.21.5", "pem", "ring", "serde", @@ -4338,7 +4341,6 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ - "phf_macros 0.11.2", "phf_shared 0.11.2", ] @@ -4430,19 +4432,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "phf_macros" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" -dependencies = [ - "phf_generator 0.11.2", - "phf_shared 0.11.2", - "proc-macro2", - "quote", - "syn 2.0.29", -] - [[package]] name = "phf_shared" version = "0.8.0" @@ -4515,7 +4504,7 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bd9647b268a3d3e14ff09c23201133a62589c658db02bb7388c7246aafe0590" dependencies = [ - "base64 0.21.2", + "base64 0.21.5", "indexmap 1.9.3", "line-wrap", "quick-xml 0.28.2", @@ -4554,7 +4543,7 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b7fa9f396f51dffd61546fd8573ee20592287996568e6175ceb0f8699ad75d" dependencies = [ - "base64 0.21.2", + "base64 0.21.5", "byteorder", "bytes", "fallible-iterator", @@ -4684,7 +4673,7 @@ checksum = "8bdf592881d821b83d471f8af290226c8d51402259e9bb5be7f9f8bdebbb11ac" dependencies = [ "bytes", "heck 0.4.1", - "itertools 0.11.0", + "itertools 0.10.5", "log", "multimap", "once_cell", @@ -4705,7 +4694,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "265baba7fabd416cf5078179f7d2cbeca4ce7a9041111900675ea7c4cb8a4c32" dependencies = [ "anyhow", - "itertools 0.11.0", + "itertools 0.10.5", "proc-macro2", "quote", "syn 2.0.29", @@ -5000,7 +4989,7 @@ dependencies = [ [[package]] name = "realtime-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d37fbbf486dc44336c87acd59cf8b6feff57b330#d37fbbf486dc44336c87acd59cf8b6feff57b330" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0873b582d61d787e20c6a7193fdcbad2ea90de0e#0873b582d61d787e20c6a7193fdcbad2ea90de0e" dependencies = [ "anyhow", "bincode", @@ -5097,7 +5086,7 @@ version = "0.11.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" dependencies = [ - "base64 0.21.2", + "base64 0.21.5", "bytes", "cookie", "cookie_store", @@ -5344,7 +5333,7 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" dependencies = [ - "base64 0.21.2", + "base64 0.21.5", ] [[package]] @@ -5641,7 +5630,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f02d8aa6e3c385bf084924f660ce2a3a6bd333ba55b35e8590b321f35d88513" dependencies = [ - "base64 0.21.2", + "base64 0.21.5", "chrono", "hex", "indexmap 1.9.3", @@ -5744,7 +5733,7 @@ dependencies = [ [[package]] name = "shared_entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d37fbbf486dc44336c87acd59cf8b6feff57b330#d37fbbf486dc44336c87acd59cf8b6feff57b330" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0873b582d61d787e20c6a7193fdcbad2ea90de0e#0873b582d61d787e20c6a7193fdcbad2ea90de0e" dependencies = [ "anyhow", "app-error", @@ -6309,7 +6298,7 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54ad2d49fdeab4a08717f5b49a163bdc72efc3b1950b6758245fcde79b645e1a" dependencies = [ - "base64 0.21.2", + "base64 0.21.5", "brotli", "ico", "json-patch", @@ -7141,9 +7130,9 @@ dependencies = [ [[package]] name = "walkdir" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" dependencies = [ "same-file", "winapi-util", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index c15c31ae1b6a..98f446e15ec3 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -38,7 +38,7 @@ custom-protocol = ["tauri/custom-protocol"] # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d37fbbf486dc44336c87acd59cf8b6feff57b330" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "0873b582d61d787e20c6a7193fdcbad2ea90de0e" } # Please use the following script to update collab. # Working directory: frontend # @@ -48,14 +48,14 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d37 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6e30a5b9" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6e30a5b9" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6e30a5b9" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6e30a5b9" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6e30a5b9" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6e30a5b9" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6e30a5b9" } -collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6e30a5b9" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "27575c570b3a6975d2efb577367a9c56cbf5a6e1" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "27575c570b3a6975d2efb577367a9c56cbf5a6e1" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "27575c570b3a6975d2efb577367a9c56cbf5a6e1" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "27575c570b3a6975d2efb577367a9c56cbf5a6e1" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "27575c570b3a6975d2efb577367a9c56cbf5a6e1" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "27575c570b3a6975d2efb577367a9c56cbf5a6e1" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "27575c570b3a6975d2efb577367a9c56cbf5a6e1" } +collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "27575c570b3a6975d2efb577367a9c56cbf5a6e1" } diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 44912063d1ef..a8311ed3a2f9 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -124,7 +124,7 @@ checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d37fbbf486dc44336c87acd59cf8b6feff57b330#d37fbbf486dc44336c87acd59cf8b6feff57b330" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0873b582d61d787e20c6a7193fdcbad2ea90de0e#0873b582d61d787e20c6a7193fdcbad2ea90de0e" dependencies = [ "anyhow", "reqwest", @@ -175,7 +175,7 @@ checksum = "7150fb5d9cc4eb0184af43ce75a89620dc3747d3c816e8b0ba200682d0155c05" dependencies = [ "async-convert", "backoff", - "base64 0.21.3", + "base64 0.21.5", "derive_builder", "futures", "rand 0.8.5", @@ -372,9 +372,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.3" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" [[package]] name = "base64ct" @@ -666,7 +666,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d37fbbf486dc44336c87acd59cf8b6feff57b330#d37fbbf486dc44336c87acd59cf8b6feff57b330" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0873b582d61d787e20c6a7193fdcbad2ea90de0e#0873b582d61d787e20c6a7193fdcbad2ea90de0e" dependencies = [ "anyhow", "app-error", @@ -683,7 +683,7 @@ dependencies = [ "mime", "mime_guess", "parking_lot", - "prost 0.12.1", + "prost", "realtime-entity", "reqwest", "scraper 0.17.1", @@ -730,7 +730,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" dependencies = [ "anyhow", "async-trait", @@ -750,11 +750,11 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" dependencies = [ "anyhow", "async-trait", - "base64 0.21.3", + "base64 0.21.5", "chrono", "collab", "collab-derive", @@ -780,7 +780,7 @@ dependencies = [ [[package]] name = "collab-derive" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" dependencies = [ "proc-macro2", "quote", @@ -792,7 +792,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" dependencies = [ "anyhow", "collab", @@ -812,7 +812,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" dependencies = [ "anyhow", "bytes", @@ -826,7 +826,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" dependencies = [ "anyhow", "chrono", @@ -868,8 +868,9 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" dependencies = [ + "anyhow", "async-trait", "bincode", "chrono", @@ -889,7 +890,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" dependencies = [ "anyhow", "async-trait", @@ -916,7 +917,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6e30a5b9#6e30a5b9415dc934c845047730a3df6988b864ba" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" dependencies = [ "anyhow", "collab", @@ -958,29 +959,30 @@ dependencies = [ [[package]] name = "console-api" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2895653b4d9f1538a83970077cb01dfc77a4810524e51a110944688e916b18e" +checksum = "fd326812b3fd01da5bb1af7d340d0d555fd3d4b641e7f1dfcf5962a902952787" dependencies = [ - "prost 0.11.9", - "prost-types 0.11.9", + "futures-core", + "prost", + "prost-types", "tonic", "tracing-core", ] [[package]] name = "console-subscriber" -version = "0.1.10" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4cf42660ac07fcebed809cfe561dd8730bcd35b075215e6479c516bcd0d11cb" +checksum = "7481d4c57092cd1c19dd541b92bdce883de840df30aa5d03fd48a3935c01842e" dependencies = [ "console-api", "crossbeam-channel", "crossbeam-utils", - "futures", + "futures-task", "hdrhistogram", "humantime", - "prost-types 0.11.9", + "prost-types", "serde", "serde_json", "thread_local", @@ -1275,7 +1277,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d37fbbf486dc44336c87acd59cf8b6feff57b330#d37fbbf486dc44336c87acd59cf8b6feff57b330" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0873b582d61d787e20c6a7193fdcbad2ea90de0e#0873b582d61d787e20c6a7193fdcbad2ea90de0e" dependencies = [ "anyhow", "app-error", @@ -1751,6 +1753,7 @@ name = "flowy-core" version = "0.1.0" dependencies = [ "anyhow", + "base64 0.21.5", "bytes", "client-api", "collab", @@ -1789,6 +1792,7 @@ dependencies = [ "tokio-stream", "tracing", "uuid", + "walkdir", ] [[package]] @@ -1930,7 +1934,7 @@ version = "0.1.0" dependencies = [ "aes-gcm", "anyhow", - "base64 0.21.3", + "base64 0.21.5", "hmac", "pbkdf2 0.12.2", "rand 0.8.5", @@ -2130,7 +2134,7 @@ name = "flowy-user" version = "0.1.0" dependencies = [ "anyhow", - "base64 0.21.3", + "base64 0.21.5", "bytes", "chrono", "collab", @@ -2463,7 +2467,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d37fbbf486dc44336c87acd59cf8b6feff57b330#d37fbbf486dc44336c87acd59cf8b6feff57b330" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0873b582d61d787e20c6a7193fdcbad2ea90de0e#0873b582d61d787e20c6a7193fdcbad2ea90de0e" dependencies = [ "anyhow", "futures-util", @@ -2479,7 +2483,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d37fbbf486dc44336c87acd59cf8b6feff57b330#d37fbbf486dc44336c87acd59cf8b6feff57b330" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0873b582d61d787e20c6a7193fdcbad2ea90de0e#0873b582d61d787e20c6a7193fdcbad2ea90de0e" dependencies = [ "anyhow", "app-error", @@ -2840,7 +2844,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d37fbbf486dc44336c87acd59cf8b6feff57b330#d37fbbf486dc44336c87acd59cf8b6feff57b330" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0873b582d61d787e20c6a7193fdcbad2ea90de0e#0873b582d61d787e20c6a7193fdcbad2ea90de0e" dependencies = [ "anyhow", "reqwest", @@ -2920,7 +2924,7 @@ version = "8.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" dependencies = [ - "base64 0.21.3", + "base64 0.21.5", "pem", "ring", "serde", @@ -3838,7 +3842,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49b6c5ef183cd3ab4ba005f1ca64c21e8bd97ce4699cfea9e8d9a2c4958ca520" dependencies = [ - "base64 0.21.3", + "base64 0.21.5", "byteorder", "bytes", "fallible-iterator", @@ -3940,16 +3944,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "prost" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" -dependencies = [ - "bytes", - "prost-derive 0.11.9", -] - [[package]] name = "prost" version = "0.12.1" @@ -3957,7 +3951,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4fdd22f3b9c31b53c060df4a0613a1c7f062d4115a2b984dd15b1858f7e340d" dependencies = [ "bytes", - "prost-derive 0.12.1", + "prost-derive", ] [[package]] @@ -3974,27 +3968,14 @@ dependencies = [ "once_cell", "petgraph", "prettyplease", - "prost 0.12.1", - "prost-types 0.12.1", + "prost", + "prost-types", "regex", "syn 2.0.31", "tempfile", "which", ] -[[package]] -name = "prost-derive" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" -dependencies = [ - "anyhow", - "itertools 0.10.5", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "prost-derive" version = "0.12.1" @@ -4008,22 +3989,13 @@ dependencies = [ "syn 2.0.31", ] -[[package]] -name = "prost-types" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13" -dependencies = [ - "prost 0.11.9", -] - [[package]] name = "prost-types" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e081b29f63d83a4bc75cfc9f3fe424f9156cf92d8a4f0c9407cce9a1b67327cf" dependencies = [ - "prost 0.12.1", + "prost", ] [[package]] @@ -4350,14 +4322,14 @@ dependencies = [ [[package]] name = "realtime-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d37fbbf486dc44336c87acd59cf8b6feff57b330#d37fbbf486dc44336c87acd59cf8b6feff57b330" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0873b582d61d787e20c6a7193fdcbad2ea90de0e#0873b582d61d787e20c6a7193fdcbad2ea90de0e" dependencies = [ "anyhow", "bincode", "bytes", "collab", "collab-entity", - "prost 0.12.1", + "prost", "prost-build", "protoc-bin-vendored", "serde", @@ -4468,7 +4440,7 @@ version = "0.11.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" dependencies = [ - "base64 0.21.3", + "base64 0.21.5", "bytes", "cookie", "cookie_store", @@ -4703,7 +4675,7 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" dependencies = [ - "base64 0.21.3", + "base64 0.21.5", ] [[package]] @@ -4993,7 +4965,7 @@ dependencies = [ [[package]] name = "shared_entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d37fbbf486dc44336c87acd59cf8b6feff57b330#d37fbbf486dc44336c87acd59cf8b6feff57b330" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0873b582d61d787e20c6a7193fdcbad2ea90de0e#0873b582d61d787e20c6a7193fdcbad2ea90de0e" dependencies = [ "anyhow", "app-error", @@ -5650,16 +5622,15 @@ dependencies = [ [[package]] name = "tonic" -version = "0.9.2" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3082666a3a6433f7f511c7192923fa1fe07c69332d3c6a2e6bb040b569199d5a" +checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e" dependencies = [ + "async-stream", "async-trait", "axum", - "base64 0.21.3", + "base64 0.21.5", "bytes", - "futures-core", - "futures-util", "h2", "http", "http-body", @@ -5667,7 +5638,7 @@ dependencies = [ "hyper-timeout", "percent-encoding", "pin-project", - "prost 0.11.9", + "prost", "tokio", "tokio-stream", "tower", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index f46c56695e50..01876a160a2f 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -83,7 +83,7 @@ incremental = false # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d37fbbf486dc44336c87acd59cf8b6feff57b330" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "0873b582d61d787e20c6a7193fdcbad2ea90de0e" } # Please use the following script to update collab. # Working directory: frontend # @@ -93,11 +93,11 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d37 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6e30a5b9" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6e30a5b9" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6e30a5b9" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6e30a5b9" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6e30a5b9" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6e30a5b9" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6e30a5b9" } -collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6e30a5b9" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "27575c570b3a6975d2efb577367a9c56cbf5a6e1" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "27575c570b3a6975d2efb577367a9c56cbf5a6e1" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "27575c570b3a6975d2efb577367a9c56cbf5a6e1" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "27575c570b3a6975d2efb577367a9c56cbf5a6e1" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "27575c570b3a6975d2efb577367a9c56cbf5a6e1" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "27575c570b3a6975d2efb577367a9c56cbf5a6e1" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "27575c570b3a6975d2efb577367a9c56cbf5a6e1" } +collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "27575c570b3a6975d2efb577367a9c56cbf5a6e1" } diff --git a/frontend/rust-lib/collab-integrate/src/collab_builder.rs b/frontend/rust-lib/collab-integrate/src/collab_builder.rs index 9a0fedfdd9c6..b17d6b44604f 100644 --- a/frontend/rust-lib/collab-integrate/src/collab_builder.rs +++ b/frontend/rust-lib/collab-integrate/src/collab_builder.rs @@ -195,6 +195,8 @@ impl AppFlowyCollabBuilder { { let cloud_storage_type = self.cloud_storage.read().await.storage_source(); let collab_object = self.collab_object(uid, object_id, object_type)?; + let span = tracing::span!(tracing::Level::TRACE, "collab_builder", object_id = %object_id); + let _enter = span.enter(); match cloud_storage_type { CollabSource::AFCloud => { #[cfg(feature = "appflowy_cloud_integrate")] @@ -259,6 +261,7 @@ impl AppFlowyCollabBuilder { } collab.lock().initialize(); + trace!("collab initialized: {}", object_id); Ok(collab) } } diff --git a/frontend/rust-lib/dart-ffi/Cargo.toml b/frontend/rust-lib/dart-ffi/Cargo.toml index 3000f5e92219..8bd45c8483db 100644 --- a/frontend/rust-lib/dart-ffi/Cargo.toml +++ b/frontend/rust-lib/dart-ffi/Cargo.toml @@ -27,10 +27,11 @@ tracing = { version = "0.1", features = ["log"] } # workspace lib-dispatch = { workspace = true } +#flowy-core = { workspace = true, features = ["profiling"] } flowy-core = { workspace = true } flowy-notification = { workspace = true } flowy-server = { workspace = true } -flowy-server-config = { workspace = true } +flowy-server-config = { workspace = true} collab-integrate = { workspace = true } flowy-derive = { path = "../../../shared-lib/flowy-derive" } diff --git a/frontend/rust-lib/dart-ffi/src/env_serde.rs b/frontend/rust-lib/dart-ffi/src/env_serde.rs index ac7f3ad820f2..276719323160 100644 --- a/frontend/rust-lib/dart-ffi/src/env_serde.rs +++ b/frontend/rust-lib/dart-ffi/src/env_serde.rs @@ -12,7 +12,7 @@ pub struct AppFlowyEnv { impl AppFlowyEnv { /// Parse the environment variable from the frontend application. The frontend will /// pass the environment variable as a json string after launching. - pub fn parser(env_str: &str) { + pub fn write_env_from(env_str: &str) { if let Ok(env) = serde_json::from_str::<AppFlowyEnv>(env_str) { env.supabase_config.write_env(); env.appflowy_cloud_config.write_env(); diff --git a/frontend/rust-lib/dart-ffi/src/lib.rs b/frontend/rust-lib/dart-ffi/src/lib.rs index 53e2795f8396..f9af82ce7b68 100644 --- a/frontend/rust-lib/dart-ffi/src/lib.rs +++ b/frontend/rust-lib/dart-ffi/src/lib.rs @@ -163,5 +163,5 @@ pub extern "C" fn backend_log(level: i64, data: *const c_char) { pub extern "C" fn set_env(data: *const c_char) { let c_str = unsafe { CStr::from_ptr(data) }; let serde_str = c_str.to_str().unwrap(); - AppFlowyEnv::parser(serde_str); + AppFlowyEnv::write_env_from(serde_str); } diff --git a/frontend/rust-lib/event-integration/tests/database/local_test/group_test.rs b/frontend/rust-lib/event-integration/tests/database/local_test/group_test.rs index bd46d337b35c..753c4d5edeb0 100644 --- a/frontend/rust-lib/event-integration/tests/database/local_test/group_test.rs +++ b/frontend/rust-lib/event-integration/tests/database/local_test/group_test.rs @@ -12,6 +12,7 @@ async fn update_group_name_test() { assert_eq!(groups.len(), 4); assert_eq!(groups[1].group_name, "To Do"); assert_eq!(groups[2].group_name, "Doing"); + assert_eq!(groups[3].group_name, "Done"); test .update_group( diff --git a/frontend/rust-lib/event-integration/tests/util.rs b/frontend/rust-lib/event-integration/tests/util.rs index 97c52a303af0..c0066bbfd7b8 100644 --- a/frontend/rust-lib/event-integration/tests/util.rs +++ b/frontend/rust-lib/event-integration/tests/util.rs @@ -28,7 +28,7 @@ use flowy_user::errors::FlowyError; use flowy_user::event_map::UserCloudServiceProvider; use flowy_user::event_map::UserEvent::*; use flowy_user_deps::cloud::UserCloudService; -use flowy_user_deps::entities::AuthType; +use flowy_user_deps::entities::Authenticator; pub fn get_supabase_config() -> Option<SupabaseConfiguration> { dotenv::from_path(".env.ci").ok()?; @@ -44,7 +44,9 @@ impl FlowySupabaseTest { let _ = get_supabase_config()?; let test = EventIntegrationTest::new().await; test.set_auth_type(AuthTypePB::Supabase); - test.server_provider.set_auth_type(AuthType::Supabase); + test + .server_provider + .set_authenticator(Authenticator::Supabase); Some(Self { inner: test }) } @@ -209,7 +211,9 @@ impl AFCloudTest { let _ = get_af_cloud_config()?; let test = EventIntegrationTest::new().await; test.set_auth_type(AuthTypePB::AFCloud); - test.server_provider.set_auth_type(AuthType::AFCloud); + test + .server_provider + .set_authenticator(Authenticator::AFCloud); Some(Self { inner: test }) } @@ -227,6 +231,14 @@ pub fn generate_test_email() -> String { format!("{}@test.com", Uuid::new_v4()) } +/// To run the test, create a .env.ci file in the 'event-integration' directory and set the following environment variables: +/// +/// - `APPFLOWY_CLOUD_BASE_URL=http://localhost:8000` +/// - `APPFLOWY_CLOUD_WS_BASE_URL=ws://localhost:8000/ws` +/// - `APPFLOWY_CLOUD_GOTRUE_URL=http://localhost:9998` +/// +/// - `GOTRUE_ADMIN_EMAIL=admin@example.com` +/// - `GOTRUE_ADMIN_PASSWORD=password` pub fn get_af_cloud_config() -> Option<AFCloudConfiguration> { dotenv::from_filename("./.env.ci").ok()?; AFCloudConfiguration::from_env().ok() diff --git a/frontend/rust-lib/flowy-core/Cargo.toml b/frontend/rust-lib/flowy-core/Cargo.toml index 1dbc66e0117a..ed578334102d 100644 --- a/frontend/rust-lib/flowy-core/Cargo.toml +++ b/frontend/rust-lib/flowy-core/Cargo.toml @@ -38,15 +38,17 @@ futures-core = { version = "0.3", default-features = false } bytes = "1.5" tokio = { version = "1.26", features = ["full"] } tokio-stream = {version = "0.1.14", features = ["sync"]} -console-subscriber = { version = "0.1.8", optional = true } +console-subscriber = { version = "0.2", optional = true } parking_lot = "0.12.1" anyhow = "1.0.75" +base64 = "0.21.5" lib-infra = { path = "../../../shared-lib/lib-infra" } serde = "1.0" serde_json = "1.0" serde_repr = "0.1" futures = "0.3.28" +walkdir = "2.4.0" [features] default = ["rev-sqlite"] diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/collab_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/collab_deps.rs index cef0cf1689c3..34f0ab9658be 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/collab_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/collab_deps.rs @@ -47,7 +47,7 @@ impl SnapshotPersistence for SnapshotDBImpl { { let conn = pool .get() - .map_err(|e| PersistenceError::Internal(Box::new(e)))?; + .map_err(|e| PersistenceError::Internal(e.into()))?; let desc = match CollabSnapshotTableSql::get_latest_snapshot(&object_id, &conn) { None => Ok("".to_string()), @@ -70,7 +70,7 @@ impl SnapshotPersistence for SnapshotDBImpl { }, &conn, ) - .map_err(|e| PersistenceError::Internal(Box::new(e))); + .map_err(|e| PersistenceError::Internal(e.into())); if let Err(e) = result { tracing::warn!("create snapshot error: {:?}", e); diff --git a/frontend/rust-lib/flowy-core/src/integrate/log.rs b/frontend/rust-lib/flowy-core/src/integrate/log.rs index c6c606a00b1c..10286ee74942 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/log.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/log.rs @@ -7,7 +7,7 @@ pub(crate) fn init_log(config: &AppFlowyCoreConfig) { if !INIT_LOG.load(Ordering::SeqCst) { INIT_LOG.store(true, Ordering::SeqCst); - let _ = lib_log::Builder::new("AppFlowy-Client", &config.storage_path) + let _ = lib_log::Builder::new("log", &config.storage_path) .env_filter(&config.log_filter) .build(); } diff --git a/frontend/rust-lib/flowy-core/src/integrate/mod.rs b/frontend/rust-lib/flowy-core/src/integrate/mod.rs index 7484472f5aa5..e929b4716b79 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/mod.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/mod.rs @@ -3,3 +3,4 @@ pub(crate) mod log; pub(crate) mod server; mod trait_impls; pub(crate) mod user; +pub(crate) mod util; diff --git a/frontend/rust-lib/flowy-core/src/integrate/server.rs b/frontend/rust-lib/flowy-core/src/integrate/server.rs index adc593243871..75acca4a18a9 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/server.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/server.rs @@ -46,7 +46,7 @@ impl Display for ServerType { } } -/// The [ServerProvider] provides list of [AppFlowyServer] base on the [AuthType]. Using +/// The [ServerProvider] provides list of [AppFlowyServer] base on the [Authenticator]. Using /// the auth type, the [ServerProvider] will create a new [AppFlowyServer] if it doesn't /// exist. /// Each server implements the [AppFlowyServer] trait, which provides the [UserCloudService], etc. @@ -66,13 +66,13 @@ pub struct ServerProvider { impl ServerProvider { pub fn new( config: AppFlowyCoreConfig, - provider_type: ServerType, + server_type: ServerType, store_preferences: Weak<StorePreferences>, ) -> Self { let encryption = EncryptionImpl::new(None); Self { config, - server_type: RwLock::new(provider_type), + server_type: RwLock::new(server_type), device_id: Arc::new(RwLock::new(uuid::Uuid::new_v4().to_string())), providers: RwLock::new(HashMap::new()), enable_sync: RwLock::new(true), @@ -115,7 +115,6 @@ impl ServerProvider { }, ServerType::AFCloud => { let config = AFCloudConfiguration::from_env()?; - tracing::trace!("🔑AppFlowy cloud config: {:?}", config); let server = Arc::new(AFCloudServer::new( config, *self.enable_sync.read(), @@ -153,23 +152,32 @@ impl ServerProvider { } } -impl From<AuthType> for ServerType { - fn from(auth_provider: AuthType) -> Self { +impl From<Authenticator> for ServerType { + fn from(auth_provider: Authenticator) -> Self { match auth_provider { - AuthType::Local => ServerType::Local, - AuthType::AFCloud => ServerType::AFCloud, - AuthType::Supabase => ServerType::Supabase, + Authenticator::Local => ServerType::Local, + Authenticator::AFCloud => ServerType::AFCloud, + Authenticator::Supabase => ServerType::Supabase, } } } -impl From<&AuthType> for ServerType { - fn from(auth_provider: &AuthType) -> Self { +impl From<ServerType> for Authenticator { + fn from(ty: ServerType) -> Self { + match ty { + ServerType::Local => Authenticator::Local, + ServerType::AFCloud => Authenticator::AFCloud, + ServerType::Supabase => Authenticator::Supabase, + } + } +} +impl From<&Authenticator> for ServerType { + fn from(auth_provider: &Authenticator) -> Self { Self::from(auth_provider.clone()) } } -pub fn current_server_provider(store_preferences: &Arc<StorePreferences>) -> ServerType { +pub fn current_server_type(store_preferences: &Arc<StorePreferences>) -> ServerType { match store_preferences.get_object::<ServerType>(SERVER_PROVIDER_TYPE_KEY) { None => ServerType::Local, Some(provider_type) => provider_type, diff --git a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs index 936b1d78084f..2d2acd457577 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs @@ -8,6 +8,7 @@ use collab::core::origin::{CollabClient, CollabOrigin}; use collab::preclude::CollabPlugin; use collab_entity::CollabType; use tokio_stream::wrappers::WatchStream; +use tracing::instrument; use collab_integrate::collab_builder::{CollabPluginContext, CollabSource, CollabStorageProvider}; use collab_integrate::postgres::SupabaseDBPlugin; @@ -23,7 +24,7 @@ use flowy_folder_deps::cloud::{ use flowy_storage::{FileStorageService, StorageObject}; use flowy_user::event_map::UserCloudServiceProvider; use flowy_user_deps::cloud::UserCloudService; -use flowy_user_deps::entities::{AuthType, UserTokenState}; +use flowy_user_deps::entities::{Authenticator, UserTokenState}; use lib_infra::future::{to_fut, Fut, FutureResult}; use crate::integrate::server::{ServerProvider, ServerType, SERVER_PROVIDER_TYPE_KEY}; @@ -82,21 +83,21 @@ impl UserCloudServiceProvider for ServerProvider { self.encryption.write().set_secret(secret); } - /// When user login, the provider type is set by the [AuthType] and save to disk for next use. + /// When user login, the provider type is set by the [Authenticator] and save to disk for next use. /// - /// Each [AuthType] has a corresponding [ServerType]. The [ServerType] is used + /// Each [Authenticator] has a corresponding [ServerType]. The [ServerType] is used /// to create a new [AppFlowyServer] if it doesn't exist. Once the [ServerType] is set, /// it will be used when user open the app again. /// - fn set_auth_type(&self, auth_type: AuthType) { - let server_type: ServerType = auth_type.into(); + fn set_authenticator(&self, authenticator: Authenticator) { + let server_type: ServerType = authenticator.into(); self.set_server_type(server_type.clone()); match self.store_preferences.upgrade() { None => tracing::error!("🔴Failed to update server provider type: store preferences is drop"), Some(store_preferences) => { match store_preferences.set_object(SERVER_PROVIDER_TYPE_KEY, server_type.clone()) { - Ok(_) => tracing::trace!("Update server provider type to: {:?}", server_type), + Ok(_) => tracing::trace!("Set server provider: {:?}", server_type), Err(e) => { tracing::error!("🔴Failed to update server provider type: {:?}", e); }, @@ -105,6 +106,11 @@ impl UserCloudServiceProvider for ServerProvider { } } + fn get_authenticator(&self) -> Authenticator { + let server_type = self.get_server_type(); + Authenticator::from(server_type) + } + fn set_device_id(&self, device_id: &str) { if device_id.is_empty() { tracing::error!("🔴Device id is empty"); @@ -309,6 +315,7 @@ impl CollabStorageProvider for ServerProvider { self.get_server_type().into() } + #[instrument(level = "debug", skip(self, context), fields(server_type = %self.get_server_type()))] fn get_plugins(&self, context: CollabPluginContext) -> Fut<Vec<Arc<dyn CollabPlugin>>> { match context { CollabPluginContext::Local => to_fut(async move { vec![] }), @@ -331,7 +338,7 @@ impl CollabStorageProvider for ServerProvider { let sink_config = SinkConfig::new() .send_timeout(8) .with_max_payload_size(1024 * 10) - .with_strategy(SinkStrategy::FixInterval(Duration::from_secs(2))); + .with_strategy(SinkStrategy::FixInterval(Duration::from_millis(600))); let sync_plugin = SyncPlugin::new( origin, sync_object, diff --git a/frontend/rust-lib/flowy-core/src/integrate/user.rs b/frontend/rust-lib/flowy-core/src/integrate/user.rs index 10dbd33413b2..6375aac0f04d 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/user.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/user.rs @@ -10,7 +10,7 @@ use flowy_error::FlowyResult; use flowy_folder2::manager::{FolderInitDataSource, FolderManager}; use flowy_user::event_map::{UserCloudServiceProvider, UserStatusCallback}; use flowy_user_deps::cloud::UserCloudConfig; -use flowy_user_deps::entities::{AuthType, UserProfile, UserWorkspace}; +use flowy_user_deps::entities::{Authenticator, UserProfile, UserWorkspace}; use lib_infra::future::{to_fut, Fut}; use crate::integrate::server::ServerProvider; @@ -27,7 +27,7 @@ pub(crate) struct UserStatusCallbackImpl { } impl UserStatusCallback for UserStatusCallbackImpl { - fn auth_type_did_changed(&self, _auth_type: AuthType) {} + fn authenticator_did_changed(&self, _auth_type: Authenticator) {} fn did_init( &self, diff --git a/frontend/rust-lib/flowy-core/src/integrate/util.rs b/frontend/rust-lib/flowy-core/src/integrate/util.rs new file mode 100644 index 000000000000..7eef3ea29f13 --- /dev/null +++ b/frontend/rust-lib/flowy-core/src/integrate/util.rs @@ -0,0 +1,20 @@ +use std::fs::{self}; +use std::io; +use std::path::Path; + +use walkdir::WalkDir; + +pub fn copy_dir_recursive(src: &Path, dst: &Path) -> io::Result<()> { + for entry in WalkDir::new(src).into_iter().filter_map(|e| e.ok()) { + let path = entry.path(); + let relative_path = path.strip_prefix(src).unwrap(); + let target_path = dst.join(relative_path); + + if path.is_dir() { + fs::create_dir_all(&target_path)?; + } else { + fs::copy(path, target_path)?; + } + } + Ok(()) +} diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index 41365b5a45a0..0a422a1a6dd5 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -1,21 +1,24 @@ #![allow(unused_doc_comments)] +use std::path::Path; use std::sync::Weak; use std::time::Duration; use std::{fmt, sync::Arc}; +use base64::Engine; use tokio::sync::RwLock; -use tracing::{error, event, instrument}; +use tracing::{debug, error, event, info, instrument}; use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabSource}; use flowy_database2::DatabaseManager; use flowy_document2::manager::DocumentManager; use flowy_folder2::manager::FolderManager; +use flowy_server_config::af_cloud_config::AFCloudConfiguration; use flowy_sqlite::kv::StorePreferences; use flowy_storage::FileStorageService; use flowy_task::{TaskDispatcher, TaskRunner}; use flowy_user::event_map::UserCloudServiceProvider; -use flowy_user::manager::{UserManager, UserSessionConfig}; +use flowy_user::manager::{UserManager, UserSessionConfig, URL_SAFE_ENGINE}; use lib_dispatch::prelude::*; use lib_dispatch::runtime::AFPluginRuntime; use module::make_plugins; @@ -24,8 +27,9 @@ pub use module::*; use crate::deps_resolve::*; use crate::integrate::collab_interact::CollabInteractImpl; use crate::integrate::log::{create_log_filter, init_log}; -use crate::integrate::server::{current_server_provider, ServerProvider, ServerType}; +use crate::integrate::server::{current_server_type, ServerProvider, ServerType}; use crate::integrate::user::UserStatusCallbackImpl; +use crate::integrate::util::copy_dir_recursive; mod deps_resolve; mod integrate; @@ -42,22 +46,56 @@ pub struct AppFlowyCoreConfig { /// Panics if the `root` path is not existing pub storage_path: String, log_filter: String, + cloud_config: Option<AFCloudConfiguration>, } impl fmt::Debug for AppFlowyCoreConfig { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("AppFlowyCoreConfig") - .field("storage_path", &self.storage_path) - .finish() + let mut debug = f.debug_struct("AppFlowy Configuration"); + debug.field("storage_path", &self.storage_path); + if let Some(config) = &self.cloud_config { + debug.field("base_url", &config.base_url); + debug.field("ws_url", &config.ws_base_url); + } + debug.finish() } } impl AppFlowyCoreConfig { pub fn new(root: &str, name: String) -> Self { + let cloud_config = AFCloudConfiguration::from_env().ok(); + let storage_path = match &cloud_config { + None => root.to_string(), + Some(config) => { + // Isolate the user data folder by the base url of AppFlowy cloud. This is to avoid + // the user data folder being shared by different AppFlowy cloud. + let server_base64 = URL_SAFE_ENGINE.encode(&config.base_url); + let storage_path = format!("{}_{}", root, server_base64); + + // Copy the user data folder from the root path to the isolated path + // The root path only exists when using the local version of appflowy + if !Path::new(&storage_path).exists() && Path::new(root).exists() { + info!("Copy dir from {} to {}", root, storage_path); + let src = Path::new(root); + match copy_dir_recursive(&src, Path::new(&storage_path)) { + Ok(_) => storage_path, + Err(err) => { + // when the copy dir failed, use the root path as the storage path + error!("Copy dir failed: {}", err); + root.to_string() + }, + } + } else { + storage_path + } + }, + }; + AppFlowyCoreConfig { name, - storage_path: root.to_owned(), + storage_path, log_filter: create_log_filter("info".to_owned(), vec![]), + cloud_config: AFCloudConfiguration::from_env().ok(), } } @@ -97,28 +135,33 @@ impl AppFlowyCore { #[instrument(skip(config, runtime))] async fn init(config: AppFlowyCoreConfig, runtime: Arc<AFPluginRuntime>) -> Self { - /// The profiling can be used to tracing the performance of the application. - /// Check out the [Link](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/backend/profiling) - /// for more information. - #[cfg(feature = "profiling")] - console_subscriber::init(); + #[allow(clippy::if_same_then_else)] + if cfg!(debug_assertions) { + /// The profiling can be used to tracing the performance of the application. + /// Check out the [Link](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/backend/profiling) + /// for more information. + #[cfg(feature = "profiling")] + console_subscriber::init(); - // Init the logger before anything else - init_log(&config); + // Init the logger before anything else + #[cfg(not(feature = "profiling"))] + init_log(&config); + } else { + init_log(&config); + } // Init the key value database let store_preference = Arc::new(StorePreferences::new(&config.storage_path).unwrap()); - - tracing::info!("🔥db {:?}", &config); - tracing::debug!("🔥{}", runtime); + info!("🔥{:?}", &config); let task_scheduler = TaskDispatcher::new(Duration::from_secs(2)); let task_dispatcher = Arc::new(RwLock::new(task_scheduler)); runtime.spawn(TaskRunner::run(task_dispatcher.clone())); - let provider_type = current_server_provider(&store_preference); + let server_type = current_server_type(&store_preference); + debug!("🔥runtime:{}, server:{}", runtime, server_type); let server_provider = Arc::new(ServerProvider::new( config.clone(), - provider_type, + server_type, Arc::downgrade(&store_preference), )); diff --git a/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs index 69ba007cf4e8..ad362a19d705 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs @@ -71,7 +71,7 @@ impl std::convert::From<&RowDetail> for RowMetaPB { document_id: row_detail.document_id.clone(), icon: row_detail.meta.icon_url.clone(), cover: row_detail.meta.cover_url.clone(), - is_document_empty: row_detail.meta.is_document_empty.clone(), + is_document_empty: row_detail.meta.is_document_empty, } } } diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index d92a36723040..e307f6ac0a5f 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -1275,14 +1275,42 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl { } fn get_rows(&self, view_id: &str) -> Fut<Vec<Arc<RowDetail>>> { - let database = self.database.lock(); - let rows = database.get_rows_for_view(view_id); - let row_details = rows - .into_iter() - .flat_map(|row| database.get_row_detail(&row.id)) - .collect::<Vec<RowDetail>>(); + let database = self.database.clone(); + let view_id = view_id.to_string(); + to_fut(async move { + let cloned_database = database.clone(); + // offloads the blocking operation to a thread where blocking is acceptable. This prevents + // blocking the main asynchronous runtime + let row_orders = tokio::task::spawn_blocking(move || { + cloned_database.lock().get_row_orders_for_view(&view_id) + }) + .await + .unwrap_or_default(); + tokio::task::yield_now().await; + + let mut all_rows = vec![]; + + // Loading the rows in chunks of 10 rows in order to prevent blocking the main asynchronous runtime + for chunk in row_orders.chunks(10) { + let cloned_database = database.clone(); + let chunk = chunk.to_vec(); + let rows = tokio::task::spawn_blocking(move || { + let orders = cloned_database.lock().get_rows_from_row_orders(&chunk); + let lock_guard = cloned_database.lock(); + orders + .into_iter() + .flat_map(|row| lock_guard.get_row_detail(&row.id)) + .collect::<Vec<RowDetail>>() + }) + .await + .unwrap_or_default(); + + all_rows.extend(rows); + tokio::task::yield_now().await; + } - to_fut(async move { row_details.into_iter().map(Arc::new).collect() }) + all_rows.into_iter().map(Arc::new).collect() + }) } fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Fut<Vec<Arc<RowCell>>> { diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs index 11bc2a497681..baab8ee110f1 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs @@ -7,6 +7,7 @@ use collab_database::fields::{Field, TypeOptionData}; use collab_database::rows::{Cells, Row, RowDetail, RowId}; use collab_database::views::{DatabaseLayout, DatabaseView}; use tokio::sync::{broadcast, RwLock}; +use tracing::instrument; use flowy_error::{FlowyError, FlowyResult}; use lib_dispatch::prelude::af_spawn; @@ -256,6 +257,7 @@ impl DatabaseViewEditor { .await } + #[instrument(level = "info", skip(self))] pub async fn v_get_rows(&self) -> Vec<Arc<RowDetail>> { let mut rows = self.delegate.get_rows(&self.view_id).await; self.v_filter_rows(&mut rows).await; diff --git a/frontend/rust-lib/flowy-database2/src/services/field_settings/field_settings_builder.rs b/frontend/rust-lib/flowy-database2/src/services/field_settings/field_settings_builder.rs index 751b84eafddc..4b4b49412b5f 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field_settings/field_settings_builder.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field_settings/field_settings_builder.rs @@ -7,7 +7,6 @@ use collab_database::views::{ use strum::IntoEnumIterator; use crate::entities::FieldVisibility; - use crate::services::field_settings::{FieldSettings, VISIBILITY}; /// Helper struct to create a new field setting @@ -52,7 +51,7 @@ pub fn default_field_visibility(layout_type: DatabaseLayout) -> FieldVisibility } pub fn default_field_settings_for_fields( - fields: &Vec<Field>, + fields: &[Field], layout_type: DatabaseLayout, ) -> FieldSettingsByFieldIdMap { fields diff --git a/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs b/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs index 97821aac5936..52392680d5e9 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs @@ -462,14 +462,20 @@ fn merge_groups( ) -> MergeGroupResult { let mut merge_result = MergeGroupResult::new(); // group_map is a helper map is used to filter out the new groups. - let mut new_group_map: IndexMap<String, Group> = IndexMap::new(); - new_groups.into_iter().for_each(|group_rev| { - new_group_map.insert(group_rev.id.clone(), group_rev); - }); + let mut new_group_map: IndexMap<String, Group> = new_groups + .into_iter() + .map(|group| (group.id.clone(), group)) + .collect(); // The group is ordered in old groups. Add them before adding the new groups for old in old_groups { - if let Some(new) = new_group_map.remove(&old.id) { + if let Some(index) = new_group_map.get_index_of(&old.id) { + let right = new_group_map.split_off(index); + merge_result.all_groups.extend(new_group_map.into_values()); + new_group_map = right; + } + + if let Some(new) = new_group_map.shift_remove(&old.id) { merge_result.all_groups.push(new.clone()); } else { merge_result.deleted_groups.push(old); diff --git a/frontend/rust-lib/flowy-document2/src/manager.rs b/frontend/rust-lib/flowy-document2/src/manager.rs index 1a2a8782360b..8c5a7c5d9925 100644 --- a/frontend/rust-lib/flowy-document2/src/manager.rs +++ b/frontend/rust-lib/flowy-document2/src/manager.rs @@ -162,6 +162,7 @@ impl DocumentManager { .map_err(internal_error) } + #[instrument(level = "debug", skip(self), err)] pub fn close_document(&self, doc_id: &str) -> FlowyResult<()> { self.documents.write().remove(doc_id); Ok(()) diff --git a/frontend/rust-lib/flowy-server-config/src/af_cloud_config.rs b/frontend/rust-lib/flowy-server-config/src/af_cloud_config.rs index b6c4b4c556d5..581976db99a5 100644 --- a/frontend/rust-lib/flowy-server-config/src/af_cloud_config.rs +++ b/frontend/rust-lib/flowy-server-config/src/af_cloud_config.rs @@ -1,3 +1,5 @@ +use std::fmt::Display; + use serde::{Deserialize, Serialize}; use flowy_error::{ErrorCode, FlowyError}; @@ -13,6 +15,15 @@ pub struct AFCloudConfiguration { pub gotrue_url: String, } +impl Display for AFCloudConfiguration { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "base_url: {}, ws_base_url: {}, gotrue_url: {}", + self.base_url, self.ws_base_url, self.gotrue_url, + )) + } +} + impl AFCloudConfiguration { pub fn from_env() -> Result<Self, FlowyError> { let base_url = std::env::var(APPFLOWY_CLOUD_BASE_URL).map_err(|_| { @@ -32,6 +43,16 @@ impl AFCloudConfiguration { let gotrue_url = std::env::var(APPFLOWY_CLOUD_GOTRUE_URL) .map_err(|_| FlowyError::new(ErrorCode::InvalidAuthConfig, "Missing AF_CLOUD_GOTRUE_URL"))?; + if base_url.is_empty() || ws_base_url.is_empty() || gotrue_url.is_empty() { + return Err(FlowyError::new( + ErrorCode::InvalidAuthConfig, + format!( + "Invalid APPFLOWY_CLOUD_BASE_URL: {}, APPFLOWY_CLOUD_WS_BASE_URL: {}, APPFLOWY_CLOUD_GOTRUE_URL: {}", + base_url, ws_base_url, gotrue_url, + )), + ); + } + Ok(Self { base_url, ws_base_url, diff --git a/frontend/rust-lib/flowy-server/Cargo.toml b/frontend/rust-lib/flowy-server/Cargo.toml index cfa3007ef057..76141ea518ba 100644 --- a/frontend/rust-lib/flowy-server/Cargo.toml +++ b/frontend/rust-lib/flowy-server/Cargo.toml @@ -43,7 +43,7 @@ mime_guess = "2.0" url = "2.4" tokio-util = "0.7" tokio-stream = { version = "0.1.14", features = ["sync"] } -client-api = { version = "0.1.0", features = ["collab-sync"] } +client-api = { version = "0.1.0", features = ["collab-sync", "test_util"] } lib-dispatch = { workspace = true } [dev-dependencies] diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs index ed29ee33b75a..99c64387a186 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs @@ -62,13 +62,27 @@ where let email = email.to_string(); let try_get_client = self.server.try_get_client(); FutureResult::new(async move { - // TODO(nathan): replace the admin_email and admin_password with encryption key - let admin_email = std::env::var("GOTRUE_ADMIN_EMAIL").unwrap(); - let admin_password = std::env::var("GOTRUE_ADMIN_PASSWORD").unwrap(); - let url = try_get_client? - .generate_sign_in_url_with_email(&admin_email, &admin_password, &email) - .await?; - Ok(url) + let client = try_get_client?; + let admin_email = std::env::var("GOTRUE_ADMIN_EMAIL").map_err(|_| { + anyhow!( + "GOTRUE_ADMIN_EMAIL is not set. Please set it to the admin email for the test server" + ) + })?; + let admin_password = std::env::var("GOTRUE_ADMIN_PASSWORD").map_err(|_| { + anyhow!( + "GOTRUE_ADMIN_PASSWORD is not set. Please set it to the admin password for the test server" + ) + })?; + let admin_client = + client_api::Client::new(client.base_url(), client.ws_addr(), client.gotrue_url()); + admin_client + .sign_in_password(&admin_email, &admin_password) + .await + .unwrap(); + + let action_link = admin_client.generate_sign_in_action_link(&email).await?; + let sign_in_url = client.extract_sign_in_url(&action_link).await?; + Ok(sign_in_url) }) } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs index 28fd37688c34..cda882766c12 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs @@ -3,8 +3,8 @@ use client_api::entity::auth_dto::{UpdateUserParams, UserMetaData}; use client_api::entity::{AFRole, AFUserProfile, AFWorkspaceMember}; use flowy_user_deps::entities::{ - AuthType, Role, UpdateUserProfileParams, UserProfile, WorkspaceMember, USER_METADATA_ICON_URL, - USER_METADATA_OPEN_AI_KEY, USER_METADATA_STABILITY_AI_KEY, + Authenticator, Role, UpdateUserProfileParams, UserProfile, WorkspaceMember, + USER_METADATA_ICON_URL, USER_METADATA_OPEN_AI_KEY, USER_METADATA_STABILITY_AI_KEY, }; use crate::af_cloud::impls::user::util::encryption_type_from_profile; @@ -57,7 +57,7 @@ pub fn user_profile_from_af_profile( openai_key: openai_key.unwrap_or_default(), stability_ai_key: stability_ai_key.unwrap_or_default(), workspace_id: profile.latest_workspace_id.to_string(), - auth_type: AuthType::AFCloud, + authenticator: Authenticator::AFCloud, encryption_type, uid: profile.uid, updated_at: profile.updated_at, diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs index de1f8f3f55d0..c5431583b12f 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs @@ -96,7 +96,7 @@ impl AppFlowyServer for AFCloudServer { while let Ok(token_state) = token_state_rx.recv().await { if let Some(client) = weak_client.upgrade() { match token_state { - TokenState::Revoked => match client.get_token() { + TokenState::Refresh => match client.get_token() { Ok(token) => { let _ = watch_tx.send(UserTokenState::Refresh { token }); }, @@ -107,7 +107,6 @@ impl AppFlowyServer for AFCloudServer { TokenState::Invalid => { let _ = watch_tx.send(UserTokenState::Invalid); }, - TokenState::DidRefresh => {}, } } } @@ -198,7 +197,7 @@ fn spawn_ws_conn( while let Ok(state) = state_recv.recv().await { info!("[websocket] state: {:?}", state); match state { - ConnectState::PingTimeout => { + ConnectState::PingTimeout | ConnectState::Closed => { // Try to reconnect if the connection is timed out. if let (Some(api_client), Some(device_id)) = (weak_api_client.upgrade(), weak_device_id.upgrade()) @@ -234,7 +233,7 @@ fn spawn_ws_conn( af_spawn(async move { while let Ok(token_state) = token_state_rx.recv().await { match token_state { - TokenState::Revoked => { + TokenState::Refresh => { if let (Some(api_client), Some(ws_client), Some(device_id)) = ( weak_api_client.upgrade(), weak_ws_client.upgrade(), @@ -256,7 +255,6 @@ fn spawn_ws_conn( ws_client.disconnect().await; } }, - TokenState::DidRefresh => {}, } } }); diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs index ede45bb1c494..94673eb8556b 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs @@ -218,7 +218,7 @@ where openai_key: "".to_string(), stability_ai_key: "".to_string(), workspace_id: response.latest_workspace_id, - auth_type: AuthType::Supabase, + authenticator: Authenticator::Supabase, encryption_type: EncryptionType::from_sign(&response.encryption_sign), updated_at: response.updated_at.timestamp(), }), diff --git a/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs b/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs index 88d3a59ace8b..765a5e73f371 100644 --- a/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs +++ b/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs @@ -10,6 +10,14 @@ use flowy_server_config::af_cloud_config::AFCloudConfiguration; use crate::setup_log; +/// To run the test, create a .env.ci file in the 'flowy-server' directory and set the following environment variables: +/// +/// - `APPFLOWY_CLOUD_BASE_URL=http://localhost:8000` +/// - `APPFLOWY_CLOUD_WS_BASE_URL=ws://localhost:8000/ws` +/// - `APPFLOWY_CLOUD_GOTRUE_URL=http://localhost:9998` +/// +/// - `GOTRUE_ADMIN_EMAIL=admin@example.com` +/// - `GOTRUE_ADMIN_PASSWORD=password` pub fn get_af_cloud_config() -> Option<AFCloudConfiguration> { dotenv::from_filename("./.env.ci").ok()?; setup_log(); @@ -23,15 +31,21 @@ pub fn af_cloud_server(config: AFCloudConfiguration) -> Arc<AFCloudServer> { } pub async fn generate_sign_in_url(user_email: &str, config: &AFCloudConfiguration) -> String { - let api_client = - client_api::Client::new(&config.base_url, &config.ws_base_url, &config.gotrue_url); - + let client = client_api::Client::new(&config.base_url, &config.ws_base_url, &config.gotrue_url); let admin_email = std::env::var("GOTRUE_ADMIN_EMAIL").unwrap(); let admin_password = std::env::var("GOTRUE_ADMIN_PASSWORD").unwrap(); - api_client - .generate_sign_in_url_with_email(&admin_email, &admin_password, user_email) + let admin_client = + client_api::Client::new(client.base_url(), client.ws_addr(), client.gotrue_url()); + admin_client + .sign_in_password(&admin_email, &admin_password) + .await + .unwrap(); + + let action_link = admin_client + .generate_sign_in_action_link(&user_email) .await - .unwrap() + .unwrap(); + client.extract_sign_in_url(&action_link).await.unwrap() } pub async fn af_cloud_sign_up_param( diff --git a/frontend/rust-lib/flowy-user-deps/src/cloud.rs b/frontend/rust-lib/flowy-user-deps/src/cloud.rs index e6056f578f1f..b54892d9aaff 100644 --- a/frontend/rust-lib/flowy-user-deps/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-deps/src/cloud.rs @@ -73,6 +73,7 @@ pub trait UserCloudService: Send + Sync + 'static { fn sign_out(&self, token: Option<String>) -> FutureResult<(), Error>; /// Generate a sign in url for the user with the given email + /// Currently, only use the admin client for testing fn generate_sign_in_url_with_email(&self, email: &str) -> FutureResult<String, Error>; /// When the user opens the OAuth URL, it redirects to the corresponding provider's OAuth web page. diff --git a/frontend/rust-lib/flowy-user-deps/src/entities.rs b/frontend/rust-lib/flowy-user-deps/src/entities.rs index 55bd733a4b7f..599dc1591b51 100644 --- a/frontend/rust-lib/flowy-user-deps/src/entities.rs +++ b/frontend/rust-lib/flowy-user-deps/src/entities.rs @@ -29,7 +29,7 @@ pub struct SignInParams { pub email: String, pub password: String, pub name: String, - pub auth_type: AuthType, + pub auth_type: Authenticator, pub device_id: String, } @@ -38,7 +38,7 @@ pub struct SignUpParams { pub email: String, pub name: String, pub password: String, - pub auth_type: AuthType, + pub auth_type: Authenticator, pub device_id: String, } @@ -101,7 +101,7 @@ impl UserAuthResponse for AuthResponse { #[derive(Clone, Debug)] pub struct UserCredentials { - /// Currently, the token is only used when the [AuthType] is AFCloud + /// Currently, the token is only used when the [Authenticator] is AFCloud pub token: Option<String>, /// The user id @@ -165,7 +165,7 @@ pub struct UserProfile { pub openai_key: String, pub stability_ai_key: String, pub workspace_id: String, - pub auth_type: AuthType, + pub authenticator: Authenticator, // If the encryption_sign is not empty, which means the user has enabled the encryption. pub encryption_type: EncryptionType, pub updated_at: i64, @@ -210,11 +210,11 @@ impl FromStr for EncryptionType { } } -impl<T> From<(&T, &AuthType)> for UserProfile +impl<T> From<(&T, &Authenticator)> for UserProfile where T: UserAuthResponse, { - fn from(params: (&T, &AuthType)) -> Self { + fn from(params: (&T, &Authenticator)) -> Self { let (value, auth_type) = params; let (icon_url, openai_key, stability_ai_key) = { value @@ -243,7 +243,7 @@ where icon_url, openai_key, workspace_id: value.latest_workspace().id.to_owned(), - auth_type: auth_type.clone(), + authenticator: auth_type.clone(), encryption_type: value.encryption_type(), stability_ai_key, updated_at: value.updated_at(), @@ -329,7 +329,7 @@ impl UpdateUserProfileParams { #[derive(Debug, Clone, Hash, Serialize_repr, Deserialize_repr, Eq, PartialEq)] #[repr(u8)] -pub enum AuthType { +pub enum Authenticator { /// It's a local server, we do fake sign in default. Local = 0, /// Currently not supported. It will be supported in the future when the @@ -339,25 +339,25 @@ pub enum AuthType { Supabase = 2, } -impl Default for AuthType { +impl Default for Authenticator { fn default() -> Self { Self::Local } } -impl AuthType { +impl Authenticator { pub fn is_local(&self) -> bool { - matches!(self, AuthType::Local) + matches!(self, Authenticator::Local) } } -impl From<i32> for AuthType { +impl From<i32> for Authenticator { fn from(value: i32) -> Self { match value { - 0 => AuthType::Local, - 1 => AuthType::AFCloud, - 2 => AuthType::Supabase, - _ => AuthType::Local, + 0 => Authenticator::Local, + 1 => Authenticator::AFCloud, + 2 => Authenticator::Supabase, + _ => Authenticator::Local, } } } diff --git a/frontend/rust-lib/flowy-user/src/anon_user_upgrade/anon_user_data.rs b/frontend/rust-lib/flowy-user/src/anon_user_upgrade/anon_user_data.rs index 152c305f8b76..e895efc41343 100644 --- a/frontend/rust-lib/flowy-user/src/anon_user_upgrade/anon_user_data.rs +++ b/frontend/rust-lib/flowy-user/src/anon_user_upgrade/anon_user_data.rs @@ -32,7 +32,6 @@ pub fn migration_anon_user_on_sign_up( .with_write_txn(|new_collab_w_txn| { let old_collab_r_txn = old_collab_db.read_txn(); let old_to_new_id_map = Arc::new(Mutex::new(OldToNewIdMap::new())); - migrate_user_awareness(old_to_new_id_map.lock().deref_mut(), old_user, new_user)?; migrate_database_with_views_object( @@ -216,9 +215,9 @@ where .map_err(|err| PersistenceError::InvalidData(err.to_string()))?; let mut folder_data = old_folder .get_folder_data() - .ok_or(PersistenceError::Internal( - anyhow!("Can't migrate the folder data").into(), - ))?; + .ok_or(PersistenceError::Internal(anyhow!( + "Can't migrate the folder data" + )))?; old_to_new_id_map .0 @@ -259,7 +258,7 @@ where let origin = CollabOrigin::Client(CollabClient::new(new_uid, "phantom")); let new_folder_collab = Collab::new_with_raw_data(origin, new_workspace_id, vec![], vec![]) - .map_err(|err| PersistenceError::Internal(Box::new(err)))?; + .map_err(|err| PersistenceError::Internal(err.into()))?; let mutex_collab = Arc::new(MutexCollab::from_collab(new_folder_collab)); let new_user_id = UserId::from(new_uid); let _ = Folder::create(new_user_id, mutex_collab.clone(), None, folder_data); diff --git a/frontend/rust-lib/flowy-user/src/anon_user_upgrade/sync_new_user.rs b/frontend/rust-lib/flowy-user/src/anon_user_upgrade/sync_new_user.rs index f3aee336101c..1880693f2ad0 100644 --- a/frontend/rust-lib/flowy-user/src/anon_user_upgrade/sync_new_user.rs +++ b/frontend/rust-lib/flowy-user/src/anon_user_upgrade/sync_new_user.rs @@ -67,6 +67,7 @@ pub async fn sync_user_data_to_cloud( tracing::error!("🔴sync {} failed: {:?}", view_id, err); } } + tokio::task::yield_now().await; Ok(()) } diff --git a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs index eb5b105fa233..4d6119eabd63 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs @@ -86,7 +86,7 @@ impl std::convert::From<UserProfile> for UserProfilePB { token: user_profile.token, icon_url: user_profile.icon_url, openai_key: user_profile.openai_key, - auth_type: user_profile.auth_type.into(), + auth_type: user_profile.authenticator.into(), encryption_sign, encryption_type: encryption_ty, workspace_id: user_profile.workspace_id, diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index 0e5819878661..c9bc5fb10d26 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -100,7 +100,7 @@ pub async fn get_user_profile_handler( // When the user is logged in with a local account, the email field is a placeholder and should // not be exposed to the client. So we set the email field to an empty string. - if user_profile.auth_type == AuthType::Local { + if user_profile.authenticator == Authenticator::Local { user_profile.email = "".to_string(); } @@ -257,7 +257,7 @@ pub async fn oauth_handler( ) -> DataResult<UserProfilePB, FlowyError> { let manager = upgrade_manager(manager)?; let params = data.into_inner(); - let auth_type: AuthType = params.auth_type.into(); + let auth_type: Authenticator = params.auth_type.into(); let user_profile = manager.sign_up(auth_type, BoxAny::new(params.map)).await?; data_result_ok(user_profile.into()) } @@ -269,7 +269,7 @@ pub async fn get_sign_in_url_handler( ) -> DataResult<SignInUrlPB, FlowyError> { let manager = upgrade_manager(manager)?; let params = data.into_inner(); - let auth_type: AuthType = params.auth_type.into(); + let auth_type: Authenticator = params.auth_type.into(); let sign_in_url = manager .generate_sign_in_url_with_email(&auth_type, ¶ms.email) .await?; @@ -469,7 +469,7 @@ pub async fn open_historical_users_handler( ) -> Result<(), FlowyError> { let user = user.into_inner(); let manager = upgrade_manager(manager)?; - let auth_type = AuthType::from(user.auth_type); + let auth_type = Authenticator::from(user.auth_type); manager .open_historical_user(user.user_id, user.device_id, auth_type) .await?; diff --git a/frontend/rust-lib/flowy-user/src/event_map.rs b/frontend/rust-lib/flowy-user/src/event_map.rs index 518004b8c522..88a8decd29a7 100644 --- a/frontend/rust-lib/flowy-user/src/event_map.rs +++ b/frontend/rust-lib/flowy-user/src/event_map.rs @@ -66,12 +66,12 @@ pub fn init(user_session: Weak<UserManager>) -> AFPlugin { #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] #[event_err = "FlowyError"] pub enum UserEvent { - /// Only use when the [AuthType] is Local or SelfHosted + /// Only use when the [Authenticator] is Local or SelfHosted /// Logging into an account using a register email and password #[event(input = "SignInPayloadPB", output = "UserProfilePB")] SignIn = 0, - /// Only use when the [AuthType] is Local or SelfHosted + /// Only use when the [Authenticator] is Local or SelfHosted /// Creating a new account #[event(input = "SignUpPayloadPB", output = "UserProfilePB")] SignUp = 1, @@ -109,7 +109,7 @@ pub enum UserEvent { OauthSignIn = 10, /// Get the OAuth callback url - /// Only use when the [AuthType] is AFCloud + /// Only use when the [Authenticator] is AFCloud #[event(input = "SignInUrlPayloadPB", output = "SignInUrlPB")] GetSignInURL = 11, @@ -145,7 +145,7 @@ pub enum UserEvent { OpenHistoricalUser = 26, /// Push a realtime event to the user. Currently, the realtime event - /// is only used when the auth type is: [AuthType::Supabase]. + /// is only used when the auth type is: [Authenticator::Supabase]. /// #[event(input = "RealtimePayloadPB")] PushRealtimeEvent = 27, @@ -201,9 +201,9 @@ pub struct SignUpContext { } pub trait UserStatusCallback: Send + Sync + 'static { - /// When the [AuthType] changed, this method will be called. Currently, the auth type + /// When the [Authenticator] changed, this method will be called. Currently, the auth type /// will be changed when the user sign in or sign up. - fn auth_type_did_changed(&self, _auth_type: AuthType) {} + fn authenticator_did_changed(&self, _authenticator: Authenticator) {} /// This will be called after the application launches if the user is already signed in. /// If the user is not signed in, this method will not be called fn did_init( @@ -244,7 +244,8 @@ pub trait UserCloudServiceProvider: Send + Sync + 'static { fn set_enable_sync(&self, uid: i64, enable_sync: bool); fn set_encrypt_secret(&self, secret: String); - fn set_auth_type(&self, auth_type: AuthType); + fn set_authenticator(&self, authenticator: Authenticator); + fn get_authenticator(&self) -> Authenticator; fn set_device_id(&self, device_id: &str); fn get_user_service(&self) -> Result<Arc<dyn UserCloudService>, FlowyError>; fn service_name(&self) -> String; @@ -266,8 +267,12 @@ where (**self).set_encrypt_secret(secret) } - fn set_auth_type(&self, auth_type: AuthType) { - (**self).set_auth_type(auth_type) + fn set_authenticator(&self, authenticator: Authenticator) { + (**self).set_authenticator(authenticator) + } + + fn get_authenticator(&self) -> Authenticator { + (**self).get_authenticator() } fn set_device_id(&self, device_id: &str) { diff --git a/frontend/rust-lib/flowy-user/src/manager.rs b/frontend/rust-lib/flowy-user/src/manager.rs index 15a6fadf6a77..2c17ae31902e 100644 --- a/frontend/rust-lib/flowy-user/src/manager.rs +++ b/frontend/rust-lib/flowy-user/src/manager.rs @@ -1,7 +1,11 @@ +use std::path::PathBuf; use std::string::ToString; use std::sync::atomic::{AtomicI64, Ordering}; use std::sync::{Arc, Weak}; +use base64::alphabet::URL_SAFE; +use base64::engine::general_purpose::PAD; +use base64::engine::GeneralPurpose; use collab_user::core::MutexUserAwareness; use serde_json::Value; use tokio::sync::{Mutex, RwLock}; @@ -29,16 +33,16 @@ use crate::migrations::workspace_and_favorite_v1::FavoriteV1AndWorkspaceArrayMig use crate::migrations::MigrationUser; use crate::services::cloud_config::get_cloud_config; use crate::services::collab_interact::{CollabInteract, DefaultCollabInteract}; -use crate::services::database::UserDB; +use crate::services::database::{UserDB, UserDBPath}; use crate::services::entities::{ResumableSignUp, Session}; use crate::services::user_awareness::UserAwarenessDataSource; use crate::services::user_sql::{UserTable, UserTableChangeset}; use crate::services::user_workspace::save_user_workspaces; use crate::{errors::FlowyError, notification::*}; +pub const URL_SAFE_ENGINE: GeneralPurpose = GeneralPurpose::new(&URL_SAFE, PAD); pub struct UserSessionConfig { root_dir: String, - /// Used as the key of `Session` when saving session information to KV. session_cache_key: String, } @@ -57,6 +61,7 @@ impl UserSessionConfig { pub struct UserManager { database: Arc<UserDB>, + user_paths: UserPaths, session_config: UserSessionConfig, pub(crate) cloud_services: Arc<dyn UserCloudServiceProvider>, pub(crate) store_preferences: Arc<StorePreferences>, @@ -76,13 +81,17 @@ impl UserManager { store_preferences: Arc<StorePreferences>, collab_builder: Weak<AppFlowyCollabBuilder>, ) -> Arc<Self> { - let database = Arc::new(UserDB::new(&session_config.root_dir)); + let user_paths = UserPaths { + root: session_config.root_dir.clone(), + }; + let database = Arc::new(UserDB::new(user_paths.clone())); let user_status_callback: RwLock<Arc<dyn UserStatusCallback>> = RwLock::new(Arc::new(DefaultUserStatusCallback)); let refresh_user_profile_since = AtomicI64::new(0); let user_manager = Arc::new(Self { database, + user_paths, session_config, cloud_services, store_preferences, @@ -249,9 +258,9 @@ impl UserManager { pub async fn sign_in( &self, params: BoxAny, - auth_type: AuthType, + authenticator: Authenticator, ) -> Result<UserProfile, FlowyError> { - self.update_auth_type(&auth_type).await; + self.update_authenticator(&authenticator).await; let response: AuthResponse = self .cloud_services .get_user_service()? @@ -261,8 +270,10 @@ impl UserManager { self.set_collab_config(&session); let latest_workspace = response.latest_workspace.clone(); - let user_profile = UserProfile::from((&response, &auth_type)); - self.save_auth_data(&response, &auth_type, &session).await?; + let user_profile = UserProfile::from((&response, &authenticator)); + self + .save_auth_data(&response, &authenticator, &session) + .await?; let _ = self .initialize_user_awareness(&session, UserAwarenessDataSource::Remote) .await; @@ -284,13 +295,13 @@ impl UserManager { Ok(user_profile) } - pub(crate) async fn update_auth_type(&self, auth_type: &AuthType) { + pub(crate) async fn update_authenticator(&self, authenticator: &Authenticator) { self .user_status_callback .read() .await - .auth_type_did_changed(auth_type.clone()); - self.cloud_services.set_auth_type(auth_type.clone()); + .authenticator_did_changed(authenticator.clone()); + self.cloud_services.set_authenticator(authenticator.clone()); } /// Manages the user sign-up process, potentially migrating data if necessary. @@ -303,15 +314,15 @@ impl UserManager { #[tracing::instrument(level = "info", skip(self, params))] pub async fn sign_up( &self, - auth_type: AuthType, + authenticator: Authenticator, params: BoxAny, ) -> Result<UserProfile, FlowyError> { - self.update_auth_type(&auth_type).await; + self.update_authenticator(&authenticator).await; - let migration_user = self.get_migration_user(&auth_type).await; + let migration_user = self.get_migration_user(&authenticator).await; let auth_service = self.cloud_services.get_user_service()?; let response: AuthResponse = auth_service.sign_up(params).await?; - let user_profile = UserProfile::from((&response, &auth_type)); + let user_profile = UserProfile::from((&response, &authenticator)); if user_profile.encryption_type.is_need_encrypt_secret() { self .resumable_sign_up @@ -321,11 +332,11 @@ impl UserManager { user_profile: user_profile.clone(), migration_user, response, - auth_type, + authenticator, }); } else { self - .continue_sign_up(&user_profile, migration_user, response, &auth_type) + .continue_sign_up(&user_profile, migration_user, response, &authenticator) .await?; } Ok(user_profile) @@ -337,7 +348,7 @@ impl UserManager { user_profile, migration_user, response, - auth_type, + authenticator, } = self .resumable_sign_up .lock() @@ -348,7 +359,7 @@ impl UserManager { "No resumable sign up data", ))?; self - .continue_sign_up(&user_profile, migration_user, response, &auth_type) + .continue_sign_up(&user_profile, migration_user, response, &authenticator) .await?; Ok(()) } @@ -359,7 +370,7 @@ impl UserManager { user_profile: &UserProfile, migration_user: Option<MigrationUser>, response: AuthResponse, - auth_type: &AuthType, + authenticator: &Authenticator, ) -> FlowyResult<()> { let new_session = Session::from(&response); self.set_collab_config(&new_session); @@ -383,7 +394,7 @@ impl UserManager { new_user.user_profile.uid ); self - .migrate_anon_user_to_cloud(&old_user, &new_user) + .migrate_anon_user_data_to_cloud(&old_user, &new_user) .await?; let _ = self.database.close(old_user.session.user_id); } @@ -393,7 +404,7 @@ impl UserManager { .await; self - .save_auth_data(&response, auth_type, &new_session) + .save_auth_data(&response, authenticator, &new_session) .await?; self @@ -495,14 +506,14 @@ impl UserManager { // If the authentication type has changed, it indicates that the user has signed in // using a different release package but is sharing the same data folder. // In such cases, notify the frontend to log out. - if old_user_profile.auth_type != AuthType::Local - && new_user_profile.auth_type != old_user_profile.auth_type + if old_user_profile.authenticator != Authenticator::Local + && new_user_profile.authenticator != old_user_profile.authenticator { event!( tracing::Level::INFO, - "User login with different cloud: {:?} -> {:?}", - old_user_profile.auth_type, - new_user_profile.auth_type + "User login with different authenticator: {:?} -> {:?}", + old_user_profile.authenticator, + new_user_profile.authenticator ); send_auth_state_notification(AuthStateChangedPB { @@ -541,8 +552,9 @@ impl UserManager { } } + #[instrument(level = "info", skip_all)] pub fn user_dir(&self, uid: i64) -> String { - format!("{}/{}", self.session_config.root_dir, uid) + self.user_paths.user_dir(uid) } pub fn user_setting(&self) -> Result<UserSettingPB, FlowyError> { @@ -646,10 +658,10 @@ impl UserManager { pub(crate) async fn generate_sign_in_url_with_email( &self, - auth_type: &AuthType, + authenticator: &Authenticator, email: &str, ) -> Result<String, FlowyError> { - self.update_auth_type(auth_type).await; + self.update_authenticator(authenticator).await; let auth_service = self.cloud_services.get_user_service()?; let url = auth_service @@ -663,7 +675,7 @@ impl UserManager { &self, oauth_provider: &str, ) -> Result<String, FlowyError> { - self.update_auth_type(&AuthType::AFCloud).await; + self.update_authenticator(&Authenticator::AFCloud).await; let auth_service = self.cloud_services.get_user_service()?; let url = auth_service .generate_oauth_url_with_provider(oauth_provider) @@ -675,24 +687,24 @@ impl UserManager { async fn save_auth_data( &self, response: &impl UserAuthResponse, - auth_type: &AuthType, + authenticator: &Authenticator, session: &Session, ) -> Result<(), FlowyError> { - let user_profile = UserProfile::from((response, auth_type)); + let user_profile = UserProfile::from((response, authenticator)); let uid = user_profile.uid; event!(tracing::Level::DEBUG, "Save new history user: {:?}", uid); self.add_historical_user( uid, response.device_id(), response.user_name().to_string(), - auth_type, + authenticator, self.user_dir(uid), ); event!(tracing::Level::DEBUG, "Save new history user workspace"); save_user_workspaces(uid, self.db_pool(uid)?, response.user_workspaces())?; event!(tracing::Level::INFO, "Save new user profile to disk"); self - .save_user(uid, (user_profile, auth_type.clone()).into()) + .save_user(uid, (user_profile, authenticator.clone()).into()) .await?; self.set_session(Some(session.clone()))?; Ok(()) @@ -725,7 +737,7 @@ impl UserManager { Ok(()) } - async fn migrate_anon_user_to_cloud( + async fn migrate_anon_user_data_to_cloud( &self, old_user: &MigrationUser, new_user: &MigrationUser, @@ -797,3 +809,26 @@ fn save_user_token(uid: i64, pool: Arc<ConnectionPool>, token: String) -> FlowyR let changeset = UserTableChangeset::new(params); upsert_user_profile_change(uid, pool, changeset) } + +#[derive(Clone)] +struct UserPaths { + root: String, +} + +impl UserPaths { + fn user_dir(&self, uid: i64) -> String { + format!("{}/{}", self.root, uid) + } +} + +impl UserDBPath for UserPaths { + fn user_db_path(&self, uid: i64) -> PathBuf { + PathBuf::from(self.user_dir(uid)) + } + + fn collab_db_path(&self, uid: i64) -> PathBuf { + let mut path = PathBuf::from(self.user_dir(uid)); + path.push("collab_db"); + path + } +} diff --git a/frontend/rust-lib/flowy-user/src/services/database.rs b/frontend/rust-lib/flowy-user/src/services/database.rs index bb2f4cf7d44b..032433cd4cd6 100644 --- a/frontend/rust-lib/flowy-user/src/services/database.rs +++ b/frontend/rust-lib/flowy-user/src/services/database.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::{collections::HashMap, sync::Arc, time::Duration}; use lazy_static::lazy_static; @@ -18,14 +18,19 @@ use flowy_user_deps::entities::{UserProfile, UserWorkspace}; use crate::services::user_sql::UserTable; use crate::services::user_workspace_sql::UserWorkspaceTable; +pub trait UserDBPath: Send + Sync + 'static { + fn user_db_path(&self, uid: i64) -> PathBuf; + fn collab_db_path(&self, uid: i64) -> PathBuf; +} + pub struct UserDB { - root: String, + paths: Box<dyn UserDBPath>, } impl UserDB { - pub fn new(db_dir: &str) -> Self { + pub fn new(paths: impl UserDBPath) -> Self { Self { - root: db_dir.to_owned(), + paths: Box::new(paths), } } @@ -51,25 +56,27 @@ impl UserDB { } pub(crate) fn get_pool(&self, user_id: i64) -> Result<Arc<ConnectionPool>, FlowyError> { - let pool = open_user_db(&self.root, user_id)?; + let pool = open_user_db(self.paths.user_db_path(user_id), user_id)?; Ok(pool) } pub(crate) fn get_collab_db(&self, user_id: i64) -> Result<Arc<RocksCollabDB>, FlowyError> { - let collab_db = open_collab_db(&self.root, user_id)?; + let collab_db = open_collab_db(self.paths.collab_db_path(user_id), user_id)?; Ok(collab_db) } } -pub fn open_user_db(root: &str, user_id: i64) -> Result<Arc<ConnectionPool>, FlowyError> { +pub fn open_user_db( + db_path: impl AsRef<Path>, + user_id: i64, +) -> Result<Arc<ConnectionPool>, FlowyError> { if let Some(database) = DB_MAP.read().get(&user_id) { return Ok(database.get_pool()); } let mut write_guard = DB_MAP.write(); - let dir = user_db_path_from_uid(root, user_id); - tracing::debug!("open sqlite db {} at path: {:?}", user_id, dir); - let db = flowy_sqlite::init(&dir) + tracing::debug!("open sqlite db {} at path: {:?}", user_id, db_path.as_ref()); + let db = flowy_sqlite::init(&db_path) .map_err(|e| FlowyError::internal().with_context(format!("open user db failed, {:?}", e)))?; let pool = db.get_pool(); write_guard.insert(user_id.to_owned(), db); @@ -98,24 +105,16 @@ pub fn get_user_workspace( Ok(Some(UserWorkspace::from(row))) } -pub fn user_db_path_from_uid(root: &str, uid: i64) -> PathBuf { - let mut dir = PathBuf::new(); - dir.push(root); - dir.push(uid.to_string()); - dir -} - /// Open a collab db for the user. If the db is already opened, return the opened db. /// -pub fn open_collab_db(root: &str, uid: i64) -> Result<Arc<RocksCollabDB>, FlowyError> { +fn open_collab_db(db_path: impl AsRef<Path>, uid: i64) -> Result<Arc<RocksCollabDB>, FlowyError> { if let Some(collab_db) = COLLAB_DB_MAP.read().get(&uid) { return Ok(collab_db.clone()); } let mut write_guard = COLLAB_DB_MAP.write(); - let dir = collab_db_path_from_uid(root, uid); - tracing::trace!("open collab db {} at path: {:?}", uid, dir); - let db = match RocksCollabDB::open(dir) { + tracing::trace!("open collab db {} at path: {:?}", uid, db_path.as_ref()); + let db = match RocksCollabDB::open(db_path) { Ok(db) => Ok(db), Err(err) => { tracing::error!("open collab db failed, {:?}", err); @@ -129,14 +128,6 @@ pub fn open_collab_db(root: &str, uid: i64) -> Result<Arc<RocksCollabDB>, FlowyE Ok(db) } -pub fn collab_db_path_from_uid(root: &str, uid: i64) -> PathBuf { - let mut dir = PathBuf::new(); - dir.push(root); - dir.push(uid.to_string()); - dir.push("collab_db"); - dir -} - lazy_static! { static ref DB_MAP: RwLock<HashMap<i64, Database>> = RwLock::new(HashMap::new()); static ref COLLAB_DB_MAP: RwLock<HashMap<i64, Arc<RocksCollabDB>>> = RwLock::new(HashMap::new()); diff --git a/frontend/rust-lib/flowy-user/src/services/entities.rs b/frontend/rust-lib/flowy-user/src/services/entities.rs index e1396a723a5a..37ee2a244463 100644 --- a/frontend/rust-lib/flowy-user/src/services/entities.rs +++ b/frontend/rust-lib/flowy-user/src/services/entities.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use flowy_user_deps::entities::{AuthResponse, UserProfile, UserWorkspace}; -use flowy_user_deps::entities::{AuthType, UserAuthResponse}; +use flowy_user_deps::entities::{Authenticator, UserAuthResponse}; use crate::entities::AuthTypePB; use crate::migrations::MigrationUser; @@ -157,22 +157,22 @@ mod tests { } } -impl From<AuthTypePB> for AuthType { +impl From<AuthTypePB> for Authenticator { fn from(pb: AuthTypePB) -> Self { match pb { - AuthTypePB::Supabase => AuthType::Supabase, - AuthTypePB::Local => AuthType::Local, - AuthTypePB::AFCloud => AuthType::AFCloud, + AuthTypePB::Supabase => Authenticator::Supabase, + AuthTypePB::Local => Authenticator::Local, + AuthTypePB::AFCloud => Authenticator::AFCloud, } } } -impl From<AuthType> for AuthTypePB { - fn from(auth_type: AuthType) -> Self { +impl From<Authenticator> for AuthTypePB { + fn from(auth_type: Authenticator) -> Self { match auth_type { - AuthType::Supabase => AuthTypePB::Supabase, - AuthType::Local => AuthTypePB::Local, - AuthType::AFCloud => AuthTypePB::AFCloud, + Authenticator::Supabase => AuthTypePB::Supabase, + Authenticator::Local => AuthTypePB::Local, + Authenticator::AFCloud => AuthTypePB::AFCloud, } } } @@ -195,18 +195,18 @@ pub struct HistoricalUser { #[serde(default = "flowy_user_deps::DEFAULT_USER_NAME")] pub user_name: String, #[serde(default = "DEFAULT_AUTH_TYPE")] - pub auth_type: AuthType, + pub auth_type: Authenticator, pub sign_in_timestamp: i64, pub storage_path: String, #[serde(default)] pub device_id: String, } -const DEFAULT_AUTH_TYPE: fn() -> AuthType = || AuthType::Local; +const DEFAULT_AUTH_TYPE: fn() -> Authenticator = || Authenticator::Local; #[derive(Clone)] pub(crate) struct ResumableSignUp { pub user_profile: UserProfile, pub response: AuthResponse, - pub auth_type: AuthType, + pub authenticator: Authenticator, pub migration_user: Option<MigrationUser>, } diff --git a/frontend/rust-lib/flowy-user/src/services/historical_user.rs b/frontend/rust-lib/flowy-user/src/services/historical_user.rs index 4e40ae546799..cd62f62e2a00 100644 --- a/frontend/rust-lib/flowy-user/src/services/historical_user.rs +++ b/frontend/rust-lib/flowy-user/src/services/historical_user.rs @@ -3,7 +3,7 @@ use diesel::RunQueryDsl; use flowy_error::FlowyResult; use flowy_sqlite::schema::user_workspace_table; use flowy_sqlite::{query_dsl::*, ExpressionMethods}; -use flowy_user_deps::entities::{AuthType, UserWorkspace}; +use flowy_user_deps::entities::{Authenticator, UserWorkspace}; use lib_infra::util::timestamp; use crate::manager::UserManager; @@ -13,12 +13,12 @@ use crate::services::user_workspace_sql::UserWorkspaceTable; const HISTORICAL_USER: &str = "af_historical_users"; impl UserManager { - pub async fn get_migration_user(&self, auth_type: &AuthType) -> Option<MigrationUser> { + pub async fn get_migration_user(&self, auth_type: &Authenticator) -> Option<MigrationUser> { // Only migrate the data if the user is login in as a guest and sign up as a new user if the current // auth type is not [AuthType::Local]. let session = self.get_session().ok()?; let user_profile = self.get_user_profile(session.user_id).await.ok()?; - if user_profile.auth_type == AuthType::Local && !auth_type.is_local() { + if user_profile.authenticator == Authenticator::Local && !auth_type.is_local() { Some(MigrationUser { user_profile, session, @@ -44,7 +44,7 @@ impl UserManager { uid: i64, device_id: &str, user_name: String, - auth_type: &AuthType, + auth_type: &Authenticator, storage_path: String, ) { let mut logger_users = self @@ -86,10 +86,10 @@ impl UserManager { &self, uid: i64, device_id: String, - auth_type: AuthType, + auth_type: Authenticator, ) -> FlowyResult<()> { debug_assert!(auth_type.is_local()); - self.update_auth_type(&auth_type).await; + self.update_authenticator(&auth_type).await; let conn = self.db_connection(uid)?; let row = user_workspace_table::dsl::user_workspace_table .filter(user_workspace_table::uid.eq(uid)) diff --git a/frontend/rust-lib/flowy-user/src/services/user_sql.rs b/frontend/rust-lib/flowy-user/src/services/user_sql.rs index b60343443058..74240ef3b3c5 100644 --- a/frontend/rust-lib/flowy-user/src/services/user_sql.rs +++ b/frontend/rust-lib/flowy-user/src/services/user_sql.rs @@ -29,8 +29,8 @@ impl UserTable { } } -impl From<(UserProfile, AuthType)> for UserTable { - fn from(value: (UserProfile, AuthType)) -> Self { +impl From<(UserProfile, Authenticator)> for UserTable { + fn from(value: (UserProfile, Authenticator)) -> Self { let (user_profile, auth_type) = value; let encryption_type = serde_json::to_string(&user_profile.encryption_type).unwrap_or_default(); UserTable { @@ -59,7 +59,7 @@ impl From<UserTable> for UserProfile { icon_url: table.icon_url, openai_key: table.openai_key, workspace_id: table.workspace, - auth_type: AuthType::from(table.auth_type), + authenticator: Authenticator::from(table.auth_type), encryption_type: EncryptionType::from_str(&table.encryption_type).unwrap_or_default(), stability_ai_key: table.stability_ai_key, updated_at: table.updated_at, diff --git a/frontend/rust-lib/lib-log/src/lib.rs b/frontend/rust-lib/lib-log/src/lib.rs index 323fbb056c6a..ac714da02a2e 100644 --- a/frontend/rust-lib/lib-log/src/lib.rs +++ b/frontend/rust-lib/lib-log/src/lib.rs @@ -25,13 +25,10 @@ pub struct Builder { impl Builder { pub fn new(name: &str, directory: &str) -> Self { - // let directory = directory.as_ref().to_str().unwrap().to_owned(); - let local_file_name = format!("{}.log", name); - Builder { name: name.to_owned(), env_filter: "Info".to_owned(), - file_appender: tracing_appender::rolling::daily(directory, local_file_name), + file_appender: tracing_appender::rolling::daily(directory, format!("{}", name)), } } diff --git a/frontend/scripts/makefile/desktop.toml b/frontend/scripts/makefile/desktop.toml index d45e4da2d028..ad408fbeb49f 100644 --- a/frontend/scripts/makefile/desktop.toml +++ b/frontend/scripts/makefile/desktop.toml @@ -66,7 +66,7 @@ script = [ cd rust-lib/ rustup show echo RUSTFLAGS="-C target-cpu=native -C link-arg=-mmacosx-version-min=11.0" cargo build --package=dart-ffi --target ${RUST_COMPILE_TARGET} --features "${FLUTTER_DESKTOP_FEATURES}" - RUSTFLAGS="-C target-cpu=native -C link-arg=-mmacosx-version-min=11.0" cargo build --package=dart-ffi --target ${RUST_COMPILE_TARGET} --features "${FLUTTER_DESKTOP_FEATURES}" + RUSTFLAGS="--cfg tokio_unstable" cargo build --package=dart-ffi --target ${RUST_COMPILE_TARGET} --features "${FLUTTER_DESKTOP_FEATURES}" cd ../ """, ] From 50e612511d7ad171ea9b65b850faabdc87f1716a Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Sun, 12 Nov 2023 21:18:27 +0800 Subject: [PATCH 43/56] feat: using workspace crate deps (#3924) * chore: workspace deps * chore: use workspace deps --- frontend/appflowy_tauri/src-tauri/Cargo.lock | 127 +++++++------- frontend/appflowy_tauri/src-tauri/Cargo.toml | 27 ++- frontend/rust-lib/Cargo.lock | 76 ++++----- frontend/rust-lib/Cargo.toml | 17 +- frontend/rust-lib/collab-integrate/Cargo.toml | 16 +- frontend/rust-lib/dart-ffi/Cargo.toml | 15 +- frontend/rust-lib/dart-ffi/src/lib.rs | 3 +- .../rust-lib/event-integration/Cargo.toml | 20 +-- frontend/rust-lib/flowy-ai/Cargo.toml | 8 +- frontend/rust-lib/flowy-config/Cargo.toml | 4 +- frontend/rust-lib/flowy-core/Cargo.toml | 24 +-- .../rust-lib/flowy-database-deps/Cargo.toml | 2 +- frontend/rust-lib/flowy-database2/Cargo.toml | 25 ++- frontend/rust-lib/flowy-date/Cargo.toml | 8 +- .../rust-lib/flowy-document-deps/Cargo.toml | 2 +- frontend/rust-lib/flowy-document2/Cargo.toml | 22 +-- frontend/rust-lib/flowy-encrypt/Cargo.toml | 2 +- frontend/rust-lib/flowy-error/Cargo.toml | 16 +- .../rust-lib/flowy-folder-deps/Cargo.toml | 4 +- frontend/rust-lib/flowy-folder2/Cargo.toml | 16 +- .../rust-lib/flowy-notification/Cargo.toml | 8 +- .../rust-lib/flowy-server-config/Cargo.toml | 2 +- frontend/rust-lib/flowy-server/Cargo.toml | 26 +-- frontend/rust-lib/flowy-sqlite/Cargo.toml | 12 +- frontend/rust-lib/flowy-storage/Cargo.toml | 8 +- frontend/rust-lib/flowy-task/Cargo.toml | 8 +- frontend/rust-lib/flowy-user-deps/Cargo.toml | 14 +- frontend/rust-lib/flowy-user/Cargo.toml | 26 ++- frontend/rust-lib/lib-dispatch/Cargo.toml | 16 +- frontend/rust-lib/lib-dispatch/src/data.rs | 2 +- .../rust-lib/lib-dispatch/src/module/data.rs | 2 +- .../lib-dispatch/src/request/request.rs | 2 +- frontend/rust-lib/lib-log/Cargo.toml | 7 +- shared-lib/Cargo.lock | 158 ++++++++++++------ shared-lib/Cargo.toml | 10 ++ shared-lib/flowy-codegen/Cargo.toml | 3 +- .../src/dart_event/dart_event.rs | 18 +- shared-lib/flowy-derive/Cargo.toml | 5 +- shared-lib/lib-infra/Cargo.toml | 8 +- shared-lib/lib-ot/Cargo.toml | 5 +- 40 files changed, 436 insertions(+), 338 deletions(-) diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 02448abe3a73..c008877ee452 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -138,7 +138,7 @@ checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0873b582d61d787e20c6a7193fdcbad2ea90de0e#0873b582d61d787e20c6a7193fdcbad2ea90de0e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2e14dcf129cab5e1c980655971cfb5ff321b0844#2e14dcf129cab5e1c980655971cfb5ff321b0844" dependencies = [ "anyhow", "reqwest", @@ -234,9 +234,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.73" +version = "0.1.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" +checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", @@ -768,7 +768,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0873b582d61d787e20c6a7193fdcbad2ea90de0e#0873b582d61d787e20c6a7193fdcbad2ea90de0e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2e14dcf129cab5e1c980655971cfb5ff321b0844#2e14dcf129cab5e1c980655971cfb5ff321b0844" dependencies = [ "anyhow", "app-error", @@ -1303,7 +1303,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa 1.0.6", - "phf 0.8.0", + "phf 0.11.2", "smallvec", ] @@ -1449,7 +1449,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0873b582d61d787e20c6a7193fdcbad2ea90de0e#0873b582d61d787e20c6a7193fdcbad2ea90de0e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2e14dcf129cab5e1c980655971cfb5ff321b0844#2e14dcf129cab5e1c980655971cfb5ff321b0844" dependencies = [ "anyhow", "app-error", @@ -2326,7 +2326,6 @@ dependencies = [ "lazy_static", "lib-dispatch", "lib-infra", - "log", "once_cell", "parking_lot", "protobuf", @@ -2407,9 +2406,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" dependencies = [ "futures-channel", "futures-core", @@ -2422,9 +2421,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" dependencies = [ "futures-core", "futures-sink", @@ -2432,15 +2431,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" [[package]] name = "futures-executor" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" dependencies = [ "futures-core", "futures-task", @@ -2460,15 +2459,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" [[package]] name = "futures-macro" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", @@ -2477,15 +2476,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" [[package]] name = "futures-timer" @@ -2495,9 +2494,9 @@ checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" dependencies = [ "futures-channel", "futures-core", @@ -2807,7 +2806,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0873b582d61d787e20c6a7193fdcbad2ea90de0e#0873b582d61d787e20c6a7193fdcbad2ea90de0e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2e14dcf129cab5e1c980655971cfb5ff321b0844#2e14dcf129cab5e1c980655971cfb5ff321b0844" dependencies = [ "anyhow", "futures-util", @@ -2823,7 +2822,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0873b582d61d787e20c6a7193fdcbad2ea90de0e#0873b582d61d787e20c6a7193fdcbad2ea90de0e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2e14dcf129cab5e1c980655971cfb5ff321b0844#2e14dcf129cab5e1c980655971cfb5ff321b0844" dependencies = [ "anyhow", "app-error", @@ -3259,7 +3258,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0873b582d61d787e20c6a7193fdcbad2ea90de0e#0873b582d61d787e20c6a7193fdcbad2ea90de0e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2e14dcf129cab5e1c980655971cfb5ff321b0844#2e14dcf129cab5e1c980655971cfb5ff321b0844" dependencies = [ "anyhow", "reqwest", @@ -3468,8 +3467,8 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "log", "nanoid", + "parking_lot", "pin-project", "protobuf", "serde", @@ -3502,7 +3501,6 @@ version = "0.1.0" dependencies = [ "chrono", "lazy_static", - "log", "serde", "serde_json", "tracing", @@ -3525,9 +3523,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.147" +version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] name = "libloading" @@ -3815,9 +3813,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" dependencies = [ "libc", "wasi 0.11.0+wasi-snapshot-preview1", @@ -4341,6 +4339,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ + "phf_macros 0.11.2", "phf_shared 0.11.2", ] @@ -4432,6 +4431,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn 2.0.29", +] + [[package]] name = "phf_shared" version = "0.8.0" @@ -4482,9 +4494,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -4673,7 +4685,7 @@ checksum = "8bdf592881d821b83d471f8af290226c8d51402259e9bb5be7f9f8bdebbb11ac" dependencies = [ "bytes", "heck 0.4.1", - "itertools 0.10.5", + "itertools 0.11.0", "log", "multimap", "once_cell", @@ -4694,7 +4706,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "265baba7fabd416cf5078179f7d2cbeca4ce7a9041111900675ea7c4cb8a4c32" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.11.0", "proc-macro2", "quote", "syn 2.0.29", @@ -4989,7 +5001,7 @@ dependencies = [ [[package]] name = "realtime-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0873b582d61d787e20c6a7193fdcbad2ea90de0e#0873b582d61d787e20c6a7193fdcbad2ea90de0e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2e14dcf129cab5e1c980655971cfb5ff321b0844#2e14dcf129cab5e1c980655971cfb5ff321b0844" dependencies = [ "anyhow", "bincode", @@ -5733,7 +5745,7 @@ dependencies = [ [[package]] name = "shared_entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0873b582d61d787e20c6a7193fdcbad2ea90de0e#0873b582d61d787e20c6a7193fdcbad2ea90de0e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2e14dcf129cab5e1c980655971cfb5ff321b0844#2e14dcf129cab5e1c980655971cfb5ff321b0844" dependencies = [ "anyhow", "app-error", @@ -5864,9 +5876,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.3" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", "windows-sys 0.48.0", @@ -6560,11 +6572,11 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.28.2" +version = "1.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2" +checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" dependencies = [ - "autocfg", + "backtrace", "bytes", "libc", "mio", @@ -6572,16 +6584,16 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.4.9", + "socket2 0.5.5", "tokio-macros", "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", @@ -6617,7 +6629,7 @@ dependencies = [ "pin-project-lite", "postgres-protocol", "postgres-types", - "socket2 0.5.3", + "socket2 0.5.5", "tokio", "tokio-util", ] @@ -6734,11 +6746,10 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", "log", "pin-project-lite", "tracing-attributes", @@ -6758,9 +6769,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", @@ -6787,9 +6798,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", "valuable", @@ -7027,9 +7038,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "uuid" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" +checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" dependencies = [ "getrandom 0.2.10", "serde", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index 98f446e15ec3..397511c258bf 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -13,13 +13,30 @@ rust-version = "1.57" [build-dependencies] tauri-build = { version = "1.2", features = [] } +[workspace.dependencies] +anyhow = "1.0.75" +tracing = "0.1.40" +bytes = "1.5.0" +serde = "1.0.108" +serde_json = "1.0.108" +protobuf = { version = "2.28.0" } +diesel = { version = "1.4.8", features = ["sqlite", "chrono"] } +uuid = { version = "1.5.0", features = ["serde", "v4"] } +serde_repr = "0.1" +parking_lot = "0.12" +futures = "0.3.29" +tokio = "1.34.0" +tokio-stream = "0.1.14" +async-trait = "0.1.74" +chrono = { version = "0.4.31", default-features = false, features = ["clock"] } + [dependencies] -serde_json = "1.0" -serde = { version = "1.0", features = ["derive"] } +serde_json.workspace = true +serde.workspace = true tauri = { version = "1.2", features = ["fs-all", "shell-open"] } tauri-utils = "1.2" -bytes = { version = "1.5" } -tracing = { version = "0.1", features = ["log"] } +bytes.workspace = true +tracing.workspace = true lib-dispatch = { path = "../../rust-lib/lib-dispatch", features = ["use_serde"] } flowy-core = { path = "../../rust-lib/flowy-core", features = ["rev-sqlite", "ts"] } flowy-notification = { path = "../../rust-lib/flowy-notification", features = ["ts"] } @@ -38,7 +55,7 @@ custom-protocol = ["tauri/custom-protocol"] # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "0873b582d61d787e20c6a7193fdcbad2ea90de0e" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "2e14dcf129cab5e1c980655971cfb5ff321b0844" } # Please use the following script to update collab. # Working directory: frontend # diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index a8311ed3a2f9..b3ae83609533 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -124,7 +124,7 @@ checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0873b582d61d787e20c6a7193fdcbad2ea90de0e#0873b582d61d787e20c6a7193fdcbad2ea90de0e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2e14dcf129cab5e1c980655971cfb5ff321b0844#2e14dcf129cab5e1c980655971cfb5ff321b0844" dependencies = [ "anyhow", "reqwest", @@ -214,9 +214,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.73" +version = "0.1.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" +checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", @@ -467,7 +467,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" dependencies = [ "borsh-derive", - "hashbrown 0.12.3", + "hashbrown 0.13.2", ] [[package]] @@ -666,7 +666,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0873b582d61d787e20c6a7193fdcbad2ea90de0e#0873b582d61d787e20c6a7193fdcbad2ea90de0e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2e14dcf129cab5e1c980655971cfb5ff321b0844#2e14dcf129cab5e1c980655971cfb5ff321b0844" dependencies = [ "anyhow", "app-error", @@ -1246,7 +1246,6 @@ dependencies = [ "flowy-server-config", "lazy_static", "lib-dispatch", - "log", "parking_lot", "protobuf", "serde", @@ -1277,7 +1276,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0873b582d61d787e20c6a7193fdcbad2ea90de0e#0873b582d61d787e20c6a7193fdcbad2ea90de0e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2e14dcf129cab5e1c980655971cfb5ff321b0844#2e14dcf129cab5e1c980655971cfb5ff321b0844" dependencies = [ "anyhow", "app-error", @@ -2160,7 +2159,6 @@ dependencies = [ "lazy_static", "lib-dispatch", "lib-infra", - "log", "nanoid", "once_cell", "parking_lot", @@ -2252,9 +2250,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" dependencies = [ "futures-channel", "futures-core", @@ -2283,9 +2281,9 @@ checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" [[package]] name = "futures-executor" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" dependencies = [ "futures-core", "futures-task", @@ -2467,7 +2465,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0873b582d61d787e20c6a7193fdcbad2ea90de0e#0873b582d61d787e20c6a7193fdcbad2ea90de0e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2e14dcf129cab5e1c980655971cfb5ff321b0844#2e14dcf129cab5e1c980655971cfb5ff321b0844" dependencies = [ "anyhow", "futures-util", @@ -2483,7 +2481,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0873b582d61d787e20c6a7193fdcbad2ea90de0e#0873b582d61d787e20c6a7193fdcbad2ea90de0e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2e14dcf129cab5e1c980655971cfb5ff321b0844#2e14dcf129cab5e1c980655971cfb5ff321b0844" dependencies = [ "anyhow", "app-error", @@ -2844,7 +2842,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0873b582d61d787e20c6a7193fdcbad2ea90de0e#0873b582d61d787e20c6a7193fdcbad2ea90de0e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2e14dcf129cab5e1c980655971cfb5ff321b0844#2e14dcf129cab5e1c980655971cfb5ff321b0844" dependencies = [ "anyhow", "reqwest", @@ -2969,8 +2967,8 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "log", "nanoid", + "parking_lot", "pin-project", "protobuf", "serde", @@ -3003,7 +3001,6 @@ version = "0.1.0" dependencies = [ "chrono", "lazy_static", - "log", "serde", "serde_json", "tracing", @@ -3026,9 +3023,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.147" +version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] name = "libloading" @@ -3265,9 +3262,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" dependencies = [ "libc", "wasi 0.11.0+wasi-snapshot-preview1", @@ -4322,7 +4319,7 @@ dependencies = [ [[package]] name = "realtime-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0873b582d61d787e20c6a7193fdcbad2ea90de0e#0873b582d61d787e20c6a7193fdcbad2ea90de0e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2e14dcf129cab5e1c980655971cfb5ff321b0844#2e14dcf129cab5e1c980655971cfb5ff321b0844" dependencies = [ "anyhow", "bincode", @@ -4965,7 +4962,7 @@ dependencies = [ [[package]] name = "shared_entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0873b582d61d787e20c6a7193fdcbad2ea90de0e#0873b582d61d787e20c6a7193fdcbad2ea90de0e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2e14dcf129cab5e1c980655971cfb5ff321b0844#2e14dcf129cab5e1c980655971cfb5ff321b0844" dependencies = [ "anyhow", "app-error", @@ -5090,9 +5087,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.3" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", "windows-sys", @@ -5475,9 +5472,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.32.0" +version = "1.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" +checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" dependencies = [ "backtrace", "bytes", @@ -5487,7 +5484,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.3", + "socket2 0.5.5", "tokio-macros", "tracing", "windows-sys", @@ -5505,9 +5502,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", @@ -5544,7 +5541,7 @@ dependencies = [ "postgres-protocol", "postgres-types", "rand 0.8.5", - "socket2 0.5.3", + "socket2 0.5.5", "tokio", "tokio-util", "whoami", @@ -5681,11 +5678,10 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", "log", "pin-project-lite", "tracing-attributes", @@ -5705,9 +5701,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", @@ -5734,9 +5730,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", "valuable", @@ -5961,9 +5957,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "uuid" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" +checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" dependencies = [ "getrandom 0.2.10", "serde", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 01876a160a2f..d20c8bac23db 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -53,6 +53,21 @@ flowy-storage = { workspace = true, path = "flowy-storage" } collab-integrate = { workspace = true, path = "collab-integrate" } flowy-ai = { workspace = true, path = "flowy-ai" } flowy-date = { workspace = true, path = "flowy-date" } +anyhow = "1.0.75" +tracing = "0.1.40" +bytes = "1.5.0" +serde_json = "1.0.108" +serde = "1.0.108" +protobuf = { version = "2.28.0" } +diesel = { version = "1.4.8", features = ["sqlite", "chrono"] } +uuid = { version = "1.5.0", features = ["serde", "v4"] } +serde_repr = "0.1" +parking_lot = "0.12" +futures = "0.3.29" +tokio = "1.34.0" +tokio-stream = "0.1.14" +async-trait = "0.1.74" +chrono = { version = "0.4.31", default-features = false, features = ["clock"] } [profile.dev] opt-level = 0 @@ -83,7 +98,7 @@ incremental = false # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "0873b582d61d787e20c6a7193fdcbad2ea90de0e" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "2e14dcf129cab5e1c980655971cfb5ff321b0844" } # Please use the following script to update collab. # Working directory: frontend # diff --git a/frontend/rust-lib/collab-integrate/Cargo.toml b/frontend/rust-lib/collab-integrate/Cargo.toml index d801024469f0..b0ba081ef3f4 100644 --- a/frontend/rust-lib/collab-integrate/Cargo.toml +++ b/frontend/rust-lib/collab-integrate/Cargo.toml @@ -13,14 +13,14 @@ collab-database = { version = "0.1.0" } collab-plugins = { version = "0.1.0" } collab-document = { version = "0.1.0" } collab-entity = { version = "0.1.0" } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -anyhow = "1.0" -tracing = "0.1" -parking_lot = "0.12.1" -futures = "0.3" -async-trait = "0.1.73" -tokio = {version = "1.26", features = ["sync"]} +serde.workspace = true +serde_json.workspace = true +anyhow.workspace = true +tracing.workspace = true +parking_lot.workspace = true +futures.workspace = true +async-trait.workspace = true +tokio = { workspace = true, features = ["sync"]} lib-infra = { path = "../../../shared-lib/lib-infra" } [features] diff --git a/frontend/rust-lib/dart-ffi/Cargo.toml b/frontend/rust-lib/dart-ffi/Cargo.toml index 8bd45c8483db..a719c3e54bdc 100644 --- a/frontend/rust-lib/dart-ffi/Cargo.toml +++ b/frontend/rust-lib/dart-ffi/Cargo.toml @@ -14,16 +14,15 @@ crate-type = ["staticlib"] [dependencies] allo-isolate = { version = "^0.1", features = ["catch-unwind"] } byteorder = { version = "1.4.3" } -protobuf = { version = "2.28.0" } -tokio = { version = "1.26", features = ["full", "rt-multi-thread", "tracing"] } -log = "0.4.17" -serde = { version = "1.0", features = ["derive"] } -serde_json = { version = "1.0" } -bytes = { version = "1.5" } +protobuf.workspace = true +tokio = { workspace = true, features = ["full", "rt-multi-thread", "tracing"] } +serde.workspace = true +serde_json.workspace = true +bytes.workspace = true crossbeam-utils = "0.8.15" lazy_static = "1.4.0" -parking_lot = "0.12.1" -tracing = { version = "0.1", features = ["log"] } +parking_lot.workspace = true +tracing.workspace = true # workspace lib-dispatch = { workspace = true } diff --git a/frontend/rust-lib/dart-ffi/src/lib.rs b/frontend/rust-lib/dart-ffi/src/lib.rs index f9af82ce7b68..3c488c6476b2 100644 --- a/frontend/rust-lib/dart-ffi/src/lib.rs +++ b/frontend/rust-lib/dart-ffi/src/lib.rs @@ -4,9 +4,8 @@ use std::sync::Arc; use std::{ffi::CStr, os::raw::c_char}; use lazy_static::lazy_static; -use log::error; use parking_lot::Mutex; -use tracing::trace; +use tracing::{error, trace}; use flowy_core::*; use flowy_notification::{register_notification_sender, unregister_all_notification_sender}; diff --git a/frontend/rust-lib/event-integration/Cargo.toml b/frontend/rust-lib/event-integration/Cargo.toml index f571986ffc93..82a342568c2e 100644 --- a/frontend/rust-lib/event-integration/Cargo.toml +++ b/frontend/rust-lib/event-integration/Cargo.toml @@ -21,20 +21,20 @@ lib-infra = { path = "../../../shared-lib/lib-infra" } flowy-server = { path = "../flowy-server" } flowy-server-config = { workspace = true } flowy-notification = { workspace = true } -anyhow = "1.0.71" +anyhow.workspace = true flowy-storage = { workspace = true } -serde = { version = "1.0", features = ["derive"] } -serde_json = {version = "1.0"} -protobuf = {version = "2.28.0"} -tokio = { version = "1.26", features = ["full"]} +serde.workspace = true +serde_json.workspace = true +protobuf.workspace = true +tokio = { workspace = true, features = ["full"]} futures-util = "0.3.26" thread-id = "3.3.0" -bytes = "1.4" +bytes.workspace = true nanoid = "0.4.0" -tracing = { version = "0.1.27" } -parking_lot = "0.12.1" -uuid = { version = "1.3.3", features = ["serde", "v4"] } +tracing.workspace = true +parking_lot.workspace = true +uuid.workspace = true collab = { version = "0.1.0" } collab-document = { version = "0.1.0" } collab-folder = { version = "0.1.0" } @@ -45,7 +45,7 @@ collab-entity = { version = "0.1.0" } [dev-dependencies] dotenv = "0.15.0" tempdir = "0.3.7" -uuid = { version = "1.3.3", features = ["v4"] } +uuid.workspace = true assert-json-diff = "2.0.2" tokio-postgres = { version = "0.7.8" } zip = "0.6.6" diff --git a/frontend/rust-lib/flowy-ai/Cargo.toml b/frontend/rust-lib/flowy-ai/Cargo.toml index a8526484c702..ece06cce6e82 100644 --- a/frontend/rust-lib/flowy-ai/Cargo.toml +++ b/frontend/rust-lib/flowy-ai/Cargo.toml @@ -7,10 +7,10 @@ edition = "2021" [dependencies] reqwest = { version = "0.11", features = ["json"] } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -anyhow = "1.0.75" +serde.workspace = true +serde_json.workspace = true +anyhow.workspace = true lib-infra = { path = "../../../shared-lib/lib-infra" } async-openai = "0.14.2" -tokio = { version = "1.12", features = ["rt", "sync"] } +tokio = { workspace = true, features = ["rt", "sync"] } dotenv = "0.15.0" \ No newline at end of file diff --git a/frontend/rust-lib/flowy-config/Cargo.toml b/frontend/rust-lib/flowy-config/Cargo.toml index 1870a1123000..a4fcf93da4ec 100644 --- a/frontend/rust-lib/flowy-config/Cargo.toml +++ b/frontend/rust-lib/flowy-config/Cargo.toml @@ -12,8 +12,8 @@ lib-dispatch = { workspace = true } flowy-error = { workspace = true } flowy-derive = { path = "../../../shared-lib/flowy-derive" } -protobuf = {version = "2.28.0"} -bytes = { version = "1.5" } +protobuf.workspace = true +bytes.workspace = true strum_macros = "0.21" [build-dependencies] diff --git a/frontend/rust-lib/flowy-core/Cargo.toml b/frontend/rust-lib/flowy-core/Cargo.toml index ed578334102d..3ae5624b7ee7 100644 --- a/frontend/rust-lib/flowy-core/Cargo.toml +++ b/frontend/rust-lib/flowy-core/Cargo.toml @@ -28,26 +28,26 @@ flowy-ai = { workspace = true } collab-entity = { version = "0.1.0" } collab-plugins = { version = "0.1.0" } collab = { version = "0.1.0" } -diesel = { version = "1.4.8", features = ["sqlite"] } -uuid = { version = "1.3.3", features = ["v4"] } +diesel.workspace = true +uuid.workspace = true flowy-storage = { workspace = true } client-api = { version = "0.1.0", features = ["collab-sync"] } -tracing = { version = "0.1", features = ["log"] } +tracing.workspace = true futures-core = { version = "0.3", default-features = false } -bytes = "1.5" -tokio = { version = "1.26", features = ["full"] } -tokio-stream = {version = "0.1.14", features = ["sync"]} +bytes.workspace = true +tokio = { workspace = true, features = ["full"] } +tokio-stream = { workspace = true, features = ["sync"]} console-subscriber = { version = "0.2", optional = true } -parking_lot = "0.12.1" -anyhow = "1.0.75" +parking_lot.workspace = true +anyhow.workspace = true base64 = "0.21.5" lib-infra = { path = "../../../shared-lib/lib-infra" } -serde = "1.0" -serde_json = "1.0" -serde_repr = "0.1" -futures = "0.3.28" +serde.workspace = true +serde_json.workspace = true +serde_repr.workspace = true +futures.workspace = true walkdir = "2.4.0" [features] diff --git a/frontend/rust-lib/flowy-database-deps/Cargo.toml b/frontend/rust-lib/flowy-database-deps/Cargo.toml index b9200fe5b222..9bd8f911df8b 100644 --- a/frontend/rust-lib/flowy-database-deps/Cargo.toml +++ b/frontend/rust-lib/flowy-database-deps/Cargo.toml @@ -9,4 +9,4 @@ edition = "2021" lib-infra = { path = "../../../shared-lib/lib-infra" } flowy-error = { workspace = true } collab-entity = { version = "0.1.0" } -anyhow = "1.0.71" \ No newline at end of file +anyhow.workspace = true \ No newline at end of file diff --git a/frontend/rust-lib/flowy-database2/Cargo.toml b/frontend/rust-lib/flowy-database2/Cargo.toml index b4867a5fe30a..edffb0d202d3 100644 --- a/frontend/rust-lib/flowy-database2/Cargo.toml +++ b/frontend/rust-lib/flowy-database2/Cargo.toml @@ -14,35 +14,34 @@ flowy-database-deps = { workspace = true } flowy-derive = { path = "../../../shared-lib/flowy-derive" } flowy-notification = { workspace = true } -parking_lot = "0.12.1" -protobuf = {version = "2.28.0"} +parking_lot.workspace = true +protobuf.workspace = true flowy-error = { workspace = true, features = ["impl_from_dispatch_error", "impl_from_collab"]} lib-dispatch = { workspace = true } -tokio = { version = "1.26", features = ["sync"] } +tokio = { workspace = true, features = ["sync"] } flowy-task= { workspace = true } -bytes = { version = "1.5" } -tracing = { version = "0.1", features = ["log"] } -serde = { version = "1.0", features = ["derive"] } -serde_json = {version = "1.0"} -serde_repr = "0.1" +bytes.workspace = true +tracing.workspace = true +serde.workspace = true +serde_json.workspace = true +serde_repr.workspace = true lib-infra = { path = "../../../shared-lib/lib-infra" } -chrono = { version = "0.4.31", default-features = false, features = ["clock"] } +chrono = { workspace = true, default-features = false, features = ["clock"] } rust_decimal = "1.28.1" rusty-money = {version = "0.4.1", features = ["iso"]} lazy_static = "1.4.0" indexmap = {version = "1.9.2", features = ["serde"]} url = { version = "2"} fancy-regex = "0.11.0" -futures = "0.3.26" +futures.workspace = true dashmap = "5" -anyhow = "1.0" +anyhow.workspace = true async-stream = "0.3.4" rayon = "1.6.1" nanoid = "0.4.0" -async-trait = "0.1.73" +async-trait.workspace = true chrono-tz = "0.8.2" csv = "1.1.6" - strum = "0.25" strum_macros = "0.25" diff --git a/frontend/rust-lib/flowy-date/Cargo.toml b/frontend/rust-lib/flowy-date/Cargo.toml index 6211fd9bb7fb..61599c478710 100644 --- a/frontend/rust-lib/flowy-date/Cargo.toml +++ b/frontend/rust-lib/flowy-date/Cargo.toml @@ -9,12 +9,12 @@ edition = "2021" lib-dispatch = { path = "../lib-dispatch" } flowy-error = { path = "../flowy-error" } flowy-derive = { path = "../../../shared-lib/flowy-derive" } -protobuf = { version = "2.28.0" } -bytes = { version = "1.4" } +protobuf.workspace = true +bytes.workspace = true strum_macros = "0.21" -tracing = { version = "0.1" } +tracing.workspace = true date_time_parser = { version = "0.2.0" } -chrono = { version = "0.4.26" } +chrono.workspace = true fancy-regex = { version = "0.11.0" } [features] diff --git a/frontend/rust-lib/flowy-document-deps/Cargo.toml b/frontend/rust-lib/flowy-document-deps/Cargo.toml index 9c926cea7693..d4b04ca2b15d 100644 --- a/frontend/rust-lib/flowy-document-deps/Cargo.toml +++ b/frontend/rust-lib/flowy-document-deps/Cargo.toml @@ -9,4 +9,4 @@ edition = "2021" lib-infra = { path = "../../../shared-lib/lib-infra" } flowy-error = { workspace = true } collab-document = { version = "0.1.0" } -anyhow = "1.0.71" \ No newline at end of file +anyhow.workspace = true \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/Cargo.toml b/frontend/rust-lib/flowy-document2/Cargo.toml index 332176aed495..ed89fcecf8b8 100644 --- a/frontend/rust-lib/flowy-document2/Cargo.toml +++ b/frontend/rust-lib/flowy-document2/Cargo.toml @@ -19,20 +19,20 @@ flowy-error = { path = "../flowy-error", features = ["impl_from_serde", "impl_fr lib-dispatch = { workspace = true } lib-infra = { path = "../../../shared-lib/lib-infra" } validator = "0.16.0" -protobuf = {version = "2.28.0"} -bytes = { version = "1.5" } +protobuf.workspace = true +bytes.workspace = true nanoid = "0.4.0" -parking_lot = "0.12.1" +parking_lot.workspace = true strum_macros = "0.21" -serde = { version = "1.0", features = ["derive"] } -serde_json = {version = "1.0"} -tracing = { version = "0.1", features = ["log"] } -tokio = { version = "1.26", features = ["full"] } -anyhow = "1.0" +serde.workspace = true +serde_json.workspace = true +tracing.workspace = true +tokio = { workspace = true, features = ["full"] } +anyhow.workspace = true indexmap = {version = "1.9.2", features = ["serde"]} -uuid = { version = "1.3.3", features = ["v4"] } -futures = "0.3.26" -tokio-stream = { version = "0.1.14", features = ["sync"] } +uuid.workspace = true +futures.workspace = true +tokio-stream = { workspace = true, features = ["sync"] } scraper = "0.18.0" [dev-dependencies] diff --git a/frontend/rust-lib/flowy-encrypt/Cargo.toml b/frontend/rust-lib/flowy-encrypt/Cargo.toml index 402d1a7b1aab..2041fccff15c 100644 --- a/frontend/rust-lib/flowy-encrypt/Cargo.toml +++ b/frontend/rust-lib/flowy-encrypt/Cargo.toml @@ -11,5 +11,5 @@ rand = "0.8" pbkdf2 = "0.12.2" hmac = "0.12.1" sha2 = "0.10.7" -anyhow = "1.0.72" +anyhow.workspace = true base64 = "0.21.2" \ No newline at end of file diff --git a/frontend/rust-lib/flowy-error/Cargo.toml b/frontend/rust-lib/flowy-error/Cargo.toml index 2a06f299efb4..6a99f0b155b9 100644 --- a/frontend/rust-lib/flowy-error/Cargo.toml +++ b/frontend/rust-lib/flowy-error/Cargo.toml @@ -7,18 +7,18 @@ edition = "2018" [dependencies] flowy-derive = { path = "../../../shared-lib/flowy-derive" } -protobuf = { version = "2.28.0" } -bytes = "1.4" -anyhow = "1.0" +protobuf.workspace = true +bytes.workspace = true +anyhow.workspace = true thiserror = "1.0" validator = "0.16.0" -tokio = { version = "1.0", features = ["sync"]} +tokio = { workspace = true, features = ["sync"]} fancy-regex = { version = "0.11.0" } lib-dispatch = { workspace = true, optional = true } -serde_json = { version = "1.0", optional = true } -serde_repr = { version = "0.1" } -serde = "1.0" +serde_json.workspace = true +serde_repr.workspace = true +serde.workspace = true reqwest = { version = "0.11.14", optional = true, features = [ "native-tls-vendored", ] } @@ -33,7 +33,7 @@ client-api = { version = "0.1.0", optional = true } [features] default = ["impl_from_appflowy_cloud", "impl_from_collab", "impl_from_reqwest", "impl_from_serde"] impl_from_dispatch_error = ["lib-dispatch"] -impl_from_serde = ["serde_json"] +impl_from_serde = [] impl_from_reqwest = ["reqwest"] impl_from_sqlite = ["flowy-sqlite", "r2d2"] impl_from_collab = ["collab-database", "collab-document", "impl_from_reqwest"] diff --git a/frontend/rust-lib/flowy-folder-deps/Cargo.toml b/frontend/rust-lib/flowy-folder-deps/Cargo.toml index ec01d5bb05eb..26c6dfaf2717 100644 --- a/frontend/rust-lib/flowy-folder-deps/Cargo.toml +++ b/frontend/rust-lib/flowy-folder-deps/Cargo.toml @@ -9,5 +9,5 @@ edition = "2021" lib-infra = { path = "../../../shared-lib/lib-infra" } flowy-error = { workspace = true } collab-folder = { version = "0.1.0" } -uuid = { version = "1.3.3", features = ["v4"] } -anyhow = "1.0.71" \ No newline at end of file +uuid.workspace = true +anyhow.workspace = true \ No newline at end of file diff --git a/frontend/rust-lib/flowy-folder2/Cargo.toml b/frontend/rust-lib/flowy-folder2/Cargo.toml index 3b306db49408..0f9842582819 100644 --- a/frontend/rust-lib/flowy-folder2/Cargo.toml +++ b/frontend/rust-lib/flowy-folder2/Cargo.toml @@ -14,21 +14,21 @@ flowy-folder-deps = { workspace = true } flowy-derive = { path = "../../../shared-lib/flowy-derive" } flowy-notification = { workspace = true } -parking_lot = "0.12.1" +parking_lot.workspace = true unicode-segmentation = "1.10" -tracing = { version = "0.1", features = ["log"] } +tracing.workspace = true flowy-error = { path = "../flowy-error", features = ["impl_from_dispatch_error"]} lib-dispatch = { workspace = true } -bytes = { version = "1.5" } +bytes.workspace = true lib-infra = { path = "../../../shared-lib/lib-infra" } -tokio = { version = "1.26", features = ["full"] } +tokio = { workspace = true, features = ["full"] } nanoid = "0.4.0" lazy_static = "1.4.0" -chrono = { version = "0.4.31", default-features = false, features = ["clock"] } +chrono = { workspace = true, default-features = false, features = ["clock"] } strum_macros = "0.21" -protobuf = {version = "2.28.0"} -uuid = { version = "1.3.3", features = ["v4"] } -tokio-stream = { version = "0.1.14", features = ["sync"] } +protobuf.workspace = true +uuid.workspace = true +tokio-stream = { workspace = true, features = ["sync"] } [build-dependencies] flowy-codegen = { path = "../../../shared-lib/flowy-codegen"} diff --git a/frontend/rust-lib/flowy-notification/Cargo.toml b/frontend/rust-lib/flowy-notification/Cargo.toml index 64dab43b4071..ef8886cd28c0 100644 --- a/frontend/rust-lib/flowy-notification/Cargo.toml +++ b/frontend/rust-lib/flowy-notification/Cargo.toml @@ -7,10 +7,10 @@ edition = "2018" [dependencies] lazy_static = { version = "1.4.0" } -protobuf = { version = "2.28.0" } -tracing = { version = "0.1", features = ["log"] } -bytes = { version = "1.5" } -serde = "1.0" +protobuf.workspace = true +tracing.workspace = true +bytes.workspace = true +serde.workspace = true flowy-derive = { path = "../../../shared-lib/flowy-derive" } lib-dispatch = { workspace = true } diff --git a/frontend/rust-lib/flowy-server-config/Cargo.toml b/frontend/rust-lib/flowy-server-config/Cargo.toml index 8e11e35439fa..183271f43094 100644 --- a/frontend/rust-lib/flowy-server-config/Cargo.toml +++ b/frontend/rust-lib/flowy-server-config/Cargo.toml @@ -7,4 +7,4 @@ edition = "2021" [dependencies] flowy-error = { workspace = true } -serde = { version = "1.0", features = ["derive"] } \ No newline at end of file +serde.workspace = true \ No newline at end of file diff --git a/frontend/rust-lib/flowy-server/Cargo.toml b/frontend/rust-lib/flowy-server/Cargo.toml index 76141ea518ba..ed0224372bb7 100644 --- a/frontend/rust-lib/flowy-server/Cargo.toml +++ b/frontend/rust-lib/flowy-server/Cargo.toml @@ -6,24 +6,24 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -tracing = { version = "0.1" } -futures = "0.3.26" +tracing.workspace = true +futures.workspace = true futures-util = "0.3.26" reqwest = { version = "0.11.20", features = ["native-tls-vendored", "multipart", "blocking"] } hyper = "0.14" config = { version = "0.10.1", default-features = false, features = ["yaml"] } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" +serde.workspace = true +serde_json.workspace = true serde-aux = "4.2.0" thiserror = "1.0" -tokio = { version = "1.26", features = ["sync"]} -parking_lot = "0.12" +tokio = { workspace = true, features = ["sync"]} +parking_lot.workspace = true lazy_static = "1.4.0" -bytes = { version = "1.5", features = ["serde"] } +bytes = { workspace = true, features = ["serde"] } tokio-retry = "0.3" -anyhow = "1.0" -uuid = { version = "1.3.3", features = ["v4"] } -chrono = { version = "0.4.31", default-features = false, features = ["clock", "serde"] } +anyhow.workspace = true +uuid.workspace = true +chrono = { workspace = true, default-features = false, features = ["clock", "serde"] } collab = { version = "0.1.0" } collab-plugins = { version = "0.1.0"} collab-document = { version = "0.1.0" } @@ -42,15 +42,15 @@ flowy-storage = { workspace = true } mime_guess = "2.0" url = "2.4" tokio-util = "0.7" -tokio-stream = { version = "0.1.14", features = ["sync"] } +tokio-stream = { workspace = true, features = ["sync"] } client-api = { version = "0.1.0", features = ["collab-sync", "test_util"] } lib-dispatch = { workspace = true } [dev-dependencies] -uuid = { version = "1.3.3", features = ["v4"] } +uuid.workspace = true tracing-subscriber = { version = "0.3.3", features = ["env-filter"] } dotenv = "0.15.0" yrs = "0.16.5" assert-json-diff = "2.0.2" -serde_json = "1.0.104" +serde_json.workspace = true client-api = { version = "0.1.0" } diff --git a/frontend/rust-lib/flowy-sqlite/Cargo.toml b/frontend/rust-lib/flowy-sqlite/Cargo.toml index 810be1006553..b6639960d571 100644 --- a/frontend/rust-lib/flowy-sqlite/Cargo.toml +++ b/frontend/rust-lib/flowy-sqlite/Cargo.toml @@ -6,15 +6,15 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -diesel = { version = "1.4.8", features = ["sqlite", "chrono"] } +diesel.workspace = true diesel_derives = { version = "1.4.1", features = ["sqlite"] } diesel_migrations = { version = "1.4.0", features = ["sqlite"] } -tracing = { version = "0.1", features = ["log"] } +tracing.workspace = true lazy_static = "1.4.0" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -anyhow = "1.0" -parking_lot = "0.12.1" +serde.workspace = true +serde_json.workspace = true +anyhow.workspace = true +parking_lot.workspace = true r2d2 = "0.8.10" libsqlite3-sys = { version = ">=0.8.0, <0.24.0", features = ["bundled"] } diff --git a/frontend/rust-lib/flowy-storage/Cargo.toml b/frontend/rust-lib/flowy-storage/Cargo.toml index 1387ec98acc0..3fef4854464d 100644 --- a/frontend/rust-lib/flowy-storage/Cargo.toml +++ b/frontend/rust-lib/flowy-storage/Cargo.toml @@ -7,10 +7,10 @@ edition = "2021" [dependencies] reqwest = { version = "0.11", features = ["json", "stream"] } -serde_json = "1.0" -serde = { version = "1.0", features = ["derive"] } -async-trait = "0.1.73" -bytes = "1.0.1" +serde_json.workspace = true +serde.workspace = true +async-trait.workspace = true +bytes.workspace = true mime_guess = "2.0" lib-infra = { path = "../../../shared-lib/lib-infra" } url = "2.2.2" diff --git a/frontend/rust-lib/flowy-task/Cargo.toml b/frontend/rust-lib/flowy-task/Cargo.toml index ee3c749e2697..e28795eff28f 100644 --- a/frontend/rust-lib/flowy-task/Cargo.toml +++ b/frontend/rust-lib/flowy-task/Cargo.toml @@ -7,11 +7,11 @@ edition = "2021" [dependencies] lib-infra = { path = "../../../shared-lib/lib-infra" } -tokio = { version = "1.26", features = ["sync", "macros", ]} +tokio = { workspace = true, features = ["sync", "macros", ]} atomic_refcell = "0.1.9" -anyhow = "1.0" -tracing = { version = "0.1", features = ["log"] } +anyhow.workspace = true +tracing.workspace = true [dev-dependencies] rand = "0.8.5" -futures = "0.3.26" +futures.workspace = true diff --git a/frontend/rust-lib/flowy-user-deps/Cargo.toml b/frontend/rust-lib/flowy-user-deps/Cargo.toml index 82dac5eda898..3ab00eb697ce 100644 --- a/frontend/rust-lib/flowy-user-deps/Cargo.toml +++ b/frontend/rust-lib/flowy-user-deps/Cargo.toml @@ -8,11 +8,11 @@ edition = "2021" [dependencies] lib-infra = { path = "../../../shared-lib/lib-infra" } flowy-error = { workspace = true } -uuid = { version = "1.3.3", features = ["v4"] } -serde = { version = "1.0", features = ["derive"] } +uuid.workspace = true +serde.workspace = true collab-entity = { version = "0.1.0" } -serde_json = { version = "1.0"} -serde_repr = "0.1" -chrono = { version = "0.4.31", default-features = false, features = ["clock", "serde"] } -anyhow = "1.0.71" -tokio = { version = "1.26", features = ["sync"] } +serde_json.workspace = true +serde_repr.workspace = true +chrono = { workspace = true, default-features = false, features = ["clock", "serde"] } +anyhow.workspace = true +tokio = { workspace = true, features = ["sync"] } diff --git a/frontend/rust-lib/flowy-user/Cargo.toml b/frontend/rust-lib/flowy-user/Cargo.toml index 459e6c4b54f8..dce4628ee1ae 100644 --- a/frontend/rust-lib/flowy-user/Cargo.toml +++ b/frontend/rust-lib/flowy-user/Cargo.toml @@ -23,28 +23,26 @@ collab-database = { version = "0.1.0" } collab-user = { version = "0.1.0" } collab-entity = { version = "0.1.0" } flowy-user-deps = { workspace = true } -anyhow = "1.0.75" - -tracing = { version = "0.1", features = ["log"] } -bytes = "1.4" -serde = { version = "1.0", features = ["derive"] } -serde_json = { version = "1.0" } -serde_repr = "0.1" -log = "0.4.17" -protobuf = { version = "2.28.0" } +anyhow.workspace = true +tracing.workspace = true +bytes.workspace = true +serde.workspace = true +serde_json.workspace = true +serde_repr.workspace = true +protobuf.workspace = true lazy_static = "1.4.0" -diesel = { version = "1.4.8", features = ["sqlite"] } +diesel.workspace = true diesel_derives = { version = "1.4.1", features = ["sqlite"] } once_cell = "1.17.1" -parking_lot = "0.12.1" +parking_lot.workspace = true strum = "0.25" strum_macros = "0.25.2" -tokio = { version = "1.26", features = ["rt"] } +tokio = { workspace = true, features = ["rt"] } validator = "0.16.0" unicode-segmentation = "1.10" fancy-regex = "0.11.0" -uuid = { version = "1.3.3", features = [ "v4"] } -chrono = { version = "0.4.31", default-features = false, features = ["clock"] } +uuid.workspace = true +chrono = { workspace = true, default-features = false, features = ["clock"] } base64 = "^0.21" tokio-stream = "0.1.14" diff --git a/frontend/rust-lib/lib-dispatch/Cargo.toml b/frontend/rust-lib/lib-dispatch/Cargo.toml index 0ae9fa340ef8..b8ce73de8066 100644 --- a/frontend/rust-lib/lib-dispatch/Cargo.toml +++ b/frontend/rust-lib/lib-dispatch/Cargo.toml @@ -9,27 +9,27 @@ edition = "2018" pin-project = "1.0" futures-core = { version = "0.3", default-features = false } futures-channel = "0.3.26" -futures = "0.3.26" +futures.workspace = true futures-util = "0.3.26" bytes = {version = "1.4", features = ["serde"]} -tokio = { version = "1.26", features = ["full"] } +tokio = { workspace = true, features = ["full"] } nanoid = "0.4.0" -log = "0.4.17" thread-id = "3.3.0" dyn-clone = "1.0" derivative = "2.2.0" -serde_json = {version = "1.0", optional = true } +serde_json = { workspace = true, optional = true } serde = { version = "1.0", features = ["derive"], optional = true } -serde_repr = { version = "0.1", optional = true } +serde_repr = { workspace = true, optional = true } validator = "0.16.1" -tracing = { version = "0.1"} +tracing.workspace = true +parking_lot = "0.12" #optional crate bincode = { version = "1.3", optional = true} -protobuf = {version = "2.28.0", optional = true} +protobuf = { workspace = true, optional = true } [dev-dependencies] -tokio = { version = "1.26", features = ["full"] } +tokio = { workspace = true, features = ["full"] } futures-util = "0.3.26" [features] diff --git a/frontend/rust-lib/lib-dispatch/src/data.rs b/frontend/rust-lib/lib-dispatch/src/data.rs index 9cb028ad649f..144cf219c3b8 100644 --- a/frontend/rust-lib/lib-dispatch/src/data.rs +++ b/frontend/rust-lib/lib-dispatch/src/data.rs @@ -78,7 +78,7 @@ where fn respond_to(self, _request: &AFPluginEventRequest) -> AFPluginEventResponse { match self.into_inner().into_bytes() { Ok(bytes) => { - log::trace!( + tracing::trace!( "Serialize Data: {:?} to event response", std::any::type_name::<T>() ); diff --git a/frontend/rust-lib/lib-dispatch/src/module/data.rs b/frontend/rust-lib/lib-dispatch/src/module/data.rs index 1d7129927462..520c3e24945c 100644 --- a/frontend/rust-lib/lib-dispatch/src/module/data.rs +++ b/frontend/rust-lib/lib-dispatch/src/module/data.rs @@ -67,7 +67,7 @@ where "Failed to get the plugin state of type: {}", type_name::<T>() ); - log::error!("{}", msg,); + tracing::error!("{}", msg,); ready(Err(InternalError::Other(msg).into())) } } diff --git a/frontend/rust-lib/lib-dispatch/src/request/request.rs b/frontend/rust-lib/lib-dispatch/src/request/request.rs index 45fb6a7d0029..c62950f65d05 100644 --- a/frontend/rust-lib/lib-dispatch/src/request/request.rs +++ b/frontend/rust-lib/lib-dispatch/src/request/request.rs @@ -80,7 +80,7 @@ impl FromAFPluginRequest for String { } pub fn unexpected_none_payload(request: &AFPluginEventRequest) -> DispatchError { - log::warn!("{:?} expected payload", &request.event); + tracing::warn!("{:?} expected payload", &request.event); InternalError::UnexpectedNone("Expected payload".to_string()).into() } diff --git a/frontend/rust-lib/lib-log/Cargo.toml b/frontend/rust-lib/lib-log/Cargo.toml index 9dc46e9121c6..1a4e9933fbe1 100644 --- a/frontend/rust-lib/lib-log/Cargo.toml +++ b/frontend/rust-lib/lib-log/Cargo.toml @@ -11,10 +11,9 @@ tracing-subscriber = { version = "0.3.17", features = ["registry", "env-filter", tracing-bunyan-formatter = "0.3.9" tracing-appender = "0.2.2" tracing-core = "0.1" -tracing = { version = "0.1", features = ["log"] } -log = "0.4.17" -serde_json = "1.0" -serde = "1.0" +tracing.workspace = true +serde_json.workspace = true +serde.workspace = true chrono = "0.4" lazy_static = "1.4.0" diff --git a/shared-lib/Cargo.lock b/shared-lib/Cargo.lock index 0550d668de43..fff37673fdfc 100644 --- a/shared-lib/Cargo.lock +++ b/shared-lib/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "aho-corasick" version = "0.7.18" @@ -37,19 +52,19 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.71" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "async-trait" -version = "0.1.64" +version = "0.1.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2" +checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.39", ] [[package]] @@ -58,6 +73,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "basic-toml" version = "0.1.2" @@ -415,7 +445,6 @@ dependencies = [ "flowy-ast", "flowy-codegen", "lazy_static", - "log", "proc-macro2", "quote", "serde_json", @@ -469,6 +498,12 @@ dependencies = [ "wasi 0.10.0+wasi-snapshot-preview1", ] +[[package]] +name = "gimli" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" + [[package]] name = "glob" version = "0.3.1" @@ -627,9 +662,9 @@ dependencies = [ [[package]] name = "itoa" -version = "0.4.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "js-sys" @@ -669,7 +704,6 @@ dependencies = [ "indexmap", "indextree", "lazy_static", - "log", "serde", "serde_json", "strum", @@ -680,9 +714,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.139" +version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] name = "libm" @@ -735,16 +769,24 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + [[package]] name = "mio" -version = "0.8.6" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" dependencies = [ "libc", - "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -766,6 +808,15 @@ dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.17.1" @@ -850,7 +901,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.39", ] [[package]] @@ -964,14 +1015,14 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.39", ] [[package]] name = "pin-project-lite" -version = "0.2.7" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "ppv-lite86" @@ -1107,9 +1158,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.27" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -1230,6 +1281,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + [[package]] name = "rustix" version = "0.37.3" @@ -1273,29 +1330,29 @@ checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" [[package]] name = "serde" -version = "1.0.163" +version = "1.0.192" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" +checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.163" +version = "1.0.192" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" +checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.39", ] [[package]] name = "serde_json" -version = "1.0.71" +version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063bf466a64011ac24040a49009724ee60a57da1b437617ceb32e53ad61bfb19" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ "itoa", "ryu", @@ -1351,12 +1408,12 @@ checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" [[package]] name = "socket2" -version = "0.4.7" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", - "winapi", + "windows-sys 0.48.0", ] [[package]] @@ -1390,9 +1447,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.16" +version = "2.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" dependencies = [ "proc-macro2", "quote", @@ -1471,7 +1528,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.39", ] [[package]] @@ -1485,14 +1542,13 @@ dependencies = [ [[package]] name = "tokio" -version = "1.26.0" +version = "1.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64" +checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" dependencies = [ - "autocfg", + "backtrace", "bytes", "libc", - "memchr", "mio", "num_cpus", "parking_lot", @@ -1500,18 +1556,18 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" -version = "1.8.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.39", ] [[package]] @@ -1525,12 +1581,10 @@ dependencies = [ [[package]] name = "tracing" -version = "0.1.29" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", - "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -1538,22 +1592,22 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.18" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f480b8f81512e825f337ad51e94c1eb5d3bbdf2b363dcd01e2b19a9ffe3f8e" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.39", ] [[package]] name = "tracing-core" -version = "0.1.21" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ - "lazy_static", + "once_cell", ] [[package]] diff --git a/shared-lib/Cargo.toml b/shared-lib/Cargo.toml index 82438fe2e41d..37d51ffeb2a6 100644 --- a/shared-lib/Cargo.toml +++ b/shared-lib/Cargo.toml @@ -11,3 +11,13 @@ members = [ opt-level = 0 #https://doc.rust-lang.org/rustc/codegen-options/index.html#debug-assertions #split-debuginfo = "unpacked" + + +[workspace.dependencies] +anyhow = "1.0.75" +tracing = "0.1.40" +serde = "1.0.108" +serde_json = "1.0.108" +tokio = "1.34.0" +async-trait = "0.1.74" +chrono = { version = "0.4.31", default-features = false, features = ["clock"] } diff --git a/shared-lib/flowy-codegen/Cargo.toml b/shared-lib/flowy-codegen/Cargo.toml index 6121de6edd5c..4b713071a369 100644 --- a/shared-lib/flowy-codegen/Cargo.toml +++ b/shared-lib/flowy-codegen/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" [dependencies] log = "0.4.17" serde = { version = "1.0", features = ["derive"]} -serde_json = "1.0" +serde_json.workspace = true flowy-ast = { path = "../flowy-ast"} quote = "1.0" @@ -27,7 +27,6 @@ protoc-bin-vendored = { version = "3.0", optional = true } toml = {version = "0.5.11", optional = true} - [features] proto_gen = [ "similar", diff --git a/shared-lib/flowy-codegen/src/dart_event/dart_event.rs b/shared-lib/flowy-codegen/src/dart_event/dart_event.rs index 3a0a69240b71..8ab6d3fb593d 100644 --- a/shared-lib/flowy-codegen/src/dart_event/dart_event.rs +++ b/shared-lib/flowy-codegen/src/dart_event/dart_event.rs @@ -1,22 +1,26 @@ -use super::event_template::*; -use crate::ast::EventASTContext; -use crate::flowy_toml::{parse_crate_config_from, CrateConfig}; -use crate::util::{is_crate_dir, is_hidden, path_string_with_component, read_file}; -use flowy_ast::ASTResult; use std::fs::File; use std::io::Write; use std::path::PathBuf; + use syn::Item; use walkdir::WalkDir; +use flowy_ast::ASTResult; + +use crate::ast::EventASTContext; +use crate::flowy_toml::{parse_crate_config_from, CrateConfig}; +use crate::util::{is_crate_dir, is_hidden, path_string_with_component, read_file}; + +use super::event_template::*; + pub fn gen(crate_name: &str) { if std::env::var("CARGO_MAKE_WORKING_DIRECTORY").is_err() { - log::warn!("CARGO_MAKE_WORKING_DIRECTORY was not set, skip generate dart pb"); + println!("CARGO_MAKE_WORKING_DIRECTORY was not set, skip generate dart pb"); return; } if std::env::var("FLUTTER_FLOWY_SDK_PATH").is_err() { - log::warn!("FLUTTER_FLOWY_SDK_PATH was not set, skip generate dart pb"); + println!("FLUTTER_FLOWY_SDK_PATH was not set, skip generate dart pb"); return; } diff --git a/shared-lib/flowy-derive/Cargo.toml b/shared-lib/flowy-derive/Cargo.toml index a363dbc95224..2c2285987dd9 100644 --- a/shared-lib/flowy-derive/Cargo.toml +++ b/shared-lib/flowy-derive/Cargo.toml @@ -21,10 +21,9 @@ flowy-ast = { path = "../flowy-ast" } lazy_static = {version = "1.4.0"} dashmap = "5" flowy-codegen = { path = "../flowy-codegen"} -serde_json = "1.0" +serde_json.workspace = true walkdir = "2.3.2" [dev-dependencies] -tokio = { version = "1.26", features = ["full"] } +tokio = { workspace = true, features = ["full"] } trybuild = "1.0.77" -log = "0.4.17" diff --git a/shared-lib/lib-infra/Cargo.toml b/shared-lib/lib-infra/Cargo.toml index e064f142682a..4a1c4df90c74 100644 --- a/shared-lib/lib-infra/Cargo.toml +++ b/shared-lib/lib-infra/Cargo.toml @@ -6,12 +6,12 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -chrono = { version = "0.4.31", default-features = false, features = ["clock"] } +chrono = { workspace = true, default-features = false, features = ["clock"] } bytes = { version = "1.5" } pin-project = "1.1.3" futures-core = { version = "0.3" } -tokio = { version = "1.26", features = ["time", "rt"] } +tokio = { workspace = true, features = ["time", "rt"] } rand = "0.8.5" -async-trait = "0.1.64" +async-trait.workspace = true md5 = "0.7.0" -anyhow = "1.0" +anyhow.workspace = true diff --git a/shared-lib/lib-ot/Cargo.toml b/shared-lib/lib-ot/Cargo.toml index 259cf1bdd547..9d36af88d702 100644 --- a/shared-lib/lib-ot/Cargo.toml +++ b/shared-lib/lib-ot/Cargo.toml @@ -8,10 +8,9 @@ edition = "2018" [dependencies] serde = { version = "1.0", features = ["derive", "rc"] } thiserror = "1.0" -serde_json = { version = "1.0" } +serde_json.workspace = true indexmap = {version = "1.9.2", features = ["serde"]} -log = "0.4" -tracing = { version = "0.1", features = ["log"] } +tracing.workspace = true lazy_static = "1.4.0" strum = "0.21" strum_macros = "0.21" From 765103dd22a65f14e29f4e6d90acad2c2349234d Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Sun, 12 Nov 2023 23:41:10 +0800 Subject: [PATCH 44/56] chore: share button text color (#3913) --- .../lib/plugins/document/presentation/share/share_button.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart index f25096955483..3e36b370e0ba 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart @@ -112,7 +112,7 @@ class ShareActionListState extends State<ShareActionList> { return RoundedTextButton( title: LocaleKeys.shareAction_buttonText.tr(), onPressed: () => controller.show(), - textColor: Theme.of(context).colorScheme.onSurface, + textColor: Theme.of(context).colorScheme.onPrimary, ); }, onSelected: (action, controller) async { From 7cee8e392f53cbce4f3fc1d4e05454d2d4223c0c Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" <lucas.xu@appflowy.io> Date: Mon, 13 Nov 2023 10:07:46 +0800 Subject: [PATCH 45/56] feat: adjust cover plugin and support recent section on mobile platform (#3921) --- frontend/appflowy_flutter/ios/Podfile.lock | 12 + .../appflowy_flutter/ios/Runner/Info.plist | 132 +++++------ .../lib/mobile/application/mobile_router.dart | 10 + .../presentation/base/mobile_view_page.dart | 10 +- .../presentation/home/mobile_home_page.dart | 5 +- .../home/mobile_home_page_recent_files.dart | 122 ---------- .../mobile_home_recent_views.dart | 104 +++++++++ .../recent_folder/mobile_recent_view.dart | 208 ++++++++++++++++++ .../show_flowy_mobile_bottom_sheet.dart | 1 + .../editor_plugins/header/cover_editor.dart | 14 +- .../header/document_header_node_widget.dart | 90 +++++++- .../image/flowy_image_picker.dart | 41 ++++ .../image/image_picker_screen.dart | 15 ++ .../image/upload_image_menu.dart | 50 ++++- .../lib/startup/deps_resolver.dart | 2 + .../lib/startup/tasks/generate_router.dart | 14 ++ frontend/appflowy_flutter/pubspec.lock | 64 ++++++ frontend/appflowy_flutter/pubspec.yaml | 1 + frontend/resources/translations/en.json | 3 +- .../flowy-folder2/src/event_handler.rs | 16 ++ .../rust-lib/flowy-folder2/src/event_map.rs | 4 + .../rust-lib/flowy-folder2/src/manager.rs | 40 +++- 22 files changed, 734 insertions(+), 224 deletions(-) delete mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_recent_files.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/flowy_image_picker.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_picker_screen.dart diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index 84858fb29ab8..cf59c986e6b0 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -48,6 +48,9 @@ PODS: - fluttertoast (0.0.2): - Flutter - Toast + - FMDB (2.7.5): + - FMDB/standard (= 2.7.5) + - FMDB/standard (2.7.5) - image_gallery_saver (2.0.2): - Flutter - image_picker_ios (0.0.1): @@ -72,6 +75,9 @@ PODS: - FlutterMacOS - sign_in_with_apple (0.0.1): - Flutter + - sqflite (0.0.3): + - Flutter + - FMDB (>= 2.7.5) - super_native_extensions (0.0.1): - Flutter - SwiftyGif (5.4.3) @@ -99,6 +105,7 @@ DEPENDENCIES: - rich_clipboard_ios (from `.symlinks/plugins/rich_clipboard_ios/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`) + - sqflite (from `.symlinks/plugins/sqflite/ios`) - super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`) @@ -107,6 +114,7 @@ SPEC REPOS: trunk: - DKImagePickerController - DKPhotoGallery + - FMDB - ReachabilitySwift - SDWebImage - SwiftyGif @@ -147,6 +155,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" sign_in_with_apple: :path: ".symlinks/plugins/sign_in_with_apple/ios" + sqflite: + :path: ".symlinks/plugins/sqflite/ios" super_native_extensions: :path: ".symlinks/plugins/super_native_extensions/ios" url_launcher_ios: @@ -165,6 +175,7 @@ SPEC CHECKSUMS: flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c + FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5 integration_test: 13825b8a9334a850581300559b8839134b124670 @@ -176,6 +187,7 @@ SPEC CHECKSUMS: SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84 shared_preferences_foundation: e2dae3258e06f44cc55f49d42024fd8dd03c590c sign_in_with_apple: f3bf75217ea4c2c8b91823f225d70230119b8440 + sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7 SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 diff --git a/frontend/appflowy_flutter/ios/Runner/Info.plist b/frontend/appflowy_flutter/ios/Runner/Info.plist index 91ee44ca33c9..8c605b9d3a02 100644 --- a/frontend/appflowy_flutter/ios/Runner/Info.plist +++ b/frontend/appflowy_flutter/ios/Runner/Info.plist @@ -1,68 +1,70 @@ <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> -<dict> - <key>NSCameraUsageDescription</key> - <string>AppFlowy requires access to the camera.</string> - <key>NSPhotoLibraryUsageDescription</key> - <string>AppFlowy requires access to the photo library.</string> - <key>CADisableMinimumFrameDurationOnPhone</key> - <true/> - <key>CFBundleDevelopmentRegion</key> - <string>$(DEVELOPMENT_LANGUAGE)</string> - <key>CFBundleExecutable</key> - <string>$(EXECUTABLE_NAME)</string> - <key>CFBundleIdentifier</key> - <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> - <key>CFBundleInfoDictionaryVersion</key> - <string>6.0</string> - <key>CFBundleLocalizations</key> - <array> - <string>en</string> - </array> - <key>CFBundleName</key> - <string>AppFlowy</string> - <key>CFBundlePackageType</key> - <string>APPL</string> - <key>CFBundleShortVersionString</key> - <string>$(FLUTTER_BUILD_NAME)</string> - <key>CFBundleSignature</key> - <string>????</string> - <key>CFBundleURLTypes</key> - <array> - <dict> - <key>CFBundleURLName</key> - <string></string> - <key>CFBundleURLSchemes</key> - <array> - <string>appflowy-flutter</string> - </array> - </dict> - </array> - <key>CFBundleVersion</key> - <string>$(FLUTTER_BUILD_NUMBER)</string> - <key>LSRequiresIPhoneOS</key> - <true/> - <key>UIApplicationSupportsIndirectInputEvents</key> - <true/> - <key>UILaunchStoryboardName</key> - <string>LaunchScreen</string> - <key>UIMainStoryboardFile</key> - <string>Main</string> - <key>UISupportedInterfaceOrientations</key> - <array> - <string>UIInterfaceOrientationPortrait</string> - <string>UIInterfaceOrientationLandscapeLeft</string> - <string>UIInterfaceOrientationLandscapeRight</string> - </array> - <key>UISupportedInterfaceOrientations~ipad</key> - <array> - <string>UIInterfaceOrientationPortrait</string> - <string>UIInterfaceOrientationPortraitUpsideDown</string> - <string>UIInterfaceOrientationLandscapeLeft</string> - <string>UIInterfaceOrientationLandscapeRight</string> - </array> - <key>UIViewControllerBasedStatusBarAppearance</key> - <false/> -</dict> -</plist> + <dict> + <key>NSCameraUsageDescription</key> + <string>AppFlowy requires access to the camera.</string> + <key>NSPhotoLibraryUsageDescription</key> + <string>AppFlowy requires access to the photo library.</string> + <key>CADisableMinimumFrameDurationOnPhone</key> + <true /> + <key>CFBundleDevelopmentRegion</key> + <string>$(DEVELOPMENT_LANGUAGE)</string> + <key>CFBundleExecutable</key> + <string>$(EXECUTABLE_NAME)</string> + <key>CFBundleIdentifier</key> + <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleLocalizations</key> + <array> + <string>en</string> + </array> + <key>FLTEnableImpeller</key> + <false /> + <key>CFBundleName</key> + <string>AppFlowy</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleShortVersionString</key> + <string>$(FLUTTER_BUILD_NAME)</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleURLTypes</key> + <array> + <dict> + <key>CFBundleURLName</key> + <string></string> + <key>CFBundleURLSchemes</key> + <array> + <string>appflowy-flutter</string> + </array> + </dict> + </array> + <key>CFBundleVersion</key> + <string>$(FLUTTER_BUILD_NUMBER)</string> + <key>LSRequiresIPhoneOS</key> + <true /> + <key>UIApplicationSupportsIndirectInputEvents</key> + <true /> + <key>UILaunchStoryboardName</key> + <string>LaunchScreen</string> + <key>UIMainStoryboardFile</key> + <string>Main</string> + <key>UISupportedInterfaceOrientations</key> + <array> + <string>UIInterfaceOrientationPortrait</string> + <string>UIInterfaceOrientationLandscapeLeft</string> + <string>UIInterfaceOrientationLandscapeRight</string> + </array> + <key>UISupportedInterfaceOrientations~ipad</key> + <array> + <string>UIInterfaceOrientationPortrait</string> + <string>UIInterfaceOrientationPortraitUpsideDown</string> + <string>UIInterfaceOrientationLandscapeLeft</string> + <string>UIInterfaceOrientationLandscapeRight</string> + </array> + <key>UIViewControllerBasedStatusBarAppearance</key> + <false /> + </dict> +</plist> \ No newline at end of file diff --git a/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart b/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart index c32463c7d8b6..2a8a4ef75f59 100644 --- a/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart +++ b/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart @@ -2,12 +2,22 @@ import 'package:appflowy/mobile/presentation/database/mobile_board_screen.dart'; import 'package:appflowy/mobile/presentation/database/mobile_calendar_screen.dart'; import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart'; import 'package:appflowy/mobile/presentation/presentation.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +class MobileRouterRecord { + PropertyValueNotifier<String> lastPushedRouter = + PropertyValueNotifier<String>(''); +} + extension MobileRouter on BuildContext { Future<void> pushView(ViewPB view) async { + await FolderEventSetLatestView(ViewIdPB(value: view.id)).send(); + getIt<MobileRouterRecord>().lastPushedRouter.value = view.routeName; push( Uri( path: view.routeName, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart index 99fef7559eb8..77a975744fee 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart @@ -117,15 +117,19 @@ class _MobileViewPageState extends State<MobileViewPage> { appBar: AppBar( titleSpacing: 0, title: Row( + mainAxisSize: MainAxisSize.min, children: [ if (icon != null) FlowyText( '$icon ', fontSize: 22.0, ), - FlowyText.regular( - view?.name ?? widget.title ?? '', - fontSize: 14.0, + Expanded( + child: FlowyText.regular( + view?.name ?? widget.title ?? '', + fontSize: 14.0, + overflow: TextOverflow.ellipsis, + ), ), ], ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart index 87da5a6b3c90..ec54775d6217 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart @@ -2,7 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/home/mobile_folders.dart'; import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart'; -import 'package:appflowy/mobile/presentation/home/mobile_home_page_recent_files.dart'; +import 'package:appflowy/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart'; @@ -97,8 +97,7 @@ class MobileHomePage extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ // Recent files - const MobileHomePageRecentFilesWidget(), - const Divider(), + const MobileRecentFolder(), // Folders Padding( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_recent_files.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_recent_files.dart deleted file mode 100644 index 010e8d30a96a..000000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_recent_files.dart +++ /dev/null @@ -1,122 +0,0 @@ -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -// TODO(yijing): replace by real data later -class MockRecentFile { - MockRecentFile({ - required this.title, - }); - final String title; - final String icon = '🐼'; - - final image = Image.asset( - 'assets/images/app_flowy_abstract_cover_1.jpg', - fit: BoxFit.cover, - ); -} - -final recentFilesList = <MockRecentFile>[ - MockRecentFile(title: 'Work out plan'), - MockRecentFile(title: 'Travel plan'), - MockRecentFile(title: 'Meeting notes'), - MockRecentFile(title: 'Recipes'), - MockRecentFile(title: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'), -]; - -class MobileHomePageRecentFilesWidget extends StatelessWidget { - const MobileHomePageRecentFilesWidget({super.key}); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - // TODO: implement the details later. - return SizedBox( - height: 168, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Padding( - padding: EdgeInsets.symmetric(horizontal: 16), - child: FlowyText.semibold( - 'Recent', - fontSize: 20.0, - ), - ), - Expanded( - child: ListView.separated( - separatorBuilder: (context, index) => const HSpace(8), - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - scrollDirection: Axis.horizontal, - itemCount: recentFilesList.length, - itemBuilder: (context, index) { - return Container( - width: 120, - decoration: BoxDecoration( - color: theme.colorScheme.background, - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: theme.colorScheme.outline.withOpacity(0.5), - ), - ), - child: Stack( - children: [ - Align( - alignment: Alignment.topCenter, - child: SizedBox( - height: 60, - width: double.infinity, - child: ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(8), - topRight: Radius.circular(8), - ), - child: recentFilesList[index].image, - ), - ), - ), - Align( - alignment: Alignment.centerLeft, - child: Container( - height: 32, - width: 32, - margin: const EdgeInsets.only(left: 8), - child: Text( - recentFilesList[index].icon, - style: const TextStyle(fontSize: 32), - ), - ), - ), - Align( - alignment: Alignment.bottomCenter, - child: Container( - height: 32, - width: double.infinity, - margin: const EdgeInsets.only( - left: 8, - right: 8, - bottom: 8, - ), - child: Text( - recentFilesList[index].title, - softWrap: true, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onBackground, - ), - maxLines: 2, - ), - ), - ), - ], - ), - ); - }, - ), - ), - ], - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart new file mode 100644 index 000000000000..32156f74f19a --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart @@ -0,0 +1,104 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/application/mobile_router.dart'; +import 'package:appflowy/mobile/presentation/home/recent_folder/mobile_recent_view.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart'; +import 'package:dartz/dartz.dart' hide State; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; + +class MobileRecentFolder extends StatefulWidget { + const MobileRecentFolder({super.key}); + + @override + State<MobileRecentFolder> createState() => _MobileRecentFolderState(); +} + +class _MobileRecentFolderState extends State<MobileRecentFolder> { + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: getIt<MobileRouterRecord>().lastPushedRouter, + builder: (context, value, child) { + return FutureBuilder<Either<RepeatedViewPB, FlowyError>>( + future: FolderEventReadRecentViews().send(), + builder: (context, snapshot) { + final recentViews = snapshot.data + ?.fold<List<ViewPB>>( + (l) => l.items, + (r) => [], + ) + // only keep the first 10 items. + .reversed + .take(10) + .toList(); + + if (recentViews == null || recentViews.isEmpty) { + return const SizedBox.shrink(); + } + + return Column( + children: [ + _RecentViews( + key: ValueKey(recentViews), + // the recent views are in reverse order + recentViews: recentViews, + ), + const VSpace(12.0) + ], + ); + }, + ); + }, + ); + } +} + +class _RecentViews extends StatelessWidget { + const _RecentViews({ + super.key, + required this.recentViews, + }); + + final List<ViewPB> recentViews; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 168, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: FlowyText.semibold( + LocaleKeys.sideBar_recent.tr(), + fontSize: 20.0, + ), + ), + Expanded( + child: ListView.separated( + separatorBuilder: (context, index) => const HSpace(8), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + scrollDirection: Axis.horizontal, + itemCount: recentViews.length, + itemBuilder: (context, index) { + return MobileRecentView( + view: recentViews[index], + height: 120, + ); + }, + ), + ), + ], + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart new file mode 100644 index 000000000000..de67e5b7dc1c --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart @@ -0,0 +1,208 @@ +import 'dart:io'; + +import 'package:appflowy/mobile/application/mobile_router.dart'; +import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy/workspace/application/doc/doc_listener.dart'; +import 'package:appflowy/workspace/application/view/prelude.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter/material.dart'; +import 'package:string_validator/string_validator.dart'; + +class MobileRecentView extends StatefulWidget { + const MobileRecentView({ + super.key, + required this.view, + required this.height, + }); + + final ViewPB view; + final double height; + + @override + State<MobileRecentView> createState() => _MobileRecentViewState(); +} + +class _MobileRecentViewState extends State<MobileRecentView> { + late final ViewListener viewListener; + late ViewPB view; + late final DocumentListener documentListener; + + @override + void initState() { + super.initState(); + + view = widget.view; + + viewListener = ViewListener( + viewId: view.id, + )..start( + onViewUpdated: (view) { + setState(() { + this.view = view; + }); + }, + ); + + documentListener = DocumentListener(id: view.id) + ..start( + didReceiveUpdate: (document) { + setState(() { + view = view; + }); + }, + ); + } + + @override + void dispose() { + viewListener.stop(); + documentListener.stop(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final icon = view.icon.value; + final theme = Theme.of(context); + + return GestureDetector( + onTap: () => context.pushView(view), + child: Container( + height: widget.height, + width: widget.height, + decoration: BoxDecoration( + color: theme.colorScheme.background, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: theme.colorScheme.outline.withOpacity(0.5), + ), + ), + child: Stack( + children: [ + Positioned( + top: 0, + left: 0, + right: 0, + child: SizedBox( + height: widget.height / 2.0, + width: double.infinity, + child: ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(8), + topRight: Radius.circular(8), + ), + child: _buildCoverWidget(), + ), + ), + ), + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.only(left: 4), + child: icon.isNotEmpty + ? FlowyText( + icon, + fontSize: 30.0, + ) + : SizedBox.square( + dimension: 32.0, + child: view.defaultIcon(), + ), + ), + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + height: widget.height / 2.0, + width: double.infinity, + padding: const EdgeInsets.only( + left: 8.0, + top: 14.0, + right: 8.0, + ), + child: FlowyText( + view.name, + maxLines: 2, + fontSize: 16.0, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildCoverWidget() { + return FutureBuilder<Node?>( + future: _getPageNode(), + builder: ((context, snapshot) { + final node = snapshot.data; + final placeholder = Container( + color: Theme.of(context).colorScheme.onSecondaryContainer, + ); + if (node == null) { + return placeholder; + } + final type = CoverType.fromString( + node.attributes[DocumentHeaderBlockKeys.coverType], + ); + final cover = + node.attributes[DocumentHeaderBlockKeys.coverDetails] as String?; + if (cover == null) { + return placeholder; + } + switch (type) { + case CoverType.file: + if (isURL(cover)) { + return CachedNetworkImage( + imageUrl: cover, + fit: BoxFit.cover, + ); + } + final imageFile = File(cover); + if (!imageFile.existsSync()) { + return placeholder; + } + return Image.file( + imageFile, + ); + case CoverType.asset: + return Image.asset( + cover, + fit: BoxFit.cover, + ); + case CoverType.color: + final color = cover.tryToColor() ?? Colors.white; + return Container( + color: color, + ); + case CoverType.none: + return placeholder; + } + }), + ); + } + + Future<Node?> _getPageNode() async { + final data = await DocumentEventGetDocumentData( + OpenDocumentPayloadPB(documentId: view.id), + ).send(); + final document = data.fold((l) => l.toDocument(), (r) => null); + if (document != null) { + return document.root; + } + return null; + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart index 58d13ce921a6..b71a664eb585 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart @@ -8,6 +8,7 @@ Future<T?> showFlowyMobileBottomSheet<T>( }) async { return showModalBottomSheet( context: context, + isScrollControlled: true, builder: (context) => Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), child: Column( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor.dart index 5e17a753fda1..919451adac3f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor.dart @@ -529,14 +529,12 @@ class ColorItem extends StatelessWidget { @override Widget build(BuildContext context) { - return InkWell( - customBorder: const RoundedRectangleBorder( - borderRadius: Corners.s6Border, - ), - hoverColor: hoverColor, - onTap: () => onTap(option.colorHex), - child: Padding( - padding: const EdgeInsets.only(right: 10.0), + return Padding( + padding: const EdgeInsets.only(right: 10.0), + child: InkWell( + customBorder: const CircleBorder(), + hoverColor: hoverColor, + onTap: () => onTap(option.colorHex), child: SizedBox.square( dimension: 25, child: DecoratedBox( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart index 59355d25c19c..2905a3271523 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart @@ -2,19 +2,23 @@ import 'dart:io'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart'; import 'package:appflowy/plugins/base/icon/icon_picker.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy/workspace/application/view/view_listener.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide UploadImageMenu; import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:cached_network_image/cached_network_image.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/rounded_button.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:string_validator/string_validator.dart'; import 'cover_editor.dart'; @@ -262,7 +266,9 @@ class _DocumentHeaderToolbarState extends State<DocumentHeaderToolbar> { FlowyButton( leftIconSize: const Size.square(18), onTap: () => widget.onCoverChanged( - cover: (CoverType.asset, builtInAssetImages.first), + cover: PlatformExtension.isDesktopOrWeb + ? (CoverType.asset, builtInAssetImages.first) + : (CoverType.color, '0xffe8e0ff'), ), useIntrinsicWidth: true, leftIcon: const FlowySvg(FlowySvgs.image_s), @@ -373,6 +379,12 @@ class DocumentCoverState extends State<DocumentCover> { @override Widget build(BuildContext context) { + return PlatformExtension.isDesktopOrWeb + ? _buildDesktopCover() + : _buildMobileCover(); + } + + Widget _buildDesktopCover() { return SizedBox( height: kCoverHeight, child: MouseRegion( @@ -393,10 +405,82 @@ class DocumentCoverState extends State<DocumentCover> { ); } + Widget _buildMobileCover() { + return SizedBox( + height: kCoverHeight, + child: Stack( + children: [ + SizedBox( + height: double.infinity, + width: double.infinity, + child: _buildCoverImage(), + ), + Positioned( + bottom: 8, + right: 12, + child: RoundedTextButton( + onPressed: () { + showFlowyMobileBottomSheet( + context, + title: LocaleKeys.document_plugins_cover_changeCover.tr(), + builder: (context) { + return ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 340, + minHeight: 80, + ), + child: UploadImageMenu( + supportTypes: const [ + UploadImageType.color, + UploadImageType.local, + UploadImageType.url, + UploadImageType.unsplash, + ], + onSelectedLocalImage: (path) async { + context.pop(); + widget.onCoverChanged(CoverType.file, path); + }, + onSelectedAIImage: (_) { + throw UnimplementedError(); + }, + onSelectedNetworkImage: (url) async { + context.pop(); + widget.onCoverChanged(CoverType.file, url); + }, + onSelectedColor: (color) { + context.pop(); + widget.onCoverChanged(CoverType.color, color); + }, + ), + ); + }, + ); + }, + fillColor: Theme.of(context).colorScheme.onSurfaceVariant, + width: 120, + height: 32, + title: LocaleKeys.document_plugins_cover_changeCover.tr(), + ), + ), + ], + ), + ); + } + Widget _buildCoverImage() { + final detail = widget.coverDetails; + if (detail == null) { + return const SizedBox.shrink(); + } switch (widget.coverType) { case CoverType.file: - final imageFile = File(widget.coverDetails ?? ""); + if (isURL(detail)) { + return CachedNetworkImage( + imageUrl: detail, + fit: BoxFit.cover, + ); + } + final imageFile = File(detail); if (!imageFile.existsSync()) { WidgetsBinding.instance.addPostFrameCallback((_) { widget.onCoverChanged(CoverType.none, null); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/flowy_image_picker.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/flowy_image_picker.dart new file mode 100644 index 000000000000..a1f4487562a1 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/flowy_image_picker.dart @@ -0,0 +1,41 @@ +import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class ImagePickerPage extends StatefulWidget { + const ImagePickerPage({ + super.key, + // required this.onSelected, + }); + + // final void Function(EmojiPickerResult) onSelected; + + @override + State<ImagePickerPage> createState() => _ImagePickerPageState(); +} + +class _ImagePickerPageState extends State<ImagePickerPage> { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + titleSpacing: 0, + title: const FlowyText.semibold( + 'Page icon', + fontSize: 14.0, + ), + leading: AppBarBackButton( + onTap: () => context.pop(), + ), + ), + body: SafeArea( + child: UploadImageMenu( + onSubmitted: (_) {}, + onUpload: (_) {}, + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_picker_screen.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_picker_screen.dart new file mode 100644 index 000000000000..0aa10412bfb8 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_picker_screen.dart @@ -0,0 +1,15 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/image/flowy_image_picker.dart'; +import 'package:flutter/material.dart'; + +class MobileImagePickerScreen extends StatelessWidget { + static const routeName = '/image_picker'; + + const MobileImagePickerScreen({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return const ImagePickerPage(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart index e7a9bfc7de68..961e2deab6a5 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart @@ -1,12 +1,15 @@ import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/embed_image_url_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/open_ai_image_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/stability_ai_image_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_file_widget.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/user/application/user_service.dart'; -import 'package:appflowy/util/platform_extension.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide ColorOption; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flutter/material.dart'; @@ -16,7 +19,8 @@ enum UploadImageType { url, unsplash, stabilityAI, - openAI; + openAI, + color; String get description { switch (this) { @@ -30,6 +34,8 @@ enum UploadImageType { return LocaleKeys.document_imageBlock_ai_label.tr(); case UploadImageType.stabilityAI: return LocaleKeys.document_imageBlock_stability_ai_label.tr(); + case UploadImageType.color: + return LocaleKeys.document_plugins_cover_colors.tr(); } } } @@ -40,12 +46,14 @@ class UploadImageMenu extends StatefulWidget { required this.onSelectedLocalImage, required this.onSelectedAIImage, required this.onSelectedNetworkImage, + this.onSelectedColor, this.supportTypes = UploadImageType.values, }); final void Function(String? path) onSelectedLocalImage; final void Function(String url) onSelectedAIImage; final void Function(String url) onSelectedNetworkImage; + final void Function(String color)? onSelectedColor; final List<UploadImageType> supportTypes; @override @@ -128,18 +136,23 @@ class _UploadImageMenuState extends State<UploadImageMenu> { } Widget _buildTab() { - final type = UploadImageType.values[currentTabIndex]; + final constraints = + PlatformExtension.isMobile ? const BoxConstraints(minHeight: 92) : null; + final type = values[currentTabIndex]; switch (type) { case UploadImageType.local: - return Padding( + return Container( padding: const EdgeInsets.all(8.0), + alignment: Alignment.center, + constraints: constraints, child: UploadImageFileWidget( onPickFile: widget.onSelectedLocalImage, ), ); case UploadImageType.url: - return Padding( + return Container( padding: const EdgeInsets.all(8.0), + constraints: constraints, child: EmbedImageUrlWidget( onSubmit: widget.onSelectedNetworkImage, ), @@ -156,8 +169,9 @@ class _UploadImageMenuState extends State<UploadImageMenu> { case UploadImageType.openAI: return supportOpenAI ? Expanded( - child: Padding( + child: Container( padding: const EdgeInsets.all(8.0), + constraints: constraints, child: OpenAIImageWidget( onSelectNetworkImage: widget.onSelectedAIImage, ), @@ -172,7 +186,7 @@ class _UploadImageMenuState extends State<UploadImageMenu> { case UploadImageType.stabilityAI: return supportStabilityAI ? Expanded( - child: Padding( + child: Container( padding: const EdgeInsets.all(8.0), child: StabilityAIImageWidget( onSelectImage: widget.onSelectedLocalImage, @@ -186,6 +200,28 @@ class _UploadImageMenuState extends State<UploadImageMenu> { .tr(), ), ); + case UploadImageType.color: + final theme = Theme.of(context); + return Container( + constraints: constraints, + padding: const EdgeInsets.all(8.0), + alignment: Alignment.center, + child: CoverColorPicker( + pickerBackgroundColor: theme.cardColor, + pickerItemHoverColor: theme.hoverColor, + backgroundColorOptions: FlowyTint.values + .map<ColorOption>( + (t) => ColorOption( + colorHex: t.color(context).toHex(), + name: t.tintName(AppFlowyEditorL10n.current), + ), + ) + .toList(), + onSubmittedBackgroundColorHex: (color) { + widget.onSelectedColor?.call(color); + }, + ), + ); } } } diff --git a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart index a8d8bd8faae4..acc04b3f0d9b 100644 --- a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart +++ b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart @@ -1,6 +1,7 @@ import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/network_monitor.dart'; import 'package:appflowy/env/env.dart'; +import 'package:appflowy/mobile/application/mobile_router.dart'; import 'package:appflowy/plugins/document/application/prelude.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart'; @@ -143,6 +144,7 @@ void _resolveHomeDeps(GetIt getIt) { getIt.registerSingleton(FToast()); getIt.registerSingleton(MenuSharedState()); + getIt.registerSingleton(MobileRouterRecord()); getIt.registerFactoryParam<UserListener, UserProfilePB, void>( (user, _) => UserListener(userProfile: user), diff --git a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart index bdb70ab5c498..ea272371294f 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart @@ -4,6 +4,7 @@ import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart'; import 'package:appflowy/mobile/presentation/favorite/mobile_favorite_page.dart'; import 'package:appflowy/mobile/presentation/presentation.dart'; import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_picker_screen.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/tasks/app_widget.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; @@ -51,6 +52,7 @@ GoRouter generateRouter(Widget child) { // emoji picker _mobileEmojiPickerPageRoute(), + _mobileImagePickerPageRoute(), ], // Desktop and Mobile @@ -216,6 +218,18 @@ GoRoute _mobileEmojiPickerPageRoute() { ); } +GoRoute _mobileImagePickerPageRoute() { + return GoRoute( + parentNavigatorKey: AppGlobals.rootNavKey, + path: MobileImagePickerScreen.routeName, + pageBuilder: (context, state) { + return const MaterialPage( + child: MobileImagePickerScreen(), + ); + }, + ); +} + GoRoute _desktopHomeScreenRoute() { return GoRoute( path: DesktopHomeScreen.routeName, diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 7c005d9ab54b..0be54f2fa729 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -178,6 +178,30 @@ packages: url: "https://pub.dev" source: hosted version: "8.6.0" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: f98972704692ba679db144261172a8e20feb145636c617af0eb4022132a6797f + url: "https://pub.dev" + source: hosted + version: "3.3.0" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "56aa42a7a01e3c9db8456d9f3f999931f1e05535b5a424271e9a38cabf066613" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "759b9a9f8f6ccbb66c185df805fac107f05730b1dab9c64626d1008cca532257" + url: "https://pub.dev" + source: hosted + version: "1.1.0" calendar_view: dependency: "direct main" description: @@ -539,6 +563,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.3" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba" + url: "https://pub.dev" + source: hosted + version: "3.3.1" flutter_colorpicker: dependency: "direct main" description: @@ -1082,6 +1114,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" + url: "https://pub.dev" + source: hosted + version: "2.0.0" package_config: dependency: transitive description: @@ -1575,6 +1615,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "8ed044102f3135add97be8653662052838859f5400075ef227f8ad72ae320803" + url: "https://pub.dev" + source: hosted + version: "2.5.0+1" stack_trace: dependency: transitive description: @@ -1672,6 +1728,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" + url: "https://pub.dev" + source: hosted + version: "3.1.0" table_calendar: dependency: "direct main" description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index ab901ec6f18d..8e9eb9aecff2 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -124,6 +124,7 @@ dependencies: flutter_slidable: ^3.0.0 image_picker: ^1.0.4 image_gallery_saver: ^2.0.3 + cached_network_image: ^3.3.0 dev_dependencies: flutter_lints: ^2.0.1 diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 41a9e9a713ec..95d30f92d2e9 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -186,7 +186,8 @@ "favorites": "Favorites", "clickToHidePersonal": "Click to hide personal section", "clickToHideFavorites": "Click to hide favorite section", - "addAPage": "Add a page" + "addAPage": "Add a page", + "recent": "Recent" }, "notifications": { "export": { diff --git a/frontend/rust-lib/flowy-folder2/src/event_handler.rs b/frontend/rust-lib/flowy-folder2/src/event_handler.rs index d826c202a3ba..4ab5398a1e75 100644 --- a/frontend/rust-lib/flowy-folder2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-folder2/src/event_handler.rs @@ -228,6 +228,22 @@ pub(crate) async fn read_favorites_handler( } data_result_ok(RepeatedViewPB { items: views }) } + +#[tracing::instrument(level = "debug", skip(folder), err)] +pub(crate) async fn read_recent_views_handler( + folder: AFPluginState<Weak<FolderManager>>, +) -> DataResult<RepeatedViewPB, FlowyError> { + let folder = upgrade_folder(folder)?; + let recent_items = folder.get_all_recent_sections().await; + let mut views = vec![]; + for item in recent_items { + if let Ok(view) = folder.get_view_pb(&item.id).await { + views.push(view); + } + } + data_result_ok(RepeatedViewPB { items: views }) +} + #[tracing::instrument(level = "debug", skip(folder), err)] pub(crate) async fn read_trash_handler( folder: AFPluginState<Weak<FolderManager>>, diff --git a/frontend/rust-lib/flowy-folder2/src/event_map.rs b/frontend/rust-lib/flowy-folder2/src/event_map.rs index e6d145c533ef..f27ff376c5c1 100644 --- a/frontend/rust-lib/flowy-folder2/src/event_map.rs +++ b/frontend/rust-lib/flowy-folder2/src/event_map.rs @@ -36,6 +36,7 @@ pub fn init(folder: Weak<FolderManager>) -> AFPlugin { .event(FolderEvent::GetFolderSnapshots, get_folder_snapshots_handler) .event(FolderEvent::UpdateViewIcon, update_view_icon_handler) .event(FolderEvent::ReadFavorites, read_favorites_handler) + .event(FolderEvent::ReadRecentViews, read_recent_views_handler) .event(FolderEvent::ToggleFavorite, toggle_favorites_handler) } @@ -145,4 +146,7 @@ pub enum FolderEvent { #[event(input = "UpdateViewIconPayloadPB")] UpdateViewIcon = 35, + + #[event(output = "RepeatedViewPB")] + ReadRecentViews = 36, } diff --git a/frontend/rust-lib/flowy-folder2/src/manager.rs b/frontend/rust-lib/flowy-folder2/src/manager.rs index 8762299a29ff..ea4c9e7a5fea 100644 --- a/frontend/rust-lib/flowy-folder2/src/manager.rs +++ b/frontend/rust-lib/flowy-folder2/src/manager.rs @@ -7,8 +7,8 @@ use collab::core::collab::{CollabRawData, MutexCollab}; use collab::core::collab_state::SyncState; use collab_entity::CollabType; use collab_folder::{ - Folder, FolderData, FolderNotify, SectionItem, TrashChange, TrashChangeReceiver, TrashInfo, - UserId, View, ViewChange, ViewChangeReceiver, ViewLayout, ViewUpdate, Workspace, + Folder, FolderData, FolderNotify, Section, SectionItem, TrashChange, TrashChangeReceiver, + TrashInfo, UserId, View, ViewChange, ViewChangeReceiver, ViewLayout, ViewUpdate, Workspace, }; use parking_lot::{Mutex, RwLock}; use tokio_stream::wrappers::WatchStream; @@ -745,6 +745,7 @@ impl FolderManager { || Err(FlowyError::record_not_found()), |folder| { folder.set_current_view(view_id); + folder.add_recent_view_ids(vec![view_id.to_string()]); Ok(folder.get_workspace_id()) }, )?; @@ -800,17 +801,12 @@ impl FolderManager { #[tracing::instrument(level = "trace", skip(self))] pub(crate) async fn get_all_favorites(&self) -> Vec<SectionItem> { - self.with_folder(Vec::new, |folder| { - let trash_ids = folder - .get_all_trash() - .into_iter() - .map(|trash| trash.id) - .collect::<Vec<String>>(); + self.get_sections(Section::Favorite) + } - let mut views = folder.get_all_favorites(); - views.retain(|view| !trash_ids.contains(&view.id)); - views - }) + #[tracing::instrument(level = "trace", skip(self))] + pub(crate) async fn get_all_recent_sections(&self) -> Vec<SectionItem> { + self.get_sections(Section::Recent) } #[tracing::instrument(level = "trace", skip(self))] @@ -1039,6 +1035,26 @@ impl FolderManager { pub fn get_cloud_service(&self) -> &Arc<dyn FolderCloudService> { &self.cloud_service } + + fn get_sections(&self, section_type: Section) -> Vec<SectionItem> { + self.with_folder(Vec::new, |folder| { + let trash_ids = folder + .get_all_trash() + .into_iter() + .map(|trash| trash.id) + .collect::<Vec<String>>(); + + let mut views = match section_type { + Section::Favorite => folder.get_all_favorites(), + Section::Recent => folder.get_all_recent_sections(), + _ => vec![], + }; + + // filter the views that are in the trash + views.retain(|view| !trash_ids.contains(&view.id)); + views + }) + } } /// Listen on the [ViewChange] after create/delete/update events happened From 251c6d22b2549b605e0080a10f5d217ee9299178 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" <lucas.xu@appflowy.io> Date: Mon, 13 Nov 2023 12:00:03 +0800 Subject: [PATCH 46/56] fix: 0.3.8 known issues (#3912) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: add a left padding to align the document and grid field * fix: emoji picker in the slash menu is too small * fix: replace the delete icon color with black * fix: improve snackbar background color * fix: cannot add new line after toggle list * feat: set ⭐ as the default icon of getting started * fix: the titlebar overflows when the title level is too deep * fix: integration test * fix: openAI hint text overflow * fix: integration tests --- .../document_with_cover_image_test.dart | 24 ++--- .../util/editor_test_operations.dart | 11 +- .../integration_test/util/expectation.dart | 2 +- .../bottom_sheet_action_widget.dart | 1 - .../plugins/base/emoji/emoji_skin_tone.dart | 102 ++++++++---------- .../widgets/row/row_document.dart | 2 +- .../widgets/auto_completion_node_widget.dart | 9 +- .../toggle/toggle_block_shortcut_event.dart | 7 +- .../workspace/application/view/view_ext.dart | 10 +- .../home/menu/sidebar/sidebar_folder.dart | 7 ++ .../workspace/presentation/home/toast.dart | 2 +- .../widgets/emoji_picker/emoji_menu_item.dart | 4 +- .../presentation/widgets/view_title_bar.dart | 74 ++++++++++--- .../lib/style_widget/text_field.dart | 15 ++- .../src/deps_resolve/folder_deps.rs | 7 +- .../flowy-folder2/src/view_operation.rs | 8 ++ 16 files changed, 178 insertions(+), 107 deletions(-) diff --git a/frontend/appflowy_flutter/integration_test/document/document_with_cover_image_test.dart b/frontend/appflowy_flutter/integration_test/document/document_with_cover_image_test.dart index b2a8d2a69047..b656fc116745 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_with_cover_image_test.dart +++ b/frontend/appflowy_flutter/integration_test/document/document_with_cover_image_test.dart @@ -22,7 +22,6 @@ void main() { // Hover over cover toolbar to show 'Add Cover' and 'Add Icon' buttons await tester.editor.hoverOnCoverToolbar(); - tester.expectToSeePluginAddCoverAndIconButton(); // Insert a document cover await tester.editor.tapOnAddCover(); @@ -58,14 +57,10 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - tester.expectToSeeDocumentIcon(null); - - // Hover over cover toolbar to show the 'Add Cover' and 'Add Icon' buttons - await tester.editor.hoverOnCoverToolbar(); - tester.expectToSeePluginAddCoverAndIconButton(); + tester.expectToSeeDocumentIcon('⭐️'); // Insert a document icon - await tester.editor.tapAddIconButton(); + await tester.editor.tapGettingStartedIcon(); await tester.tapEmoji('😀'); tester.expectToSeeDocumentIcon('😀'); @@ -95,18 +90,15 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - tester.expectToSeeDocumentIcon(null); + tester.expectToSeeDocumentIcon('⭐️'); tester.expectToSeeNoDocumentCover(); - // Hover over cover toolbar to show the 'Add Cover' and 'Add Icon' buttons - await tester.editor.hoverOnCoverToolbar(); - tester.expectToSeePluginAddCoverAndIconButton(); - // Insert a document icon - await tester.editor.tapAddIconButton(); + await tester.editor.tapGettingStartedIcon(); await tester.tapEmoji('😀'); // Insert a document cover + await tester.editor.hoverOnCoverToolbar(); await tester.editor.tapOnAddCover(); // Expect to see the icon and cover at the same time @@ -122,8 +114,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await tester.editor.hoverOnCoverToolbar(); - await tester.editor.tapAddIconButton(); + await tester.editor.tapGettingStartedIcon(); // click the shuffle button await tester.tapButton( @@ -136,8 +127,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await tester.editor.hoverOnCoverToolbar(); - await tester.editor.tapAddIconButton(); + await tester.editor.tapGettingStartedIcon(); final searchEmojiTextField = find.byWidgetPredicate( (widget) => diff --git a/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart b/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart index 7236a4b94bbc..7f089d518a7d 100644 --- a/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart +++ b/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart @@ -60,6 +60,15 @@ class EditorOperations { expect(find.byType(FlowyEmojiPicker), findsOneWidget); } + Future<void> tapGettingStartedIcon() async { + await tester.tapButton( + find.descendant( + of: find.byType(DocumentHeaderNodeWidget), + matching: find.findTextInFlowyText('⭐️'), + ), + ); + } + /// Taps on the 'Skin tone' button /// /// Must call [tapAddIconButton] first. @@ -67,7 +76,7 @@ class EditorOperations { await tester.tapButton( find.byTooltip(LocaleKeys.emoji_selectSkinTone.tr()), ); - final skinToneButton = find.text(EmojiSkinToneWrapper(skinTone).name); + final skinToneButton = find.byKey(emojiSkinToneKey(skinTone.icon)); await tester.tapButton(skinToneButton); } diff --git a/frontend/appflowy_flutter/integration_test/util/expectation.dart b/frontend/appflowy_flutter/integration_test/util/expectation.dart index 582cd85ae3de..b808ec7b841a 100644 --- a/frontend/appflowy_flutter/integration_test/util/expectation.dart +++ b/frontend/appflowy_flutter/integration_test/util/expectation.dart @@ -12,7 +12,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter_test/flutter_test.dart'; // const String readme = 'Read me'; -const String gettingStarted = '⭐️ Getting started'; +const String gettingStarted = 'Getting started'; extension Expectation on WidgetTester { /// Expect to see the home page and with a default read me page. diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart index 2bc843cc0a44..b4aa9deee101 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart @@ -24,7 +24,6 @@ class BottomSheetActionWidget extends StatelessWidget { icon: FlowySvg( svg, size: const Size.square(22.0), - blendMode: BlendMode.dst, color: iconColor, ), label: Text(text), diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_skin_tone.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_skin_tone.dart index eebc73ed0536..a3f934ac898a 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_skin_tone.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_skin_tone.dart @@ -1,5 +1,4 @@ import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:emoji_mart/emoji_mart.dart'; @@ -10,6 +9,11 @@ import 'package:flutter/material.dart'; // use a temporary global value to store last selected skin tone EmojiSkinTone? lastSelectedEmojiSkinTone; +@visibleForTesting +ValueKey emojiSkinToneKey(String icon) { + return ValueKey('emoji_skin_tone_$icon'); +} + class FlowyEmojiSkinToneSelector extends StatefulWidget { const FlowyEmojiSkinToneSelector({ super.key, @@ -26,73 +30,59 @@ class FlowyEmojiSkinToneSelector extends StatefulWidget { class _FlowyEmojiSkinToneSelectorState extends State<FlowyEmojiSkinToneSelector> { EmojiSkinTone skinTone = EmojiSkinTone.none; + final controller = PopoverController(); @override Widget build(BuildContext context) { - return PopoverActionList<EmojiSkinToneWrapper>( + return AppFlowyPopover( direction: PopoverDirection.bottomWithCenterAligned, - offset: const Offset(0, 8), - actions: EmojiSkinTone.values - .map((action) => EmojiSkinToneWrapper(action)) - .toList(), - buildChild: (controller) { - return FlowyTooltip( - message: LocaleKeys.emoji_selectSkinTone.tr(), - child: FlowyIconButton( - icon: Padding( - // add a left padding to align the emoji center - padding: const EdgeInsets.only( - left: 3.0, - ), - child: FlowyText( - lastSelectedEmojiSkinTone?.icon ?? '✋', - fontSize: 22.0, - ), - ), - onPressed: () => controller.show(), - ), + controller: controller, + popupBuilder: (context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: EmojiSkinTone.values + .map( + (e) => _buildIconButton( + e.icon, + () { + setState(() => lastSelectedEmojiSkinTone = e); + widget.onEmojiSkinToneChanged(e); + controller.close(); + }, + ), + ) + .toList(), ); }, - onSelected: (action, controller) async { - widget.onEmojiSkinToneChanged(action.inner); - setState(() { - lastSelectedEmojiSkinTone = action.inner; - }); - controller.close(); - }, + child: FlowyTooltip( + message: LocaleKeys.emoji_selectSkinTone.tr(), + child: _buildIconButton( + lastSelectedEmojiSkinTone?.icon ?? '✋', + () => controller.show(), + ), + ), ); } -} -class EmojiSkinToneWrapper extends ActionCell { - EmojiSkinToneWrapper(this.inner); - - final EmojiSkinTone inner; - - Widget? icon(Color iconColor) => null; - - @override - String get name { - final String i18n; - switch (inner) { - case EmojiSkinTone.none: - i18n = LocaleKeys.emoji_skinTone_default.tr(); - case EmojiSkinTone.light: - i18n = LocaleKeys.emoji_skinTone_light.tr(); - case EmojiSkinTone.mediumLight: - i18n = LocaleKeys.emoji_skinTone_mediumLight.tr(); - case EmojiSkinTone.medium: - i18n = LocaleKeys.emoji_skinTone_medium.tr(); - case EmojiSkinTone.mediumDark: - i18n = LocaleKeys.emoji_skinTone_mediumDark.tr(); - case EmojiSkinTone.dark: - i18n = LocaleKeys.emoji_skinTone_dark.tr(); - } - return '${inner.icon} $i18n'; + Widget _buildIconButton(String icon, VoidCallback onPressed) { + return FlowyIconButton( + key: emojiSkinToneKey(icon), + icon: Padding( + // add a left padding to align the emoji center + padding: const EdgeInsets.only( + left: 3.0, + ), + child: FlowyText( + icon, + fontSize: 22.0, + ), + ), + onPressed: onPressed, + ); } } -extension on EmojiSkinTone { +extension EmojiSkinToneIcon on EmojiSkinTone { String get icon { switch (this) { case EmojiSkinTone.none: diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_document.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_document.dart index 795c6c6818ae..6a529a3976d3 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_document.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_document.dart @@ -123,7 +123,7 @@ class _RowEditorState extends State<RowEditor> { scrollController: widget.scrollController, styleCustomizer: EditorStyleCustomizer( context: context, - padding: const EdgeInsets.symmetric(horizontal: 10), + padding: const EdgeInsets.only(left: 16, right: 54), ), showParagraphPlaceholder: (editorState, node) => editorState.document.isEmpty, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/auto_completion_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/auto_completion_node_widget.dart index 2541e948152f..121cb0776d91 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/auto_completion_node_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/auto_completion_node_widget.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/build_context_extension.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/text_robot.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart'; @@ -7,6 +8,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/wid import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text_field.dart'; @@ -15,8 +17,6 @@ import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:provider/provider.dart'; class AutoCompletionBlockKeys { @@ -169,9 +169,12 @@ class _AutoCompletionBlockComponentState return FlowyTextField( hintText: LocaleKeys.document_plugins_autoGeneratorHintText.tr(), controller: controller, - maxLines: 3, + maxLines: 5, focusNode: textFieldFocusNode, autoFocus: false, + hintTextConstraints: const BoxConstraints( + maxHeight: double.infinity, + ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcut_event.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcut_event.dart index 8e9ad2305b5b..6428e0dccf32 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcut_event.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcut_event.dart @@ -70,9 +70,12 @@ CharacterShortcutEvent insertChildNodeInsideToggleList = CharacterShortcutEvent( // insert a toggle list block below the current toggle list block transaction ..deleteText(node, selection.startIndex, slicedDelta.length) - ..insertNode( + ..insertNodes( selection.start.path.next, - toggleListBlockNode(collapsed: true, delta: slicedDelta), + [ + toggleListBlockNode(collapsed: true, delta: slicedDelta), + paragraphNode(), + ], ) ..afterSelection = Selection.collapsed( Position(path: selection.start.path.next, offset: 0), diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart index 4b8a15590148..35a7fe310b2a 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart @@ -106,16 +106,20 @@ extension ViewExtension on ViewPB { FlowySvgData get iconData => layout.icon; - Future<List<ViewPB>> getAncestors({bool includeSelf = false}) async { + Future<List<ViewPB>> getAncestors({ + bool includeSelf = false, + bool includeRoot = false, + }) async { final ancestors = <ViewPB>[]; if (includeSelf) { - ancestors.add(this); + final self = await ViewBackendService.getView(id); + ancestors.add(self.getLeftOrNull<ViewPB>() ?? this); } var parent = await ViewBackendService.getView(parentViewId); while (parent.isLeft()) { // parent is not null final view = parent.getLeftOrNull<ViewPB>(); - if (view == null) { + if (view == null || (!includeRoot && view.parentViewId.isEmpty)) { break; } ancestors.add(view); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_folder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_folder.dart index 74590d790dcf..bdc926ba5223 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_folder.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_folder.dart @@ -18,6 +18,12 @@ class SidebarFolder extends StatelessWidget { @override Widget build(BuildContext context) { + // check if there is any duplicate views + final views = this.views.toSet().toList(); + final favoriteViews = this.favoriteViews.toSet().toList(); + assert(views.length == this.views.length); + assert(favoriteViews.length == favoriteViews.length); + return ValueListenableBuilder( valueListenable: getIt<MenuSharedState>().notifier, builder: (context, value, child) { @@ -27,6 +33,7 @@ class SidebarFolder extends StatelessWidget { // favorite if (favoriteViews.isNotEmpty) ...[ FavoriteFolder( + // remove the duplicate views views: favoriteViews, ), const VSpace(10), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart index 3cb102db4249..a4bcf376317c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart @@ -55,6 +55,7 @@ void showSnackBarMessage( }) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( + backgroundColor: Theme.of(context).colorScheme.onSecondary, action: !showCancel ? null : SnackBarAction( @@ -66,7 +67,6 @@ void showSnackBarMessage( ), content: FlowyText( message, - color: Theme.of(context).colorScheme.onSurface, ), ), ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart index d7a1a266572b..6b49630600e7 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart @@ -44,8 +44,8 @@ void showEmojiPickerMenu( builder: (context) => Material( type: MaterialType.transparency, child: Container( - width: 300, - height: 250, + width: 360, + height: 380, padding: const EdgeInsets.all(4.0), decoration: FlowyDecoration.decoration( Theme.of(context).cardColor, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart index 4f36c5cb4c3b..7d07f490f892 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart @@ -1,4 +1,5 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart'; +import 'package:appflowy/startup/tasks/app_window_size_manager.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_listener.dart'; @@ -29,9 +30,7 @@ class _ViewTitleBarState extends State<ViewTitleBar> { void initState() { super.initState(); - ancestors = widget.view.getAncestors( - includeSelf: true, - ); + _reloadAncestors(); } @override @@ -39,9 +38,7 @@ class _ViewTitleBarState extends State<ViewTitleBar> { super.didUpdateWidget(oldWidget); if (oldWidget.view.id != widget.view.id) { - ancestors = widget.view.getAncestors( - includeSelf: true, - ); + _reloadAncestors(); } } @@ -51,27 +48,57 @@ class _ViewTitleBarState extends State<ViewTitleBar> { future: ancestors, builder: ((context, snapshot) { final ancestors = snapshot.data; - if (ancestors == null || - snapshot.connectionState != ConnectionState.done) { + if (ancestors == null) { return const SizedBox.shrink(); } - return Row( + const maxWidth = WindowSizeManager.minWindowWidth - 100; + final replacement = Row( + // refresh the view title bar when the ancestors changed + key: ValueKey(ancestors.hashCode), children: _buildViewTitles(ancestors), ); + return LayoutBuilder( + builder: (context, constraints) { + return Visibility( + visible: constraints.maxWidth < maxWidth, + replacement: replacement, + // if the width is too small, only show one view title bar without the ancestors + child: _ViewTitle( + view: ancestors.last, + behavior: _ViewTitleBehavior.editable, + maxTitleWidth: constraints.maxWidth - 50.0, + onUpdated: () => setState(() => _reloadAncestors()), + ), + ); + }, + ); }), ); } List<Widget> _buildViewTitles(List<ViewPB> views) { + // if the level is too deep, only show the last two view, the first one view and the root view + bool hasAddedEllipsis = false; final children = <Widget>[]; + for (var i = 0; i < views.length; i++) { final view = views[i]; + if (i >= 1 && i < views.length - 2) { + if (!hasAddedEllipsis) { + hasAddedEllipsis = true; + children.add( + const FlowyText.regular(' ... /'), + ); + } + continue; + } children.add( _ViewTitle( view: view, behavior: i == views.length - 1 ? _ViewTitleBehavior.editable // only the last one is editable : _ViewTitleBehavior.uneditable, // others are not editable + onUpdated: () => setState(() => _reloadAncestors()), ), ); if (i != views.length - 1) { @@ -81,6 +108,12 @@ class _ViewTitleBarState extends State<ViewTitleBar> { } return children; } + + void _reloadAncestors() { + ancestors = widget.view.getAncestors( + includeSelf: true, + ); + } } enum _ViewTitleBehavior { @@ -92,10 +125,14 @@ class _ViewTitle extends StatefulWidget { const _ViewTitle({ required this.view, this.behavior = _ViewTitleBehavior.editable, + this.maxTitleWidth = 180, + required this.onUpdated, }); final ViewPB view; final _ViewTitleBehavior behavior; + final double maxTitleWidth; + final VoidCallback onUpdated; @override State<_ViewTitle> createState() => _ViewTitleState(); @@ -124,6 +161,7 @@ class _ViewTitleState extends State<_ViewTitle> { icon = view.icon.value; _resetTextEditingController(); }); + widget.onUpdated(); }, ); } @@ -156,7 +194,15 @@ class _ViewTitleState extends State<_ViewTitle> { fontSize: 18.0, ), const HSpace(2.0), - FlowyText.regular(name), + ConstrainedBox( + constraints: BoxConstraints( + maxWidth: widget.maxTitleWidth, + ), + child: FlowyText.regular( + name, + overflow: TextOverflow.ellipsis, + ), + ), ], ); @@ -188,8 +234,8 @@ class _ViewTitleState extends State<_ViewTitle> { defaultIcon: widget.view.defaultIcon(), direction: PopoverDirection.bottomWithCenterAligned, offset: const Offset(0, 18), - onSubmitted: (emoji, _) { - ViewBackendService.updateViewIcon( + onSubmitted: (emoji, _) async { + await ViewBackendService.updateViewIcon( viewId: widget.view.id, viewIcon: emoji, ); @@ -203,9 +249,9 @@ class _ViewTitleState extends State<_ViewTitle> { child: FlowyTextField( autoFocus: true, controller: textEditingController, - onSubmitted: (text) { + onSubmitted: (text) async { if (text.isNotEmpty && text != name) { - ViewBackendService.updateView( + await ViewBackendService.updateView( viewId: widget.view.id, name: text, ); diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart index 2765c58a9fa1..0d40af1f8adc 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart @@ -26,6 +26,7 @@ class FlowyTextField extends StatefulWidget { final Widget? suffixIcon; final BoxConstraints? prefixIconConstraints; final BoxConstraints? suffixIconConstraints; + final BoxConstraints? hintTextConstraints; const FlowyTextField({ super.key, @@ -50,6 +51,7 @@ class FlowyTextField extends StatefulWidget { this.suffixIcon, this.prefixIconConstraints, this.suffixIconConstraints, + this.hintTextConstraints, }); @override @@ -121,15 +123,22 @@ class FlowyTextFieldState extends State<FlowyTextField> { }, onSubmitted: (text) => _onSubmitted(text), onEditingComplete: widget.onEditingComplete, + minLines: 1, maxLines: widget.maxLines, maxLength: widget.maxLength, maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds, style: widget.textStyle ?? Theme.of(context).textTheme.bodySmall, textAlignVertical: TextAlignVertical.center, + keyboardType: TextInputType.multiline, decoration: InputDecoration( - constraints: BoxConstraints( - maxHeight: widget.errorText?.isEmpty ?? true ? 32 : 58), - contentPadding: const EdgeInsets.symmetric(horizontal: 12), + constraints: widget.hintTextConstraints ?? + BoxConstraints( + maxHeight: widget.errorText?.isEmpty ?? true ? 32 : 58, + ), + contentPadding: EdgeInsets.symmetric( + horizontal: 12, + vertical: widget.maxLines > 1 ? 12 : 0, + ), enabledBorder: OutlineInputBorder( borderSide: BorderSide( color: Theme.of(context).colorScheme.outline, diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs index a3907b2e198c..59547aeb4546 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs @@ -101,11 +101,14 @@ impl FolderOperationHandler for DocumentFolderOperation { FutureResult::new(async move { let mut write_guard = workspace_view_builder.write().await; - // Create a view named "⭐️ Getting started" with built-in README data. + // Create a view named "Getting started" with an icon ⭐️ and the built-in README data. // Don't modify this code unless you know what you are doing. write_guard .with_view_builder(|view_builder| async { - let view = view_builder.with_name("⭐️ Getting started").build(); + let view = view_builder + .with_name("Getting started") + .with_icon("⭐️") + .build(); // create a empty document let json_str = include_str!("../../assets/read_me.json"); let document_pb = JsonToDocumentParser::json_str_to_document(json_str).unwrap(); diff --git a/frontend/rust-lib/flowy-folder2/src/view_operation.rs b/frontend/rust-lib/flowy-folder2/src/view_operation.rs index 20e43ffa5288..7aae7742d568 100644 --- a/frontend/rust-lib/flowy-folder2/src/view_operation.rs +++ b/frontend/rust-lib/flowy-folder2/src/view_operation.rs @@ -96,6 +96,14 @@ impl ViewBuilder { self } + pub fn with_icon(mut self, icon: &str) -> Self { + self.icon = Some(ViewIcon { + ty: collab_folder::IconType::Emoji, + value: icon.to_string(), + }); + self + } + /// Create a child view for the current view. /// The view created by this builder will be the next level view of the current view. pub async fn with_child_view_builder<F, O>(mut self, child_view_builder: F) -> Self From 7867f0366e8db0879c7b7fa34322c3ac0460169b Mon Sep 17 00:00:00 2001 From: "Kilu.He" <108015703+qinluhe@users.noreply.github.com> Date: Mon, 13 Nov 2023 14:16:32 +0800 Subject: [PATCH 47/56] feat: support the operations of field in the grid of tauri (#3906) * feat: support the operations of field in the grid of tauri * fix: performance optimizate --- .../src/appflowy_app/assets/board.svg | 16 ++++ .../src/appflowy_app/assets/document.svg | 14 +++ .../src/appflowy_app/assets/grid.svg | 6 ++ .../components/database/Database.hooks.ts | 18 +++- .../components/database/Database.tsx | 9 +- .../application/field/field_listeners.ts | 11 +++ .../application/field/field_service.ts | 95 +++++++++++++++---- .../database/application/field/field_types.ts | 11 +-- .../database/application/field/index.ts | 1 + .../database/components/cell/TextCell.tsx | 11 ++- .../database/components/field/FieldSelect.tsx | 25 ++--- .../database/components/field/FieldsMenu.tsx | 4 +- .../components/tab_bar/DatabaseTabBar.tsx | 39 +++++--- .../grid/GridCalculate/GridCalculate.tsx | 30 ++++++ .../database/grid/GridCalculate/index.ts | 1 + .../database/grid/GridField/GridField.tsx | 18 +++- .../database/grid/GridField/GridFieldMenu.tsx | 47 +++++---- .../grid/GridField/GridFieldMenuActions.tsx | 72 ++++++++++++-- .../database/grid/GridField/GridResizer.tsx | 92 ++++++++++++++++++ .../grid/GridRow/GridCalculateRow.tsx | 20 +++- .../GridRow/GridCellRow/GridCellRow.hooks.ts | 7 ++ .../grid/GridRow/GridCellRow/GridCellRow.tsx | 27 +++--- .../database/grid/GridRow/GridFieldRow.tsx | 29 +++--- .../database/grid/GridRow/GridRow.tsx | 10 +- .../database/grid/GridRow/constants.ts | 16 ++-- .../database/grid/GridTable/GridTable.tsx | 12 ++- .../components/document/CodeBlock/index.tsx | 4 +- .../document/EquationBlock/index.tsx | 2 +- .../components/document/GridBlock/index.tsx | 2 +- .../components/document/ImageBlock/index.tsx | 2 +- .../components/document/Node/NodeChildren.tsx | 2 +- .../components/document/Node/index.tsx | 4 +- .../components/document/Root/index.tsx | 4 +- .../appflowy_app/hooks/notification.hooks.ts | 2 + 34 files changed, 512 insertions(+), 151 deletions(-) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/assets/board.svg create mode 100644 frontend/appflowy_tauri/src/appflowy_app/assets/document.svg create mode 100644 frontend/appflowy_tauri/src/appflowy_app/assets/grid.svg create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/field_listeners.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridCalculate/GridCalculate.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridCalculate/index.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridResizer.tsx diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/board.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/board.svg new file mode 100644 index 000000000000..0bb0e3fabe56 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/board.svg @@ -0,0 +1,16 @@ +<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'> + <path + d='M12.8 2H3.2C2.53726 2 2 2.55964 2 3.25V5.75C2 6.44036 2.53726 7 3.2 7H12.8C13.4627 7 14 6.44036 14 5.75V3.25C14 2.55964 13.4627 2 12.8 2Z' + stroke='currentColor' + stroke-linecap='round' + stroke-linejoin='round' + /> + <path + d='M12.8 9H3.2C2.53726 9 2 9.55964 2 10.25V12.75C2 13.4404 2.53726 14 3.2 14H12.8C13.4627 14 14 13.4404 14 12.75V10.25C14 9.55964 13.4627 9 12.8 9Z' + stroke='currentColor' + stroke-linecap='round' + stroke-linejoin='round' + /> + <circle cx='4.5' cy='4.5' r='0.5' fill='currentColor' /> + <circle cx='4.5' cy='11.5' r='0.5' fill='currentColor' /> +</svg> \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/document.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/document.svg new file mode 100644 index 000000000000..b00e1cfb38c9 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/document.svg @@ -0,0 +1,14 @@ +<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'> + <path + d='M10.5 3H11.5C11.8315 3 12.1495 3.12877 12.3839 3.35798C12.6183 3.58719 12.75 3.89807 12.75 4.22222V12.7778C12.75 13.1019 12.6183 13.4128 12.3839 13.642C12.1495 13.8712 11.8315 14 11.5 14H4.5C4.16848 14 3.85054 13.8712 3.61612 13.642C3.3817 13.4128 3.25 13.1019 3.25 12.7778V4.22222C3.25 3.89807 3.3817 3.58719 3.61612 3.35798C3.85054 3.12877 4.16848 3 4.5 3H5.5' + stroke='currentColor' + stroke-linecap='round' + stroke-linejoin='round' + /> + <path + d='M9.5 2H6.5C6.22386 2 6 2.22386 6 2.5V3.5C6 3.77614 6.22386 4 6.5 4H9.5C9.77614 4 10 3.77614 10 3.5V2.5C10 2.22386 9.77614 2 9.5 2Z' + stroke='currentColor' + stroke-linecap='round' + stroke-linejoin='round' + /> +</svg> \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/grid.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/grid.svg new file mode 100644 index 000000000000..c397af813011 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/grid.svg @@ -0,0 +1,6 @@ +<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M5.25 1.75H2.625C2.14175 1.75 1.75 2.14175 1.75 2.625V5.25C1.75 5.73325 2.14175 6.125 2.625 6.125H5.25C5.73325 6.125 6.125 5.73325 6.125 5.25V2.625C6.125 2.14175 5.73325 1.75 5.25 1.75Z" stroke="#333333" stroke-width="0.875" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M11.375 1.75H8.75C8.26675 1.75 7.875 2.14175 7.875 2.625V5.25C7.875 5.73325 8.26675 6.125 8.75 6.125H11.375C11.8582 6.125 12.25 5.73325 12.25 5.25V2.625C12.25 2.14175 11.8582 1.75 11.375 1.75Z" stroke="#333333" stroke-width="0.875" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M11.375 7.875H8.75C8.26675 7.875 7.875 8.26675 7.875 8.75V11.375C7.875 11.8582 8.26675 12.25 8.75 12.25H11.375C11.8582 12.25 12.25 11.8582 12.25 11.375V8.75C12.25 8.26675 11.8582 7.875 11.375 7.875Z" stroke="#333333" stroke-width="0.875" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M5.25 7.875H2.625C2.14175 7.875 1.75 8.26675 1.75 8.75V11.375C1.75 11.8582 2.14175 12.25 2.625 12.25H5.25C5.73325 12.25 6.125 11.8582 6.125 11.375V8.75C6.125 8.26675 5.73325 7.875 5.25 7.875Z" stroke="#333333" stroke-width="0.875" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.hooks.ts index 847d5aa03b18..a69942e7166a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.hooks.ts @@ -1,9 +1,9 @@ -import { createContext, useContext, useCallback, useMemo, useEffect, useState, useRef } from 'react'; +import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { proxy, useSnapshot } from 'valtio'; -import { DatabaseLayoutPB, DatabaseNotification } from '@/services/backend'; +import { DatabaseLayoutPB, DatabaseNotification, FieldVisibility } from '@/services/backend'; import { subscribeNotifications } from '$app/hooks'; -import { Database, databaseService, fieldService, rowListeners, sortListeners } from './application'; +import { Database, databaseService, fieldListeners, fieldService, rowListeners, sortListeners } from './application'; export function useSelectDatabaseView({ viewId }: { viewId?: string }) { const key = 'v'; @@ -40,6 +40,15 @@ export const DatabaseProvider = DatabaseContext.Provider; export const useDatabase = () => useSnapshot(useContext(DatabaseContext)); +export const useDatabaseVisibilityFields = () => { + const database = useDatabase(); + + return useMemo( + () => database.fields.filter((field) => field.visibility !== FieldVisibility.AlwaysHidden), + [database.fields] + ); +}; + export const useConnectDatabase = (viewId: string) => { const database = useMemo(() => { const proxyDatabase = proxy<Database>({ @@ -65,6 +74,9 @@ export const useConnectDatabase = (viewId: string) => { [DatabaseNotification.DidUpdateFields]: async () => { database.fields = await fieldService.getFields(viewId); }, + [DatabaseNotification.DidUpdateFieldSettings]: async (changeset) => { + fieldListeners.didUpdateFieldSettings(database, changeset); + }, [DatabaseNotification.DidUpdateViewRows]: (changeset) => { rowListeners.didUpdateViewRows(database, changeset); }, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.tsx index 997b05ab1083..e8e348cd1083 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.tsx @@ -52,7 +52,14 @@ export const Database = ({ selectedViewId, setSelectedViewId }: Props) => { selectedViewId={selectedViewId} childViewIds={childViewIds} /> - <SwipeableViews className={'flex-1 overflow-hidden'} axis={'x'} index={index}> + <SwipeableViews + slideStyle={{ + overflow: 'hidden', + }} + className={'flex-1 overflow-hidden'} + axis={'x'} + index={index} + > {childViewIds.map((id) => ( <TabPanel key={id} index={index} value={index}> <DatabaseLoader viewId={id}> diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/field_listeners.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/field_listeners.ts new file mode 100644 index 000000000000..b000ce5edce6 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/field_listeners.ts @@ -0,0 +1,11 @@ +import { FieldSettingsPB } from '@/services/backend'; +import { Database } from '$app/components/database/application'; + +export function didUpdateFieldSettings(database: Database, settings: FieldSettingsPB) { + const { field_id: fieldId, visibility, width } = settings; + const field = database.fields.find((field) => field.id === fieldId); + + if (!field) return; + field.visibility = visibility; + field.width = width; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/field_service.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/field_service.ts index abb0c53f993d..d7317db3a6dc 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/field_service.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/field_service.ts @@ -8,6 +8,9 @@ import { MoveFieldPayloadPB, RepeatedFieldIdPB, UpdateFieldTypePayloadPB, + FieldSettingsChangesetPB, + FieldVisibility, + DatabaseViewIdPB, } from '@/services/backend'; import { DatabaseEventDuplicateField, @@ -17,6 +20,8 @@ import { DatabaseEventGetFields, DatabaseEventDeleteField, DatabaseEventCreateTypeOption, + DatabaseEventUpdateFieldSettings, + DatabaseEventGetAllFieldSettings, } from '@/services/backend/events/flowy-database2'; import { Field, pbToField } from './field_types'; import { bytesToTypeOption, getTypeOption } from './type_option'; @@ -24,20 +29,39 @@ import { bytesToTypeOption, getTypeOption } from './type_option'; export async function getFields(viewId: string, fieldIds?: string[]): Promise<Field[]> { const payload = GetFieldPayloadPB.fromObject({ view_id: viewId, - field_ids: fieldIds ? RepeatedFieldIdPB.fromObject({ - items: fieldIds.map(fieldId => ({ field_id: fieldId })), - }) : undefined, + field_ids: fieldIds + ? RepeatedFieldIdPB.fromObject({ + items: fieldIds.map((fieldId) => ({ field_id: fieldId })), + }) + : undefined, }); const result = await DatabaseEventGetFields(payload); - const fields = result.map((value) => value.items.map(pbToField)).unwrap(); + const getSettingsPayload = DatabaseViewIdPB.fromObject({ + value: viewId, + }); + + const settings = await DatabaseEventGetAllFieldSettings(getSettingsPayload); - await Promise.all(fields.map(async field => { - const typeOption = await getTypeOption(viewId, field.id, field.type); + if (settings.ok === false || result.ok === false) { + return Promise.reject('Failed to get fields'); + } - field.typeOption = typeOption; - })); + const fields = await Promise.all( + result.val.items.map(async (item) => { + const setting = settings.val.items.find((setting) => setting.field_id === item.id); + const field = pbToField(item); + const typeOption = await getTypeOption(viewId, field.id, field.type); + + return { + ...field, + visibility: setting?.visibility, + width: setting?.width, + typeOption, + }; + }) + ); return fields; } @@ -51,13 +75,14 @@ export async function createField(viewId: string, fieldType?: FieldType, data?: const result = await DatabaseEventCreateTypeOption(payload); - return result.map(value => { - const field = pbToField(value.field); + if (result.ok === false) { + return Promise.reject('Failed to create field'); + } - field.typeOption = bytesToTypeOption(value.type_option_data, field.type); + const field = pbToField(result.val.field); - return field; - }).unwrap(); + field.typeOption = bytesToTypeOption(result.val.type_option_data, field.type); + return field; } export async function duplicateField(viewId: string, fieldId: string): Promise<void> { @@ -68,16 +93,21 @@ export async function duplicateField(viewId: string, fieldId: string): Promise<v const result = await DatabaseEventDuplicateField(payload); - return result.unwrap(); + if (result.ok === false) { + return Promise.reject('Failed to duplicate field'); + } + + return result.val; } -export async function updateField(viewId: string, fieldId: string, data: { - name?: string; - desc?: string; - frozen?: boolean; - visibility?: boolean; - width?: number; -}): Promise<void> { +export async function updateField( + viewId: string, + fieldId: string, + data: { + name?: string; + desc?: string; + } +): Promise<void> { const payload = FieldChangesetPB.fromObject({ view_id: viewId, field_id: fieldId, @@ -124,3 +154,26 @@ export async function deleteField(viewId: string, fieldId: string): Promise<void return result.unwrap(); } + +export async function updateFieldSetting( + viewId: string, + fieldId: string, + settings: { + visibility?: FieldVisibility; + width?: number; + } +): Promise<void> { + const payload = FieldSettingsChangesetPB.fromObject({ + view_id: viewId, + field_id: fieldId, + ...settings, + }); + + const result = await DatabaseEventUpdateFieldSettings(payload); + + if (result.ok === false) { + return Promise.reject('Failed to update field settings'); + } + + return result.val; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/field_types.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/field_types.ts index c685e478bebf..5c6a4f01fde5 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/field_types.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/field_types.ts @@ -1,7 +1,4 @@ -import { - FieldPB, - FieldType, -} from '@/services/backend'; +import { FieldPB, FieldType, FieldVisibility } from '@/services/backend'; import { DateTimeTypeOption, NumberTypeOption, SelectTypeOption } from './type_option/type_option_types'; export interface Field { @@ -9,8 +6,8 @@ export interface Field { name: string; type: FieldType; typeOption?: unknown; - visibility: boolean; - width: number; + visibility?: FieldVisibility; + width?: number; isPrimary: boolean; } @@ -35,7 +32,5 @@ export const pbToField = (pb: FieldPB): Field => ({ id: pb.id, name: pb.name, type: pb.field_type, - visibility: pb.visibility, - width: pb.width, isPrimary: pb.is_primary, }); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/index.ts index 2cb812dc0de1..fa993023e111 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/index.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/application/field/index.ts @@ -2,3 +2,4 @@ export * from './select_option'; export * from './type_option'; export * from './field_types'; export * as fieldService from './field_service'; +export * as fieldListeners from './field_listeners'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/TextCell.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/TextCell.tsx index d4e9d5a53508..48445c91efee 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/TextCell.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/TextCell.tsx @@ -1,8 +1,9 @@ import { Popover, TextareaAutosize } from '@mui/material'; -import { FC, FormEventHandler, useCallback, useLayoutEffect, useRef, useState } from 'react'; +import { FC, FormEventHandler, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; import { useViewId } from '$app/hooks'; import { cellService, Field, TextCell as TextCellType } from '../../application'; import { CellText } from '../../_shared'; +import { useGridUIStateDispatcher } from '$app/components/database/proxy/grid/ui_state/actions'; export const TextCell: FC<{ field: Field; @@ -13,7 +14,7 @@ export const TextCell: FC<{ const [editing, setEditing] = useState(false); const [text, setText] = useState(''); const [width, setWidth] = useState<number | undefined>(undefined); - + const { setRowHover } = useGridUIStateDispatcher(); const handleClose = () => { if (!cell) return; if (editing) { @@ -41,6 +42,12 @@ export const TextCell: FC<{ } }, [editing]); + useEffect(() => { + if (editing) { + setRowHover(null); + } + }, [editing, setRowHover]); + return ( <> <CellText ref={cellRef} className='w-full' onClick={handleClick}> diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldSelect.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldSelect.tsx index 7ad17a5a219a..bb6c899a00b2 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldSelect.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldSelect.tsx @@ -1,28 +1,31 @@ import { MenuItem, Select, SelectChangeEvent, SelectProps } from '@mui/material'; import { FC, useCallback } from 'react'; import { Field as FieldType } from '../../application'; -import { useDatabase } from '../../Database.hooks'; +import { useDatabaseVisibilityFields } from '../../Database.hooks'; import { Field } from './Field'; export interface FieldSelectProps extends Omit<SelectProps, 'onChange'> { onChange?: (event: SelectChangeEvent<unknown>, field: FieldType | undefined) => void; } -export const FieldSelect: FC<FieldSelectProps> = ({ - onChange, - ...props -}) => { - const { fields } = useDatabase(); +export const FieldSelect: FC<FieldSelectProps> = ({ onChange, ...props }) => { + const fields = useDatabaseVisibilityFields(); - const handleChange = useCallback((event: SelectChangeEvent<unknown>) => { - const selectedId = event.target.value; + const handleChange = useCallback( + (event: SelectChangeEvent<unknown>) => { + const selectedId = event.target.value; - onChange?.(event, fields.find(field => field.id === selectedId)); - }, [onChange, fields]); + onChange?.( + event, + fields.find((field) => field.id === selectedId) + ); + }, + [onChange, fields] + ); return ( <Select onChange={handleChange} {...props}> - {fields.map(field => ( + {fields.map((field) => ( <MenuItem key={field.id} value={field.id}> <Field field={field} /> </MenuItem> diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldsMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldsMenu.tsx index a35b0cf0870f..3407292633ad 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldsMenu.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field/FieldsMenu.tsx @@ -1,7 +1,7 @@ import { Menu, MenuItem, MenuProps } from '@mui/material'; import { FC, MouseEvent } from 'react'; import { Field as FieldType } from '../../application'; -import { useDatabase } from '../../Database.hooks'; +import { useDatabaseVisibilityFields } from '../../Database.hooks'; import { Field } from './Field'; export interface FieldsMenuProps extends MenuProps { @@ -9,7 +9,7 @@ export interface FieldsMenuProps extends MenuProps { } export const FieldsMenu: FC<FieldsMenuProps> = ({ onMenuItemClick, ...props }) => { - const { fields } = useDatabase(); + const fields = useDatabaseVisibilityFields(); return ( <Menu {...props}> diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/DatabaseTabBar.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/DatabaseTabBar.tsx index 24846633afe4..ef2020bfcffc 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/DatabaseTabBar.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/DatabaseTabBar.tsx @@ -1,8 +1,12 @@ -import { FC, useEffect } from 'react'; +import { FC, FunctionComponent, SVGProps, useEffect } from 'react'; import { ViewTabs, ViewTab } from './ViewTabs'; import { useAppSelector } from '$app/stores/store'; import { useTranslation } from 'react-i18next'; import AddViewBtn from '$app/components/database/components/tab_bar/AddViewBtn'; +import { ViewLayoutPB } from '@/services/backend'; +import { ReactComponent as GridSvg } from '$app/assets/grid.svg'; +import { ReactComponent as BoardSvg } from '$app/assets/board.svg'; +import { ReactComponent as DocumentSvg } from '$app/assets/document.svg'; export interface DatabaseTabBarProps { childViewIds: string[]; @@ -11,6 +15,15 @@ export interface DatabaseTabBarProps { pageId: string; } +const DatabaseIcons: { + [key in ViewLayoutPB]: FunctionComponent<SVGProps<SVGSVGElement> & { title?: string | undefined }>; +} = { + [ViewLayoutPB.Document]: DocumentSvg, + [ViewLayoutPB.Grid]: GridSvg, + [ViewLayoutPB.Board]: BoardSvg, + [ViewLayoutPB.Calendar]: GridSvg, +}; + export const DatabaseTabBar: FC<DatabaseTabBarProps> = ({ pageId, childViewIds, selectedViewId, setSelectedViewId }) => { const { t } = useTranslation(); const views = useAppSelector((state) => { @@ -33,16 +46,20 @@ export const DatabaseTabBar: FC<DatabaseTabBarProps> = ({ pageId, childViewIds, <div className='-mb-px flex items-center border-b border-line-divider'> <div className='flex flex-1 items-center'> <ViewTabs value={selectedViewId} onChange={handleChange}> - {views.map((view) => ( - <ViewTab - key={view.id} - icon={undefined} - iconPosition='start' - color='inherit' - label={view.name || t('grid.title.placeholder')} - value={view.id} - /> - ))} + {views.map((view) => { + const Icon = DatabaseIcons[view.layout]; + + return ( + <ViewTab + key={view.id} + icon={<Icon />} + iconPosition='start' + color='inherit' + label={view.name || t('grid.title.placeholder')} + value={view.id} + /> + ); + })} </ViewTabs> <AddViewBtn pageId={pageId} /> </div> diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridCalculate/GridCalculate.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridCalculate/GridCalculate.tsx new file mode 100644 index 000000000000..ecb591955f15 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridCalculate/GridCalculate.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { useDatabase } from '$app/components/database'; +import { Field } from '$app/components/database/application'; +import { DEFAULT_FIELD_WIDTH } from '$app/components/database/grid/GridRow'; + +interface Props { + field: Field; + index: number; +} + +function GridCalculate({ field, index }: Props) { + const { rowMetas } = useDatabase(); + const count = rowMetas.length; + const width = field.width ?? DEFAULT_FIELD_WIDTH; + + return ( + <div + style={{ + width, + visibility: index === 0 ? 'visible' : 'hidden', + }} + className={'flex justify-end'} + > + <span className={'mr-2 text-text-caption'}>Count</span> + <span>{count}</span> + </div> + ); +} + +export default GridCalculate; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridCalculate/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridCalculate/index.ts new file mode 100644 index 000000000000..2bd3b71b1eb0 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridCalculate/index.ts @@ -0,0 +1 @@ +export * from './GridCalculate'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridField.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridField.tsx index a63061b87cc5..c60cccc95ceb 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridField.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridField.tsx @@ -7,6 +7,8 @@ import { fieldService, Field } from '../../application'; import { useDatabase } from '../../Database.hooks'; import { FieldTypeSvg } from './FieldTypeSvg'; import { GridFieldMenu } from './GridFieldMenu'; +import GridResizer from '$app/components/database/grid/GridField/GridResizer'; +import { DEFAULT_FIELD_WIDTH } from '$app/components/database/grid/GridRow'; export interface GridFieldProps { field: Field; @@ -18,6 +20,7 @@ export const GridField: FC<GridFieldProps> = ({ field }) => { const [openMenu, setOpenMenu] = useState(false); const [openTooltip, setOpenTooltip] = useState(false); const [dropPosition, setDropPosition] = useState<DropPosition>(DropPosition.Before); + const [fieldWidth, setFieldWidth] = useState(field.width || DEFAULT_FIELD_WIDTH); const handleClick = useCallback(() => { setOpenMenu(true); @@ -89,7 +92,12 @@ export const GridField: FC<GridFieldProps> = ({ field }) => { }); return ( - <> + <div + className={'flex border-r border-line-divider'} + style={{ + width: fieldWidth, + }} + > <Tooltip open={openTooltip && !isDragging} title={field.name} @@ -104,6 +112,11 @@ export const GridField: FC<GridFieldProps> = ({ field }) => { ref={setPreviewRef} className='relative flex w-full items-center px-2' disableRipple + onContextMenu={(event) => { + event.stopPropagation(); + event.preventDefault(); + handleClick(); + }} onClick={handleClick} {...attributes} {...listeners} @@ -118,11 +131,12 @@ export const GridField: FC<GridFieldProps> = ({ field }) => { }`} /> )} + <GridResizer field={field} onWidthChange={(width) => setFieldWidth(width)} /> </Button> </Tooltip> {openMenu && ( <GridFieldMenu field={field} open={openMenu} anchorEl={previewRef.current} onClose={handleMenuClose} /> )} - </> + </div> ); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridFieldMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridFieldMenu.tsx index 1d7202fc3b5c..e531d7a8e015 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridFieldMenu.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridFieldMenu.tsx @@ -7,20 +7,14 @@ import { FieldTypeSvg } from './FieldTypeSvg'; import { FieldTypeText } from './FieldTypeText'; import { GridFieldMenuActions } from './GridFieldMenuActions'; - export interface GridFieldMenuProps { field: Field; anchorEl: MenuProps['anchorEl']; open: boolean; - onClose: MenuProps['onClose']; + onClose: () => void; } -export const GridFieldMenu: FC<GridFieldMenuProps> = ({ - field, - anchorEl, - open, - onClose, -}) => { +export const GridFieldMenu: FC<GridFieldMenuProps> = ({ field, anchorEl, open, onClose }) => { const viewId = useViewId(); const [inputtingName, setInputtingName] = useState(field.name); @@ -43,8 +37,8 @@ export const GridFieldMenu: FC<GridFieldMenuProps> = ({ const fieldNameInput = ( <OutlinedInput - className="mx-3 mt-1 mb-5 !rounded-[10px]" - size="small" + className='mx-3 mb-5 mt-1 !rounded-[10px]' + size='small' value={inputtingName} onChange={handleInput} onBlur={handleBlur} @@ -53,24 +47,27 @@ export const GridFieldMenu: FC<GridFieldMenuProps> = ({ const fieldTypeSelect = ( <MenuItem dense> - <FieldTypeSvg type={field.type} className="text-base mr-2" /> - <span className="flex-1 text-xs font-medium"> - {FieldTypeText(field.type)} - </span> - <MoreSvg className="text-base" /> + <FieldTypeSvg type={field.type} className='mr-2 text-base' /> + <span className='flex-1 text-xs font-medium'>{FieldTypeText(field.type)}</span> + <MoreSvg className='text-base' /> </MenuItem> ); + const isPrimary = field.isPrimary; + return ( - <Menu - anchorEl={anchorEl} - open={open} - onClose={onClose} - > - {fieldNameInput} - {fieldTypeSelect} - <Divider /> - <GridFieldMenuActions /> - </Menu> + <> + <Menu anchorEl={anchorEl} open={open} onClose={onClose}> + {fieldNameInput} + {!isPrimary && ( + <> + {fieldTypeSelect} + <Divider /> + </> + )} + + <GridFieldMenuActions isPrimary={isPrimary} onMenuItemClick={() => onClose()} fieldId={field.id} /> + </Menu> + </> ); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridFieldMenuActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridFieldMenuActions.tsx index 26a4f27c15be..85c29b668b90 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridFieldMenuActions.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridFieldMenuActions.tsx @@ -6,6 +6,11 @@ import { ReactComponent as CopySvg } from '$app/assets/copy.svg'; import { ReactComponent as DeleteSvg } from '$app/assets/delete.svg'; import { ReactComponent as LeftSvg } from '$app/assets/left.svg'; import { ReactComponent as RightSvg } from '$app/assets/right.svg'; +import { fieldService } from '$app/components/database/application'; +import { FieldVisibility } from '@/services/backend'; +import { useViewId } from '$app/hooks'; +import ConfirmDialog from '$app/components/_shared/app-dialog/ConfirmDialog'; +import { useState } from 'react'; enum FieldAction { Hide = 'hide', @@ -25,26 +30,79 @@ const FieldActionSvgMap = { const TwoColumnActions: FieldAction[][] = [ [FieldAction.Hide, FieldAction.Duplicate, FieldAction.Delete], - [FieldAction.InsertLeft, FieldAction.InsertRight], + // [FieldAction.InsertLeft, FieldAction.InsertRight], ]; -export const GridFieldMenuActions = () => { +// prevent default actions for primary fields +const primaryPreventDefaultActions = [FieldAction.Delete, FieldAction.Duplicate]; + +interface GridFieldMenuActionsProps { + fieldId: string; + isPrimary?: boolean; + onMenuItemClick?: (action: FieldAction) => void; +} + +export const GridFieldMenuActions = ({ fieldId, onMenuItemClick, isPrimary }: GridFieldMenuActionsProps) => { + const viewId = useViewId(); + const [openConfirm, setOpenConfirm] = useState(false); + + const handleOpenConfirm = () => { + setOpenConfirm(true); + }; + + const handleMenuItemClick = async (action: FieldAction) => { + const preventDefault = isPrimary && primaryPreventDefaultActions.includes(action); + + if (preventDefault) { + return; + } + + switch (action) { + case FieldAction.Hide: + await fieldService.updateFieldSetting(viewId, fieldId, { + visibility: FieldVisibility.AlwaysHidden, + }); + break; + case FieldAction.Duplicate: + await fieldService.duplicateField(viewId, fieldId); + break; + case FieldAction.Delete: + handleOpenConfirm(); + return; + } + + onMenuItemClick?.(action); + }; + return ( - <Grid container spacing={2}> + <Grid container columns={TwoColumnActions.length} spacing={2}> {TwoColumnActions.map((column, index) => ( <Grid key={index} item xs={6}> - {column.map(action => { + {column.map((action) => { const ActionSvg = FieldActionSvgMap[action]; + const disabled = isPrimary && primaryPreventDefaultActions.includes(action); return ( - <MenuItem key={action} dense> - <ActionSvg className="mr-2 text-base" /> + <MenuItem disabled={disabled} onClick={() => handleMenuItemClick(action)} key={action} dense> + <ActionSvg className='mr-2 text-base' /> {t(`grid.field.${action}`)} </MenuItem> ); })} </Grid> ))} + <ConfirmDialog + open={openConfirm} + subtitle={''} + title={t('grid.field.deleteFieldPromptMessage')} + onOk={async () => { + await fieldService.deleteField(viewId, fieldId); + }} + onClose={() => { + setOpenConfirm(false); + onMenuItemClick?.(FieldAction.Delete); + }} + /> </Grid> ); -}; \ No newline at end of file +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridResizer.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridResizer.tsx new file mode 100644 index 000000000000..6a834b6fab3f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridField/GridResizer.tsx @@ -0,0 +1,92 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { Field, fieldService } from '$app/components/database/application'; +import { useViewId } from '$app/hooks'; + +interface GridResizerProps { + field: Field; + onWidthChange?: (width: number) => void; +} + +const minWidth = 100; + +function GridResizer({ field, onWidthChange }: GridResizerProps) { + const viewId = useViewId(); + const fieldId = field.id; + const width = field.width || 0; + const [isResizing, setIsResizing] = useState(false); + const [newWidth, setNewWidth] = useState(width); + const [hover, setHover] = useState(false); + const startX = useRef(0); + + const onResize = useCallback( + (e: MouseEvent) => { + const diff = e.clientX - startX.current; + const newWidth = width + diff; + + if (newWidth < minWidth) { + return; + } + + setNewWidth(newWidth); + }, + [width] + ); + + useEffect(() => { + onWidthChange?.(newWidth); + }, [newWidth, onWidthChange]); + + useEffect(() => { + if (!isResizing && width !== newWidth) { + void fieldService.updateFieldSetting(viewId, fieldId, { + width: newWidth, + }); + } + }, [fieldId, isResizing, newWidth, viewId, width]); + + const onResizeEnd = useCallback(() => { + setIsResizing(false); + document.removeEventListener('mousemove', onResize); + document.removeEventListener('mouseup', onResizeEnd); + }, [onResize]); + + const onResizeStart = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + startX.current = e.clientX; + setIsResizing(true); + document.addEventListener('mousemove', onResize); + document.addEventListener('mouseup', onResizeEnd); + }, + [onResize, onResizeEnd] + ); + + return ( + <div + onMouseDown={onResizeStart} + onClick={(e) => { + e.stopPropagation(); + }} + onMouseEnter={() => { + setHover(true); + }} + onMouseLeave={() => { + setHover(false); + }} + style={{ + right: `-3px`, + }} + className={'absolute top-0 z-10 h-full cursor-col-resize'} + > + <div + className={'h-full w-[6px] select-none bg-transparent'} + style={{ + backgroundColor: hover || isResizing ? 'var(--content-on-fill-hover)' : 'transparent', + }} + ></div> + </div> + ); +} + +export default GridResizer; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCalculateRow.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCalculateRow.tsx index 871958e74e89..cd4c5a53f905 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCalculateRow.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCalculateRow.tsx @@ -1,3 +1,17 @@ -export const GridCalculateRow = () => { - return null; -}; +import React from 'react'; +import { useDatabaseVisibilityFields } from '$app/components/database'; +import GridCalculate from '$app/components/database/grid/GridCalculate/GridCalculate'; + +function GridCalculateRow() { + const fields = useDatabaseVisibilityFields(); + + return ( + <div className='flex grow items-center'> + {fields.map((field, index) => { + return <GridCalculate index={index} key={field.id} field={field} />; + })} + </div> + ); +} + +export default GridCalculateRow; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRow.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRow.hooks.ts index 87d65ba0eba4..0b564e703d1c 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRow.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRow.hooks.ts @@ -14,6 +14,12 @@ export function useGridRowActionsDisplay(rowId: string, ref: React.RefObject<HTM setRowHover(rowId); }, [setRowHover, rowId]); + const onMouseLeave = useCallback(() => { + if (hover) { + setRowHover(null); + } + }, [setRowHover, hover]); + useEffect(() => { // Next frame to avoid layout thrashing requestAnimationFrame(() => { @@ -37,6 +43,7 @@ export function useGridRowActionsDisplay(rowId: string, ref: React.RefObject<HTM return { actionsStyle, onMouseEnter, + onMouseLeave, hover, }; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRow.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRow.tsx index cc9e913a969b..04da0dcf51af 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRow.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridCellRow/GridCellRow.tsx @@ -3,7 +3,7 @@ import { Portal } from '@mui/material'; import { DragEventHandler, FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { throttle } from '$app/utils/tool'; import { useViewId } from '$app/hooks'; -import { useDatabase } from '../../../Database.hooks'; +import { useDatabaseVisibilityFields } from '../../../Database.hooks'; import { rowService, RowMeta } from '../../../application'; import { DragItem, @@ -21,6 +21,7 @@ import { useGridRowContextMenu, } from '$app/components/database/grid/GridRow/GridCellRow/GridCellRow.hooks'; import GridCellRowContextMenu from '$app/components/database/grid/GridRow/GridCellRow/GridCellRowContextMenu'; +import { DEFAULT_FIELD_WIDTH } from '$app/components/database/grid/GridRow'; export interface GridCellRowProps { rowMeta: RowMeta; @@ -32,14 +33,14 @@ export const GridCellRow: FC<GridCellRowProps> = ({ rowMeta, virtualizer, getPre const rowId = rowMeta.id; const viewId = useViewId(); const ref = useRef<HTMLDivElement | null>(null); - const { onMouseEnter, actionsStyle, hover } = useGridRowActionsDisplay(rowId, ref); + const { onMouseLeave, onMouseEnter, actionsStyle, hover } = useGridRowActionsDisplay(rowId, ref); const { isContextMenuOpen, closeContextMenu, openContextMenu, position: contextMenuPosition, } = useGridRowContextMenu(); - const { fields } = useDatabase(); + const fields = useDatabaseVisibilityFields(); const [dropPosition, setDropPosition] = useState<DropPosition>(DropPosition.Before); const dragData = useMemo( @@ -106,7 +107,7 @@ export const GridCellRow: FC<GridCellRowProps> = ({ rowMeta, virtualizer, getPre }, [openContextMenu]); return ( - <div ref={ref} className='flex grow' onMouseEnter={onMouseEnter} {...dropListeners}> + <div ref={ref} className='flex grow' onMouseLeave={onMouseLeave} onMouseEnter={onMouseEnter} {...dropListeners}> <div ref={setPreviewRef} className={`relative flex grow border-b border-line-divider ${isDragging ? 'bg-blue-50' : ''}`} @@ -117,7 +118,7 @@ export const GridCellRow: FC<GridCellRowProps> = ({ rowMeta, virtualizer, getPre virtualizer={virtualizer} renderItem={(index) => <GridCell rowId={rowMeta.id} field={fields[index]} />} /> - <div className='min-w-20 grow' /> + <div className={`w-[${DEFAULT_FIELD_WIDTH}px]`} /> {isOver && ( <div className={`absolute left-0 right-0 z-10 h-0.5 bg-blue-500 ${ @@ -137,13 +138,15 @@ export const GridCellRow: FC<GridCellRowProps> = ({ rowMeta, virtualizer, getPre rowId={rowMeta.id} getPrevRowId={getPrevRowId} /> - <GridCellRowContextMenu - open={isContextMenuOpen} - onClose={closeContextMenu} - anchorPosition={contextMenuPosition} - rowId={rowId} - getPrevRowId={getPrevRowId} - /> + {isContextMenuOpen && ( + <GridCellRowContextMenu + open={isContextMenuOpen} + onClose={closeContextMenu} + anchorPosition={contextMenuPosition} + rowId={rowId} + getPrevRowId={getPrevRowId} + /> + )} </Portal> </div> ); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridFieldRow.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridFieldRow.tsx index dc819e4ab8a3..a64de4a15ef2 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridFieldRow.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridFieldRow.tsx @@ -1,36 +1,31 @@ -import { Virtualizer } from '@tanstack/react-virtual'; -import { FC } from 'react'; import { Button } from '@mui/material'; import { FieldType } from '@/services/backend'; import { ReactComponent as AddSvg } from '$app/assets/add.svg'; import { fieldService } from '../../application'; -import { useDatabase } from '../../Database.hooks'; -import { VirtualizedList } from '../../_shared'; +import { useDatabaseVisibilityFields } from '../../Database.hooks'; import { GridField } from '../GridField'; import { useViewId } from '@/appflowy_app/hooks'; import { useTranslation } from 'react-i18next'; +import { DEFAULT_FIELD_WIDTH } from '$app/components/database/grid/GridRow/constants'; -export interface GridFieldRowProps { - virtualizer: Virtualizer<HTMLDivElement, HTMLDivElement>; -} - -export const GridFieldRow: FC<GridFieldRowProps> = ({ virtualizer }) => { +export const GridFieldRow = () => { const { t } = useTranslation(); const viewId = useViewId(); - const { fields } = useDatabase(); + const fields = useDatabaseVisibilityFields(); + const handleClick = async () => { await fieldService.createField(viewId, FieldType.RichText); }; return ( <div className='z-10 flex border-b border-line-divider'> - <VirtualizedList - className='flex' - virtualizer={virtualizer} - itemClassName='flex border-r border-line-divider' - renderItem={(index) => <GridField field={fields[index]} />} - /> - <div className='min-w-20 grow'> + <div className={'flex'}> + {fields.map((field) => { + return <GridField key={field.id} field={field} />; + })} + </div> + + <div className={`w-[${DEFAULT_FIELD_WIDTH}px]`}> <Button color={'inherit'} className='flex h-full w-full items-center justify-start whitespace-nowrap text-left' diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridRow.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridRow.tsx index b3e53076e3df..b34fe5f2b15e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridRow.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/GridRow.tsx @@ -2,9 +2,9 @@ import { Virtualizer } from '@tanstack/react-virtual'; import { FC } from 'react'; import { RenderRow, RenderRowType } from './constants'; import { GridCellRow } from './GridCellRow'; -import { GridFieldRow } from './GridFieldRow'; import { GridNewRow } from './GridNewRow'; -import { GridCalculateRow } from './GridCalculateRow'; +import { GridFieldRow } from '$app/components/database/grid/GridRow/GridFieldRow'; +import GridCalculateRow from '$app/components/database/grid/GridRow/GridCalculateRow'; export interface GridRowProps { row: RenderRow; @@ -14,13 +14,13 @@ export interface GridRowProps { export const GridRow: FC<GridRowProps> = ({ row, virtualizer, getPrevRowId }) => { switch (row.type) { + case RenderRowType.Fields: + return <GridFieldRow />; case RenderRowType.Row: return <GridCellRow rowMeta={row.data.meta} virtualizer={virtualizer} getPrevRowId={getPrevRowId} />; - case RenderRowType.Fields: - return <GridFieldRow virtualizer={virtualizer} />; case RenderRowType.NewRow: return <GridNewRow startRowId={row.data.startRowId} groupId={row.data.groupId} />; - case RenderRowType.Calculate: + case RenderRowType.CalculateRow: return <GridCalculateRow />; default: return null; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/constants.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/constants.ts index 2bb6137d1d94..ba0b0ea96678 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/constants.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridRow/constants.ts @@ -1,10 +1,18 @@ import { RowMeta } from '../../application'; +export const GridCalculateCountHeight = 40; + +export const DEFAULT_FIELD_WIDTH = 150; + export enum RenderRowType { Fields = 'fields', Row = 'row', NewRow = 'new-row', - Calculate = 'calculate', + CalculateRow = 'calculate-row', +} + +export interface CalculateRenderRow { + type: RenderRowType.CalculateRow; } export interface FieldRenderRow { @@ -26,10 +34,6 @@ export interface NewRenderRow { }; } -export interface CalculateRenderRow { - type: RenderRowType.Calculate; -} - export type RenderRow = FieldRenderRow | CellRenderRow | NewRenderRow | CalculateRenderRow; export const rowMetasToRenderRow = (rowMetas: RowMeta[]): RenderRow[] => { @@ -50,7 +54,7 @@ export const rowMetasToRenderRow = (rowMetas: RowMeta[]): RenderRow[] => { }, }, { - type: RenderRowType.Calculate, + type: RenderRowType.CalculateRow, }, ]; }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridTable/GridTable.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridTable/GridTable.tsx index 84b8dcd513ef..5a5d7cfeb603 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridTable/GridTable.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/GridTable/GridTable.tsx @@ -1,9 +1,9 @@ import { useVirtualizer } from '@tanstack/react-virtual'; import { FC, useMemo, useRef } from 'react'; import { RowMeta } from '../../application'; -import { useDatabase } from '../../Database.hooks'; +import { useDatabase, useDatabaseVisibilityFields } from '../../Database.hooks'; import { VirtualizedList } from '../../_shared'; -import { GridRow, RenderRow, RenderRowType, rowMetasToRenderRow } from '../GridRow'; +import { DEFAULT_FIELD_WIDTH, GridRow, RenderRow, RenderRowType, rowMetasToRenderRow } from '../GridRow'; const getRenderRowKey = (row: RenderRow) => { if (row.type === RenderRowType.Row) { @@ -16,9 +16,9 @@ const getRenderRowKey = (row: RenderRow) => { export const GridTable: FC<{ tableHeight: number }> = ({ tableHeight }) => { const verticalScrollElementRef = useRef<HTMLDivElement | null>(null); const horizontalScrollElementRef = useRef<HTMLDivElement | null>(null); - const { rowMetas, fields } = useDatabase(); + const { rowMetas } = useDatabase(); const renderRows = useMemo<RenderRow[]>(() => rowMetasToRenderRow(rowMetas as RowMeta[]), [rowMetas]); - + const fields = useDatabaseVisibilityFields(); const rowVirtualizer = useVirtualizer<HTMLDivElement, HTMLDivElement>({ count: renderRows.length, overscan: 20, @@ -33,7 +33,9 @@ export const GridTable: FC<{ tableHeight: number }> = ({ tableHeight }) => { overscan: 5, getItemKey: (i) => fields[i].id, getScrollElement: () => horizontalScrollElementRef.current, - estimateSize: (i) => fields[i].width ?? 201, + estimateSize: (i) => { + return fields[i].width ?? DEFAULT_FIELD_WIDTH; + }, }); const getPrevRowId = (id: string) => { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/CodeBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/CodeBlock/index.tsx index eea05bb50cc1..907c671c7bd0 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/CodeBlock/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/CodeBlock/index.tsx @@ -8,7 +8,7 @@ import { useSelection } from '$app/components/document/_shared/EditorHooks/useSe import { useAppSelector } from '$app/stores/store'; import { ThemeMode } from '$app/interfaces'; -export default function CodeBlock({ +export default React.memo(function CodeBlock({ node, placeholder, ...props @@ -40,4 +40,4 @@ export default function CodeBlock({ /> </div> ); -} +}); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/EquationBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/EquationBlock/index.tsx index 8532aaa511ba..489a7aa64177 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/EquationBlock/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/EquationBlock/index.tsx @@ -84,4 +84,4 @@ function EquationBlock({ node }: { node: NestedBlock<BlockType.EquationBlock> }) ); } -export default EquationBlock; +export default React.memo(EquationBlock); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/GridBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/GridBlock/index.tsx index 1f689943b590..d3c9d10dc1cd 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/GridBlock/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/GridBlock/index.tsx @@ -33,4 +33,4 @@ function GridBlock({ node }: { node: NestedBlock<BlockType.GridBlock> }) { ); } -export default GridBlock; +export default React.memo(GridBlock); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/ImageBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/ImageBlock/index.tsx index b497989eb34e..aff3cf4bc667 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/ImageBlock/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/ImageBlock/index.tsx @@ -77,4 +77,4 @@ function ImageBlock({ node }: { node: NestedBlock<BlockType.ImageBlock> }) { ); } -export default ImageBlock; +export default React.memo(ImageBlock); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/NodeChildren.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/NodeChildren.tsx index 7b9978cda7a5..c134058dbab4 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/NodeChildren.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/NodeChildren.tsx @@ -11,4 +11,4 @@ function NodeChildren({ childIds, ...props }: { childIds?: string[] } & React.HT ) : null; } -export default NodeChildren; +export default React.memo(NodeChildren); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx index 46f394a6be64..3dc5f6c50b4b 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Node/index.tsx @@ -96,7 +96,7 @@ function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<H ); } -const NodeWithErrorBoundary = withErrorBoundary(NodeComponent, { +const NodeWithErrorBoundary = withErrorBoundary(React.memo(NodeComponent), { FallbackComponent: ErrorBoundaryFallbackComponent, }); @@ -110,4 +110,4 @@ const UnSupportedBlock = () => { ); }; -export default React.memo(NodeWithErrorBoundary); +export default NodeWithErrorBoundary; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx index 23b9e6720c96..d8683e4d55a4 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Root/index.tsx @@ -27,8 +27,8 @@ function Root({ documentData }: { documentData: DocumentData }) { ); } -const RootWithErrorBoundary = withErrorBoundary(Root, { +const RootWithErrorBoundary = withErrorBoundary(React.memo(Root), { FallbackComponent: ErrorBoundaryFallbackComponent, }); -export default React.memo(RootWithErrorBoundary); +export default RootWithErrorBoundary; diff --git a/frontend/appflowy_tauri/src/appflowy_app/hooks/notification.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/hooks/notification.hooks.ts index 5c1efe43dbf3..73edd7059486 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/hooks/notification.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/hooks/notification.hooks.ts @@ -14,6 +14,7 @@ import { RowsChangePB, RowsVisibilityChangePB, SortChangesetNotificationPB, + FieldSettingsPB, } from '@/services/backend'; const NotificationPBMap = { @@ -28,6 +29,7 @@ const NotificationPBMap = { [DatabaseNotification.DidUpdateField]: FieldPB, [DatabaseNotification.DidUpdateCell]: null, [DatabaseNotification.DidUpdateSort]: SortChangesetNotificationPB, + [DatabaseNotification.DidUpdateFieldSettings]: FieldSettingsPB, }; type NotificationMap = typeof NotificationPBMap; From a63a7ea61140fec716918a8a04b4537e3ed225ee Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 13 Nov 2023 16:14:31 +0800 Subject: [PATCH 48/56] feat: hidden kanban groups (#3907) * feat: hide/unhide ui * chore: implement collapsible side bar and adjust group header (#2) * refactor: hidden columns into own file * chore: adjust new group button position * fix: flowy icon buton secondary color bleed * chore: some UI adjustments * fix: some regressions * chore: proper group is_visible fetching * chore: use a bloc to manage hidden groups * fix: hiding groups not working * chore: implement hidden group popups * chore: proper ungrouped item column management * chore: remove ungrouped items button * chore: flowy hover build * fix: clean up code * test: integration tests * fix: not null promise on null value * fix: hide and unhide multiple groups * chore: i18n and code review * chore: missed review * fix: rust-lib-test * fix: dont completely remove flowyiconhovercolor * chore: apply suggest * fix: number of rows inside hidden groups not updating properly * fix: hidden groups disappearing after collapse * fix: hidden group title alignment * fix: insert newly unhidden groups into the correct position * chore: adjust padding all around * feat: reorder hidden groups * chore: adjust padding * chore: collapse hidden groups section persist * chore: no status group at beginning * fix: hiding groups when grouping with other types * chore: disable rename groups that arent supported * chore: update appflowy board ref * chore: better naming * test: fix tests --------- Co-authored-by: Mathias Mogensen <mathias@appflowy.io> --- .../board/board_add_row_test.dart | 11 +- .../board/board_hide_groups_test.dart | 98 ++++ .../board/application/board_bloc.dart | 330 ++++++++---- .../board/application/group.dart | 12 - .../board/application/group_controller.dart | 45 +- .../application/ungrouped_items_bloc.dart | 112 ---- .../board/presentation/board_page.dart | 221 ++++---- .../presentation/ungrouped_items_button.dart | 237 --------- .../widgets/board_column_header.dart | 183 ++++--- .../widgets/board_hidden_groups.dart | 483 ++++++++++++++++++ .../grid/application/grid_header_bloc.dart | 4 +- .../grid/presentation/layout/sizes.dart | 2 +- .../widgets/header/field_type_extension.dart | 6 + .../database_view/tar_bar/tab_bar_view.dart | 9 +- .../presentation/home/home_stack.dart | 1 + .../lib/style_widget/icon_button.dart | 13 +- frontend/appflowy_flutter/pubspec.lock | 6 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- .../group_by_multi_select_field_test.dart | 6 +- .../resources/flowy_icons/16x/hamburger_s.svg | 7 + .../flowy_icons/16x/pull_left_outlined.svg | 6 + frontend/resources/translations/en.json | 18 +- .../tests/database/local_test/test.rs | 3 +- .../src/entities/board_entities.rs | 5 + .../src/services/database_view/view_editor.rs | 19 +- .../src/services/group/action.rs | 2 +- .../src/services/group/configuration.rs | 10 +- .../src/services/group/controller.rs | 26 +- .../controller_impls/checkbox_controller.rs | 14 +- .../group/controller_impls/date_controller.rs | 12 +- .../controller_impls/default_controller.rs | 9 +- .../group/controller_impls/url_controller.rs | 13 +- .../src/services/group/entities.rs | 10 +- .../src/services/setting/entities.rs | 5 + .../tests/database/layout_test/test.rs | 1 + 35 files changed, 1196 insertions(+), 745 deletions(-) create mode 100644 frontend/appflowy_flutter/integration_test/board/board_hide_groups_test.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/database_view/board/application/group.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/database_view/board/application/ungrouped_items_bloc.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/ungrouped_items_button.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/widgets/board_hidden_groups.dart create mode 100644 frontend/resources/flowy_icons/16x/hamburger_s.svg create mode 100644 frontend/resources/flowy_icons/16x/pull_left_outlined.svg diff --git a/frontend/appflowy_flutter/integration_test/board/board_add_row_test.dart b/frontend/appflowy_flutter/integration_test/board/board_add_row_test.dart index 0ef813bec618..fe58e3836bb9 100644 --- a/frontend/appflowy_flutter/integration_test/board/board_add_row_test.dart +++ b/frontend/appflowy_flutter/integration_test/board/board_add_row_test.dart @@ -1,4 +1,5 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/plugins/database_view/board/presentation/widgets/board_column_header.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_board/appflowy_board.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; @@ -32,10 +33,12 @@ void main() { await tester.tap( find .descendant( - of: find.byType(AppFlowyGroupHeader), - matching: find.byType(FlowySvg), + of: find.byType(BoardColumnHeader), + matching: find.byWidgetPredicate( + (widget) => widget is FlowySvg && widget.svg == FlowySvgs.add_s, + ), ) - .first, + .at(1), ); await tester.pumpAndSettle(); @@ -77,7 +80,7 @@ void main() { of: find.byType(AppFlowyGroupFooter), matching: find.byType(FlowySvg), ) - .first, + .at(1), ); await tester.pumpAndSettle(); diff --git a/frontend/appflowy_flutter/integration_test/board/board_hide_groups_test.dart b/frontend/appflowy_flutter/integration_test/board/board_hide_groups_test.dart new file mode 100644 index 000000000000..46d91766d983 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/board/board_hide_groups_test.dart @@ -0,0 +1,98 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/plugins/database_view/board/presentation/widgets/board_column_header.dart'; +import 'package:appflowy/plugins/database_view/board/presentation/widgets/board_hidden_groups.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../util/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('board hide groups test', () { + testWidgets('expand/collapse hidden groups', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Board); + + final collapseFinder = find.byFlowySvg(FlowySvgs.pull_left_outlined_s); + final expandFinder = find.byFlowySvg(FlowySvgs.hamburger_s_s); + + // Is expanded by default + expect(collapseFinder, findsOneWidget); + expect(expandFinder, findsNothing); + + // Collapse hidden groups + await tester.tap(collapseFinder); + await tester.pumpAndSettle(); + + // Is collapsed + expect(collapseFinder, findsNothing); + expect(expandFinder, findsOneWidget); + + // Expand hidden groups + await tester.tap(expandFinder); + await tester.pumpAndSettle(); + + // Is expanded + expect(collapseFinder, findsOneWidget); + expect(expandFinder, findsNothing); + }); + + testWidgets('hide first group, and show it again', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Board); + + // Tap the options of the first group + final optionsFinder = find + .descendant( + of: find.byType(BoardColumnHeader), + matching: find.byFlowySvg(FlowySvgs.details_horizontal_s), + ) + .first; + + await tester.tap(optionsFinder); + await tester.pumpAndSettle(); + + // Tap the hide option + await tester.tap(find.byFlowySvg(FlowySvgs.hide_s)); + await tester.pumpAndSettle(); + + int shownGroups = + tester.widgetList(find.byType(BoardColumnHeader)).length; + + // We still show Doing, Done, No Status + expect(shownGroups, 3); + + final hiddenCardFinder = find.byType(HiddenGroupCard); + await tester.hoverOnWidget(hiddenCardFinder); + await tester.tap(find.byFlowySvg(FlowySvgs.show_m)); + await tester.pumpAndSettle(); + + shownGroups = tester.widgetList(find.byType(BoardColumnHeader)).length; + expect(shownGroups, 4); + }); + }); +} + +extension FlowySvgFinder on CommonFinders { + Finder byFlowySvg(FlowySvgData svg) => _FlowySvgFinder(svg); +} + +class _FlowySvgFinder extends MatchFinder { + _FlowySvgFinder(this.svg); + + final FlowySvgData svg; + + @override + String get description => 'flowy_svg "$svg"'; + + @override + bool matches(Element candidate) { + final Widget widget = candidate.widget; + return widget is FlowySvg && widget.svg == svg; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart index 30694c03d16a..5a4ee76e6b34 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart @@ -6,6 +6,7 @@ import 'package:appflowy/plugins/database_view/application/field/field_info.dart import 'package:appflowy/plugins/database_view/application/group/group_service.dart'; import 'package:appflowy/plugins/database_view/application/row/row_service.dart'; import 'package:appflowy_board/appflowy_board.dart'; +import 'package:collection/collection.dart'; import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; @@ -14,6 +15,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:protobuf/protobuf.dart' hide FieldInfo; import '../../application/field/field_controller.dart'; import '../../application/row/row_cache.dart'; @@ -23,12 +25,13 @@ import 'group_controller.dart'; part 'board_bloc.freezed.dart'; class BoardBloc extends Bloc<BoardEvent, BoardState> { - late final GroupBackendService groupBackendSvc; final DatabaseController databaseController; - late final AppFlowyBoardController boardController; final LinkedHashMap<String, GroupController> groupControllers = LinkedHashMap(); - GroupPB? ungroupedGroup; + final List<GroupPB> groupList = []; + + late final GroupBackendService groupBackendSvc; + late final AppFlowyBoardController boardController; FieldController get fieldController => databaseController.fieldController; String get viewId => databaseController.viewId; @@ -82,10 +85,8 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> { groupId: groupId, startRowId: startRowId, ); - result.fold( - (_) {}, - (err) => Log.error(err), - ); + + result.fold((_) {}, (err) => Log.error(err)); }, createHeaderRow: (String groupId) async { final result = await databaseController.createRow( @@ -93,10 +94,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> { fromBeginning: true, ); - result.fold( - (_) {}, - (err) => Log.error(err), - ); + result.fold((_) {}, (err) => Log.error(err)); }, createGroup: (name) async { final result = await groupBackendSvc.createGroup(name: name); @@ -115,6 +113,48 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> { ); _groupItemStartEditing(group, row, true); }, + didReceiveGridUpdate: (DatabasePB grid) { + emit(state.copyWith(grid: Some(grid))); + }, + didReceiveError: (FlowyError error) { + emit(state.copyWith(noneOrError: some(error))); + }, + didReceiveGroups: (List<GroupPB> groups) { + final hiddenGroups = _filterHiddenGroups(hideUngrouped, groups); + emit( + state.copyWith( + hiddenGroups: hiddenGroups, + groupIds: groups.map((group) => group.groupId).toList(), + ), + ); + }, + didUpdateLayoutSettings: (layoutSettings) { + final hiddenGroups = _filterHiddenGroups(hideUngrouped, groupList); + emit( + state.copyWith( + layoutSettings: layoutSettings, + hiddenGroups: hiddenGroups, + ), + ); + }, + toggleGroupVisibility: (GroupPB group, bool isVisible) async { + await _toggleGroupVisibility(group, isVisible); + }, + toggleHiddenSectionVisibility: (isVisible) async { + final newLayoutSettings = state.layoutSettings!; + newLayoutSettings.freeze(); + + final newLayoutSetting = newLayoutSettings.rebuild( + (message) => message.collapseHiddenGroups = isVisible, + ); + + await databaseController.updateLayoutSetting( + boardLayoutSetting: newLayoutSetting, + ); + }, + reorderGroup: (fromGroupId, toGroupId) async { + _reorderGroup(fromGroupId, toGroupId, emit); + }, startEditingRow: (group, row) { emit( state.copyWith( @@ -140,22 +180,6 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> { emit(state.copyWith(isEditingRow: false, editingRow: null)); } }, - didReceiveGridUpdate: (DatabasePB grid) { - emit(state.copyWith(grid: Some(grid))); - }, - didReceiveError: (FlowyError error) { - emit(state.copyWith(noneOrError: some(error))); - }, - didReceiveGroups: (List<GroupPB> groups) { - emit( - state.copyWith( - groupIds: groups.map((group) => group.groupId).toList(), - ), - ); - }, - didUpdateLayoutSettings: (layoutSettings) { - emit(state.copyWith(layoutSettings: layoutSettings)); - }, startEditingHeader: (String groupId) { emit( state.copyWith(isEditingHeader: true, editingHeaderId: groupId), @@ -167,7 +191,6 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> { groupId: groupId, name: groupName, ); - emit(state.copyWith(isEditingHeader: false)); }, ); @@ -178,13 +201,50 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> { void _groupItemStartEditing(GroupPB group, RowMetaPB row, bool isEdit) { final fieldInfo = fieldController.getField(group.fieldId); if (fieldInfo == null) { - Log.warn("fieldInfo should not be null"); - return; + return Log.warn("fieldInfo should not be null"); } boardController.enableGroupDragging(!isEdit); } + Future<void> _toggleGroupVisibility(GroupPB group, bool isVisible) async { + if (group.isDefault) { + final newLayoutSettings = state.layoutSettings!; + newLayoutSettings.freeze(); + + final newLayoutSetting = newLayoutSettings.rebuild( + (message) => message.hideUngroupedColumn = !isVisible, + ); + + return databaseController.updateLayoutSetting( + boardLayoutSetting: newLayoutSetting, + ); + } + + await groupBackendSvc.updateGroup( + fieldId: groupControllers.values.first.group.fieldId, + groupId: group.groupId, + visible: isVisible, + ); + } + + Future<void> _reorderGroup( + String fromGroupId, + String toGroupId, + Emitter<BoardState> emit, + ) async { + final fromIndex = groupList.indexWhere((g) => g.groupId == fromGroupId); + final toIndex = groupList.indexWhere((g) => g.groupId == toGroupId); + final group = groupList.removeAt(fromIndex); + groupList.insert(toIndex, group); + add(BoardEvent.didReceiveGroups(groupList)); + final result = await databaseController.moveGroup( + fromGroupId: fromGroupId, + toGroupId: toGroupId, + ); + result.fold((l) => {}, (err) => Log.error(err)); + } + @override Future<void> close() async { for (final controller in groupControllers.values) { @@ -193,40 +253,45 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> { return super.close(); } + bool get hideUngrouped => + databaseController.databaseLayoutSetting?.board.hideUngroupedColumn ?? + false; + + FieldType? get groupingFieldType { + final fieldInfo = databaseController.fieldController.fieldInfos + .firstWhereOrNull((field) => field.isGroupField); + + return fieldInfo?.fieldType; + } + void initializeGroups(List<GroupPB> groups) { for (final controller in groupControllers.values) { controller.dispose(); } + groupControllers.clear(); boardController.clear(); - - final ungroupedGroupIndex = - groups.indexWhere((group) => group.groupId == group.fieldId); - - if (ungroupedGroupIndex != -1) { - ungroupedGroup = groups[ungroupedGroupIndex]; - final group = groups.removeAt(ungroupedGroupIndex); - if (!(state.layoutSettings?.hideUngroupedColumn ?? false)) { - groups.add(group); - } - } + groupList.clear(); + groupList.addAll(groups); boardController.addGroups( groups - .where((group) => fieldController.getField(group.fieldId) != null) - .map((group) => initializeGroupData(group)) + .where( + (group) => + fieldController.getField(group.fieldId) != null && + (group.isVisible || (group.isDefault && !hideUngrouped)), + ) + .map((group) => _initializeGroupData(group)) .toList(), ); for (final group in groups) { - final controller = initializeGroupController(group); - groupControllers[controller.group.groupId] = (controller); + final controller = _initializeGroupController(group); + groupControllers[controller.group.groupId] = controller; } } - RowCache? getRowCache() { - return databaseController.rowCache; - } + RowCache? getRowCache() => databaseController.rowCache; void _startListening() { final onDatabaseChanged = DatabaseCallbacks( @@ -238,15 +303,22 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> { ); final onLayoutSettingsChanged = DatabaseLayoutSettingCallbacks( onLayoutSettingsChanged: (layoutSettings) { - if (isClosed || !layoutSettings.hasBoard()) { + if (isClosed) { return; } - if (ungroupedGroup != null) { + final index = groupList.indexWhere((element) => element.isDefault); + if (index != -1) { if (layoutSettings.board.hideUngroupedColumn) { - boardController.removeGroup(ungroupedGroup!.fieldId); + boardController.removeGroup(groupList[index].fieldId); } else { - final newGroup = initializeGroupData(ungroupedGroup!); - boardController.addGroup(newGroup); + final newGroup = _initializeGroupData(groupList[index]); + final visibleGroups = [...groupList] + ..retainWhere((g) => g.isVisible || g.isDefault); + final indexInVisibleGroups = + visibleGroups.indexWhere((g) => g.isDefault); + if (indexInVisibleGroups != -1) { + boardController.insertGroup(indexInVisibleGroups, newGroup); + } } } add(BoardEvent.didUpdateLayoutSettings(layoutSettings.board)); @@ -254,30 +326,72 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> { ); final onGroupChanged = GroupCallbacks( onGroupByField: (groups) { - if (isClosed) return; - ungroupedGroup = null; + if (isClosed) { + return; + } + initializeGroups(groups); add(BoardEvent.didReceiveGroups(groups)); }, onDeleteGroup: (groupIds) { - if (isClosed) return; + if (isClosed) { + return; + } + boardController.removeGroups(groupIds); + groupList.removeWhere((group) => groupIds.contains(group.groupId)); + add(BoardEvent.didReceiveGroups(groupList)); }, onInsertGroup: (insertGroups) { - if (isClosed) return; + if (isClosed) { + return; + } + final group = insertGroups.group; - final newGroup = initializeGroupData(group); - final controller = initializeGroupController(group); - groupControllers[controller.group.groupId] = (controller); + final newGroup = _initializeGroupData(group); + final controller = _initializeGroupController(group); + groupControllers[controller.group.groupId] = controller; boardController.addGroup(newGroup); + groupList.insert(insertGroups.index, group); + add(BoardEvent.didReceiveGroups(groupList)); }, onUpdateGroup: (updatedGroups) { - if (isClosed) return; + if (isClosed) { + return; + } + for (final group in updatedGroups) { + // see if the column is already in the board + + final index = groupList.indexWhere((g) => g.groupId == group.groupId); + if (index == -1) continue; final columnController = boardController.getGroupController(group.groupId); - columnController?.updateGroupName(group.groupName); + if (columnController != null) { + // remove the group or update its name + columnController.updateGroupName(group.groupName); + if (!group.isVisible) { + boardController.removeGroup(group.groupId); + } + } else { + final newGroup = _initializeGroupData(group); + final visibleGroups = [...groupList]..retainWhere( + (g) => + g.isVisible || + g.isDefault && !hideUngrouped || + g.groupId == group.groupId, + ); + final indexInVisibleGroups = + visibleGroups.indexWhere((g) => g.groupId == group.groupId); + if (indexInVisibleGroups != -1) { + boardController.insertGroup(indexInVisibleGroups, newGroup); + } + } + + groupList.removeAt(index); + groupList.insert(index, group); } + add(BoardEvent.didReceiveGroups(groupList)); }, ); @@ -315,24 +429,35 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> { ); } - GroupController initializeGroupController(GroupPB group) { + GroupController _initializeGroupController(GroupPB group) { final delegate = GroupControllerDelegateImpl( controller: boardController, fieldController: fieldController, - onNewColumnItem: (groupId, row, index) { - add(BoardEvent.didCreateRow(group, row, index)); - }, + onNewColumnItem: (groupId, row, index) => + add(BoardEvent.didCreateRow(group, row, index)), ); + final controller = GroupController( viewId: state.viewId, group: group, delegate: delegate, + onGroupChanged: (newGroup) { + if (isClosed) return; + + final index = + groupList.indexWhere((g) => g.groupId == newGroup.groupId); + if (index != -1) { + groupList.removeAt(index); + groupList.insert(index, newGroup); + add(BoardEvent.didReceiveGroups(groupList)); + } + }, ); - controller.startListening(); - return controller; + + return controller..startListening(); } - AppFlowyGroupData initializeGroupData(GroupPB group) { + AppFlowyGroupData _initializeGroupData(GroupPB group) { return AppFlowyGroupData( id: group.groupId, name: group.groupName, @@ -365,6 +490,14 @@ class BoardEvent with _$BoardEvent { RowMetaPB row, ) = _StartEditRow; const factory BoardEvent.endEditingRow(RowId rowId) = _EndEditRow; + const factory BoardEvent.toggleGroupVisibility( + GroupPB group, + bool isVisible, + ) = _ToggleGroupVisibility; + const factory BoardEvent.toggleHiddenSectionVisibility(bool isVisible) = + _ToggleHiddenSectionVisibility; + const factory BoardEvent.reorderGroup(String fromGroupId, String toGroupId) = + _ReorderGroup; const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError; const factory BoardEvent.didReceiveGridUpdate( DatabasePB grid, @@ -383,12 +516,13 @@ class BoardState with _$BoardState { required Option<DatabasePB> grid, required List<String> groupIds, required bool isEditingHeader, - String? editingHeaderId, required bool isEditingRow, - BoardEditingRow? editingRow, required LoadingState loadingState, required Option<FlowyError> noneOrError, required BoardLayoutSettingPB? layoutSettings, + String? editingHeaderId, + BoardEditingRow? editingRow, + required List<GroupPB> hiddenGroups, }) = _BoardState; factory BoardState.initial(String viewId) => BoardState( @@ -400,9 +534,16 @@ class BoardState with _$BoardState { noneOrError: none(), loadingState: const LoadingState.loading(), layoutSettings: null, + hiddenGroups: [], ); } +List<GroupPB> _filterHiddenGroups(bool hideUngrouped, List<GroupPB> groups) { + return [...groups]..retainWhere( + (group) => !group.isVisible || group.isDefault && hideUngrouped, + ); +} + class GroupItem extends AppFlowyGroupItem { final RowMetaPB row; final FieldInfo fieldInfo; @@ -430,12 +571,16 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate { required this.onNewColumnItem, }); + @override + bool hasGroup(String groupId) { + return controller.groupIds.contains(groupId); + } + @override void insertRow(GroupPB group, RowMetaPB row, int? index) { final fieldInfo = fieldController.getField(group.fieldId); if (fieldInfo == null) { - Log.warn("fieldInfo should not be null"); - return; + return Log.warn("fieldInfo should not be null"); } if (index != null) { @@ -454,17 +599,16 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate { } @override - void removeRow(GroupPB group, RowId rowId) { - controller.removeGroupItem(group.groupId, rowId.toString()); - } + void removeRow(GroupPB group, RowId rowId) => + controller.removeGroupItem(group.groupId, rowId.toString()); @override void updateRow(GroupPB group, RowMetaPB row) { final fieldInfo = fieldController.getField(group.fieldId); if (fieldInfo == null) { - Log.warn("fieldInfo should not be null"); - return; + return Log.warn("fieldInfo should not be null"); } + controller.updateGroupItem( group.groupId, GroupItem( @@ -478,20 +622,17 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate { void addNewRow(GroupPB group, RowMetaPB row, int? index) { final fieldInfo = fieldController.getField(group.fieldId); if (fieldInfo == null) { - Log.warn("fieldInfo should not be null"); - return; + return Log.warn("fieldInfo should not be null"); } - final item = GroupItem( - row: row, - fieldInfo: fieldInfo, - draggable: false, - ); + + final item = GroupItem(row: row, fieldInfo: fieldInfo, draggable: false); if (index != null) { controller.insertGroupItem(group.groupId, index, item); } else { controller.addGroupItem(group.groupId, item); } + onNewColumnItem(group.groupId, row, index); } } @@ -509,27 +650,26 @@ class BoardEditingRow { } class GroupData { - final GroupPB group; - final FieldInfo fieldInfo; GroupData({ required this.group, required this.fieldInfo, }); - CheckboxGroup? asCheckboxGroup() { - if (fieldType != FieldType.Checkbox) return null; - return CheckboxGroup(group); - } + final GroupPB group; + final FieldInfo fieldInfo; + + CheckboxGroup? asCheckboxGroup() => + fieldType == FieldType.Checkbox ? CheckboxGroup(group) : null; FieldType get fieldType => fieldInfo.fieldType; } class CheckboxGroup { - final GroupPB group; + const CheckboxGroup(this.group); - CheckboxGroup(this.group); + final GroupPB group; -// Hardcode value: "Yes" that equal to the value defined in Rust -// pub const CHECK: &str = "Yes"; + // Hardcode value: "Yes" that equal to the value defined in Rust + // pub const CHECK: &str = "Yes"; bool get isCheck => group.groupId == "Yes"; } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/group.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/group.dart deleted file mode 100644 index feefa34db70f..000000000000 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/group.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; - -class BoardGroupService { - final String viewId; - FieldPB? groupField; - - BoardGroupService(this.viewId); - - void setGroupField(FieldPB field) { - groupField = field; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/group_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/group_controller.dart index 0e4a315de379..6f7f359992e2 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/group_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/group_controller.dart @@ -7,10 +7,12 @@ import 'dart:typed_data'; import 'package:appflowy/core/notification/grid_notification.dart'; import 'package:flowy_infra/notifier.dart'; import 'package:dartz/dartz.dart'; +import 'package:protobuf/protobuf.dart'; typedef OnGroupError = void Function(FlowyError); abstract class GroupControllerDelegate { + bool hasGroup(String groupId); void removeRow(GroupPB group, RowId rowId); void insertRow(GroupPB group, RowMetaPB row, int? index); void updateRow(GroupPB group, RowMetaPB row); @@ -18,14 +20,16 @@ abstract class GroupControllerDelegate { } class GroupController { - final GroupPB group; + GroupPB group; final SingleGroupListener _listener; final GroupControllerDelegate delegate; + final void Function(GroupPB group) onGroupChanged; GroupController({ required String viewId, required this.group, required this.delegate, + required this.onGroupChanged, }) : _listener = SingleGroupListener(group); RowMetaPB? rowAtIndex(int index) { @@ -46,37 +50,52 @@ class GroupController { onGroupChanged: (result) { result.fold( (GroupRowsNotificationPB changeset) { + final newItems = [...group.rows]; + final isGroupExist = delegate.hasGroup(group.groupId); for (final deletedRow in changeset.deletedRows) { - group.rows.removeWhere((rowPB) => rowPB.id == deletedRow); - delegate.removeRow(group, deletedRow); + newItems.removeWhere((rowPB) => rowPB.id == deletedRow); + if (isGroupExist) { + delegate.removeRow(group, deletedRow); + } } 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.rowMeta); + newItems.length > insertedRow.index) { + newItems.insert(insertedRow.index, insertedRow.rowMeta); } else { - group.rows.add(insertedRow.rowMeta); + newItems.add(insertedRow.rowMeta); } - if (insertedRow.isNew) { - delegate.addNewRow(group, insertedRow.rowMeta, index); - } else { - delegate.insertRow(group, insertedRow.rowMeta, index); + if (isGroupExist) { + if (insertedRow.isNew) { + delegate.addNewRow(group, insertedRow.rowMeta, index); + } else { + delegate.insertRow(group, insertedRow.rowMeta, index); + } } } for (final updatedRow in changeset.updatedRows) { - final index = group.rows.indexWhere( + final index = newItems.indexWhere( (rowPB) => rowPB.id == updatedRow.id, ); if (index != -1) { - group.rows[index] = updatedRow; - delegate.updateRow(group, updatedRow); + newItems[index] = updatedRow; + if (isGroupExist) { + delegate.updateRow(group, updatedRow); + } } } + + group.freeze(); + group = group.rebuild((group) { + group.rows.clear(); + group.rows.addAll(newItems); + }); + onGroupChanged(group); }, (err) => Log.error(err), ); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/ungrouped_items_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/ungrouped_items_bloc.dart deleted file mode 100644 index b2510087a331..000000000000 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/ungrouped_items_bloc.dart +++ /dev/null @@ -1,112 +0,0 @@ -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:bloc/bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -import 'group_controller.dart'; - -part 'ungrouped_items_bloc.freezed.dart'; - -class UngroupedItemsBloc - extends Bloc<UngroupedItemsEvent, UngroupedItemsState> { - UngroupedItemsListener? listener; - - UngroupedItemsBloc({required GroupPB group}) - : super(UngroupedItemsState(ungroupedItems: group.rows)) { - on<UngroupedItemsEvent>( - (event, emit) { - event.when( - initial: () { - listener = UngroupedItemsListener( - initialGroup: group, - onGroupChanged: (ungroupedItems) { - if (isClosed) return; - add( - UngroupedItemsEvent.updateGroup( - ungroupedItems: ungroupedItems, - ), - ); - }, - )..startListening(); - }, - updateGroup: (newItems) => - emit(UngroupedItemsState(ungroupedItems: newItems)), - ); - }, - ); - } -} - -@freezed -class UngroupedItemsEvent with _$UngroupedItemsEvent { - const factory UngroupedItemsEvent.initial() = _Initial; - const factory UngroupedItemsEvent.updateGroup({ - required List<RowMetaPB> ungroupedItems, - }) = _UpdateGroup; -} - -@freezed -class UngroupedItemsState with _$UngroupedItemsState { - const factory UngroupedItemsState({ - required List<RowMetaPB> ungroupedItems, - }) = _UngroupedItemsState; -} - -class UngroupedItemsListener { - List<RowMetaPB> _ungroupedItems; - final SingleGroupListener _listener; - final void Function(List<RowMetaPB> items) onGroupChanged; - - UngroupedItemsListener({ - required GroupPB initialGroup, - required this.onGroupChanged, - }) : _ungroupedItems = List<RowMetaPB>.from(initialGroup.rows), - _listener = SingleGroupListener(initialGroup); - - void startListening() { - _listener.start( - onGroupChanged: (result) { - result.fold( - (GroupRowsNotificationPB changeset) { - final newItems = List<RowMetaPB>.from(_ungroupedItems); - for (final deletedRow in changeset.deletedRows) { - newItems.removeWhere((rowPB) => rowPB.id == deletedRow); - } - - for (final insertedRow in changeset.insertedRows) { - final index = newItems.indexWhere( - (rowPB) => rowPB.id == insertedRow.rowMeta.id, - ); - if (index != -1) { - continue; - } - if (insertedRow.hasIndex() && - newItems.length > insertedRow.index) { - newItems.insert(insertedRow.index, insertedRow.rowMeta); - } else { - newItems.add(insertedRow.rowMeta); - } - } - - for (final updatedRow in changeset.updatedRows) { - final index = newItems.indexWhere( - (rowPB) => rowPB.id == updatedRow.id, - ); - - if (index != -1) { - newItems[index] = updatedRow; - } - } - onGroupChanged.call(newItems); - _ungroupedItems = newItems; - }, - (err) => Log.error(err), - ); - }, - ); - } - - Future<void> dispose() async { - _listener.stop(); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart index 3c1371d8bb71..4bae94b30a75 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart @@ -1,5 +1,3 @@ -// ignore_for_file: unused_field - import 'dart:collection'; import 'package:appflowy/generated/flowy_svgs.g.dart'; @@ -9,7 +7,7 @@ import 'package:appflowy/plugins/database_view/application/field/field_controlle import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; import 'package:appflowy/plugins/database_view/application/row/row_controller.dart'; import 'package:appflowy/plugins/database_view/board/presentation/widgets/board_column_header.dart'; -import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart'; import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; @@ -30,7 +28,7 @@ import '../../widgets/row/cell_builder.dart'; import '../application/board_bloc.dart'; import '../../widgets/card/card.dart'; import 'toolbar/board_setting_bar.dart'; -import 'ungrouped_items_button.dart'; +import 'widgets/board_hidden_groups.dart'; class BoardPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder { @override @@ -39,46 +37,38 @@ class BoardPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder { ViewPB view, DatabaseController controller, bool shrinkWrap, - ) { - return BoardPage( - key: _makeValueKey(controller), - view: view, - databaseController: controller, - ); - } + ) => + BoardPage(view: view, databaseController: controller); @override - Widget settingBar(BuildContext context, DatabaseController controller) { - return BoardSettingBar( - key: _makeValueKey(controller), - databaseController: controller, - ); - } + Widget settingBar(BuildContext context, DatabaseController controller) => + BoardSettingBar( + key: _makeValueKey(controller), + databaseController: controller, + ); @override Widget settingBarExtension( BuildContext context, DatabaseController controller, - ) { - return SizedBox.fromSize(); - } + ) => + const SizedBox.shrink(); - ValueKey _makeValueKey(DatabaseController controller) { - return ValueKey(controller.viewId); - } + ValueKey _makeValueKey(DatabaseController controller) => + ValueKey(controller.viewId); } class BoardPage extends StatelessWidget { - final DatabaseController databaseController; BoardPage({ required this.view, required this.databaseController, - Key? key, this.onEditStateChanged, }) : super(key: ValueKey(view.id)); final ViewPB view; + final DatabaseController databaseController; + /// Called when edit state changed final VoidCallback? onEditStateChanged; @@ -91,23 +81,18 @@ class BoardPage extends StatelessWidget { )..add(const BoardEvent.initial()), child: BlocBuilder<BoardBloc, BoardState>( buildWhen: (p, c) => p.loadingState != c.loadingState, - builder: (context, state) { - return state.loadingState.map( - loading: (_) => - const Center(child: CircularProgressIndicator.adaptive()), - finish: (result) { - return result.successOrFail.fold( - (_) => BoardContent( - onEditStateChanged: onEditStateChanged, - ), - (err) => FlowyErrorPage.message( - err.toString(), - howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), - ), - ); - }, - ); - }, + builder: (context, state) => state.loadingState.map( + loading: (_) => const Center( + child: CircularProgressIndicator.adaptive(), + ), + finish: (result) => result.successOrFail.fold( + (_) => BoardContent(onEditStateChanged: onEditStateChanged), + (err) => FlowyErrorPage.message( + err.toString(), + howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), + ), + ), + ), ), ); } @@ -126,12 +111,14 @@ class BoardContent extends StatefulWidget { } class _BoardContentState extends State<BoardContent> { - late AppFlowyBoardScrollController scrollManager; - late final ScrollController scrollController; final renderHook = RowCardRenderHook<String>(); + late final ScrollController scrollController; + late final AppFlowyBoardScrollController scrollManager; final config = const AppFlowyBoardConfig( groupBackgroundColor: Color(0xffF7F8FC), + headerPadding: EdgeInsets.symmetric(horizontal: 8), + cardPadding: EdgeInsets.symmetric(horizontal: 4, vertical: 3), ); @override @@ -162,43 +149,36 @@ class _BoardContentState extends State<BoardContent> { }, child: BlocBuilder<BoardBloc, BoardState>( builder: (context, state) { + final showCreateGroupButton = + context.read<BoardBloc>().groupingFieldType!.canCreateNewGroup; return Padding( - padding: GridSize.contentInsets, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - const VSpace(8.0), - if (state.layoutSettings?.hideUngroupedColumn ?? false) - _buildBoardHeader(context), - Expanded( - child: AppFlowyBoard( - boardScrollController: scrollManager, - scrollController: scrollController, - controller: context.read<BoardBloc>().boardController, - headerBuilder: (_, groupData) => - BlocProvider<BoardBloc>.value( - value: context.read<BoardBloc>(), - child: BoardColumnHeader( - groupData: groupData, - margin: config.headerPadding, - ), - ), - footerBuilder: _buildFooter, - trailing: BoardTrailing(scrollController: scrollController), - cardBuilder: (_, column, columnItem) => _buildCard( - context, - column, - columnItem, - ), - groupConstraints: const BoxConstraints.tightFor(width: 300), - config: AppFlowyBoardConfig( - groupBackgroundColor: - Theme.of(context).colorScheme.surfaceVariant, - ), - ), - ) - ], + padding: const EdgeInsets.only(top: 8.0), + child: AppFlowyBoard( + boardScrollController: scrollManager, + scrollController: scrollController, + controller: context.read<BoardBloc>().boardController, + groupConstraints: const BoxConstraints.tightFor(width: 300), + config: const AppFlowyBoardConfig( + groupPadding: EdgeInsets.symmetric(horizontal: 4), + groupItemPadding: EdgeInsets.symmetric(horizontal: 4), + ), + leading: HiddenGroupsColumn(margin: config.headerPadding), + trailing: showCreateGroupButton + ? BoardTrailing(scrollController: scrollController) + : null, + headerBuilder: (_, groupData) => BlocProvider<BoardBloc>.value( + value: context.read<BoardBloc>(), + child: BoardColumnHeader( + groupData: groupData, + margin: config.headerPadding, + ), + ), + footerBuilder: _buildFooter, + cardBuilder: (_, column, columnItem) => _buildCard( + context, + column, + columnItem, + ), ), ); }, @@ -206,19 +186,6 @@ class _BoardContentState extends State<BoardContent> { ); } - Widget _buildBoardHeader(BuildContext context) { - return const Padding( - padding: EdgeInsets.only(bottom: 8.0), - child: SizedBox( - height: 24, - child: Align( - alignment: AlignmentDirectional.centerEnd, - child: UngroupedItemsButton(), - ), - ), - ); - } - void _handleEditStateChanged(BoardState state, BuildContext context) { if (state.isEditingRow && state.editingRow != null) { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -231,25 +198,24 @@ class _BoardContentState extends State<BoardContent> { Widget _buildFooter(BuildContext context, AppFlowyGroupData columnData) { return AppFlowyGroupFooter( + height: 50, + margin: config.footerPadding, icon: SizedBox( height: 20, width: 20, child: FlowySvg( FlowySvgs.add_s, - color: Theme.of(context).iconTheme.color, + color: Theme.of(context).hintColor, ), ), title: FlowyText.medium( LocaleKeys.board_column_createNewCard.tr(), + color: Theme.of(context).hintColor, fontSize: 14, ), - height: 50, - margin: config.footerPadding, - onAddButtonClick: () { - context.read<BoardBloc>().add( - BoardEvent.createBottomRow(columnData.id), - ); - }, + onAddButtonClick: () => context + .read<BoardBloc>() + .add(BoardEvent.createBottomRow(columnData.id)), ); } @@ -307,15 +273,27 @@ class _BoardContentState extends State<BoardContent> { } BoxDecoration _makeBoxDecoration(BuildContext context) { - final borderSide = BorderSide( - color: Theme.of(context).dividerColor, - width: 1.0, - ); - final isLightMode = Theme.of(context).brightness == Brightness.light; return BoxDecoration( color: Theme.of(context).colorScheme.surface, - border: isLightMode ? Border.fromBorderSide(borderSide) : null, borderRadius: const BorderRadius.all(Radius.circular(6)), + border: Border.fromBorderSide( + BorderSide( + color: Theme.of(context).dividerColor, + width: 1.4, + ), + ), + boxShadow: [ + BoxShadow( + blurRadius: 4, + spreadRadius: 0, + color: const Color(0xFF1F2329).withOpacity(0.02), + ), + BoxShadow( + blurRadius: 4, + spreadRadius: -2, + color: const Color(0xFF1F2329).withOpacity(0.02), + ), + ], ); } @@ -343,40 +321,37 @@ class _BoardContentState extends State<BoardContent> { FlowyOverlay.show( context: context, - builder: (BuildContext context) { - return RowDetailPage( - cellBuilder: GridCellBuilder(cellCache: dataController.cellCache), - rowController: dataController, - ); - }, + builder: (_) => RowDetailPage( + cellBuilder: GridCellBuilder(cellCache: dataController.cellCache), + rowController: dataController, + ), ); } } class BoardTrailing extends StatefulWidget { + const BoardTrailing({super.key, required this.scrollController}); + final ScrollController scrollController; - const BoardTrailing({required this.scrollController, super.key}); @override State<BoardTrailing> createState() => _BoardTrailingState(); } class _BoardTrailingState extends State<BoardTrailing> { - bool isEditing = false; - late final TextEditingController _textController; + final TextEditingController _textController = TextEditingController(); late final FocusNode _focusNode; + bool isEditing = false; + void _cancelAddNewGroup() { _textController.clear(); - setState(() { - isEditing = false; - }); + setState(() => isEditing = false); } @override void initState() { super.initState(); - _textController = TextEditingController(); _focusNode = FocusNode( onKeyEvent: (node, event) { if (_focusNode.hasFocus && @@ -406,7 +381,7 @@ class _BoardTrailingState extends State<BoardTrailing> { }); return Padding( - padding: const EdgeInsets.only(left: 8.0), + padding: const EdgeInsets.only(left: 8.0, top: 12), child: Align( alignment: AlignmentDirectional.topStart, child: AnimatedSwitcher( @@ -448,9 +423,7 @@ class _BoardTrailingState extends State<BoardTrailing> { width: 26, icon: const FlowySvg(FlowySvgs.add_s), iconColorOnHover: Theme.of(context).colorScheme.onSurface, - onPressed: () => setState(() { - isEditing = true; - }), + onPressed: () => setState(() => isEditing = true), ), ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/ungrouped_items_button.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/ungrouped_items_button.dart deleted file mode 100644 index c1582d29f752..000000000000 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/ungrouped_items_button.dart +++ /dev/null @@ -1,237 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart'; -import 'package:appflowy/plugins/database_view/application/field/field_info.dart'; -import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; -import 'package:appflowy/plugins/database_view/application/row/row_controller.dart'; -import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart'; -import 'package:appflowy/plugins/database_view/board/application/ungrouped_items_bloc.dart'; -import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart'; -import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart'; -import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart'; -import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart'; -import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; -import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class UngroupedItemsButton extends StatefulWidget { - const UngroupedItemsButton({super.key}); - - @override - State<UngroupedItemsButton> createState() => _UnscheduledEventsButtonState(); -} - -class _UnscheduledEventsButtonState extends State<UngroupedItemsButton> { - late final PopoverController _popoverController; - - @override - void initState() { - super.initState(); - _popoverController = PopoverController(); - } - - @override - Widget build(BuildContext context) { - return BlocBuilder<BoardBloc, BoardState>( - builder: (context, boardState) { - final ungroupedGroup = context.watch<BoardBloc>().ungroupedGroup; - final databaseController = context.read<BoardBloc>().databaseController; - final primaryField = databaseController.fieldController.fieldInfos - .firstWhereOrNull((element) => element.isPrimary)!; - - if (ungroupedGroup == null) { - return const SizedBox.shrink(); - } - - return BlocProvider<UngroupedItemsBloc>( - create: (_) => UngroupedItemsBloc(group: ungroupedGroup) - ..add(const UngroupedItemsEvent.initial()), - child: BlocBuilder<UngroupedItemsBloc, UngroupedItemsState>( - builder: (context, state) { - return AppFlowyPopover( - direction: PopoverDirection.bottomWithCenterAligned, - triggerActions: PopoverTriggerFlags.none, - controller: _popoverController, - offset: const Offset(0, 8), - constraints: - const BoxConstraints(maxWidth: 282, maxHeight: 600), - child: FlowyTooltip( - message: LocaleKeys.board_ungroupedButtonTooltip.tr(), - child: OutlinedButton( - style: OutlinedButton.styleFrom( - shape: RoundedRectangleBorder( - side: BorderSide( - color: Theme.of(context).dividerColor, - width: 1, - ), - borderRadius: Corners.s6Border, - ), - side: BorderSide( - color: Theme.of(context).dividerColor, - width: 1, - ), - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - visualDensity: VisualDensity.compact, - ), - onPressed: () { - if (state.ungroupedItems.isNotEmpty) { - _popoverController.show(); - } - }, - child: FlowyText.regular( - "${LocaleKeys.board_ungroupedButtonText.tr()} (${state.ungroupedItems.length})", - fontSize: 10, - ), - ), - ), - popupBuilder: (context) { - return UngroupedItemList( - viewId: databaseController.viewId, - primaryField: primaryField, - rowCache: databaseController.rowCache, - ungroupedItems: state.ungroupedItems, - ); - }, - ); - }, - ), - ); - }, - ); - } -} - -class UngroupedItemList extends StatelessWidget { - final String viewId; - final FieldInfo primaryField; - final RowCache rowCache; - final List<RowMetaPB> ungroupedItems; - const UngroupedItemList({ - required this.viewId, - required this.primaryField, - required this.ungroupedItems, - required this.rowCache, - super.key, - }); - - @override - Widget build(BuildContext context) { - final cells = <Widget>[ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), - child: FlowyText.medium( - LocaleKeys.board_ungroupedItemsTitle.tr(), - fontSize: 10, - color: Theme.of(context).hintColor, - overflow: TextOverflow.ellipsis, - ), - ), - ...ungroupedItems.map( - (item) { - final rowController = RowController( - rowMeta: item, - viewId: viewId, - rowCache: rowCache, - ); - final renderHook = RowCardRenderHook<String>(); - renderHook.addTextCellHook((cellData, _, __) { - return BlocBuilder<TextCellBloc, TextCellState>( - builder: (context, state) { - final text = cellData.isEmpty - ? LocaleKeys.grid_row_titlePlaceholder.tr() - : cellData; - - if (text.isEmpty) { - return const SizedBox.shrink(); - } - - return Align( - alignment: Alignment.centerLeft, - child: FlowyText.medium( - text, - textAlign: TextAlign.left, - fontSize: 11, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ); - }, - ); - }); - return UngroupedItem( - cellContext: rowCache.loadCells(item)[primaryField.id]!, - primaryField: primaryField, - rowController: rowController, - cellBuilder: CardCellBuilder<String>(rowController.cellCache), - renderHook: renderHook, - onPressed: () { - FlowyOverlay.show( - context: context, - builder: (BuildContext context) { - return RowDetailPage( - cellBuilder: - GridCellBuilder(cellCache: rowController.cellCache), - rowController: rowController, - ); - }, - ); - PopoverContainer.of(context).close(); - }, - ); - }, - ) - ]; - - return ListView.separated( - itemBuilder: (context, index) => cells[index], - itemCount: cells.length, - separatorBuilder: (context, index) => - VSpace(GridSize.typeOptionSeparatorHeight), - shrinkWrap: true, - ); - } -} - -class UngroupedItem extends StatelessWidget { - final DatabaseCellContext cellContext; - final FieldInfo primaryField; - final RowController rowController; - final CardCellBuilder cellBuilder; - final RowCardRenderHook<String> renderHook; - final VoidCallback onPressed; - const UngroupedItem({ - super.key, - required this.cellContext, - required this.onPressed, - required this.cellBuilder, - required this.rowController, - required this.primaryField, - required this.renderHook, - }); - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 26, - child: FlowyButton( - margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), - text: cellBuilder.buildCell( - cellContext: cellContext, - renderHook: renderHook, - hasNotes: false, - ), - onTap: onPressed, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/widgets/board_column_header.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/widgets/board_column_header.dart index 2c86a1d4315d..b3fe562e4817 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/widgets/board_column_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/widgets/board_column_header.dart @@ -1,14 +1,15 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart'; import 'package:appflowy/plugins/database_view/widgets/card/define.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart'; import 'package:appflowy_board/appflowy_board.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -18,11 +19,11 @@ class BoardColumnHeader extends StatefulWidget { const BoardColumnHeader({ super.key, required this.groupData, - this.margin, + required this.margin, }); final AppFlowyGroupData groupData; - final EdgeInsets? margin; + final EdgeInsets margin; @override State<BoardColumnHeader> createState() => _BoardColumnHeaderState(); @@ -74,7 +75,7 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> { child: FlowyText.medium( widget.groupData.headerData.groupName, fontSize: 14, - overflow: TextOverflow.clip, + overflow: TextOverflow.ellipsis, ), ); @@ -84,22 +85,16 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> { fit: FlexFit.tight, child: FlowyTooltip( message: LocaleKeys.board_column_renameGroupTooltip.tr(), - child: FlowyHover( - style: HoverStyle( - hoverColor: Colors.transparent, - foregroundColorOnHover: - AFThemeExtension.of(context).textColor, - ), + child: MouseRegion( + cursor: SystemMouseCursors.click, child: GestureDetector( - onTap: () => context.read<BoardBloc>().add( - BoardEvent.startEditingHeader( - widget.groupData.id, - ), - ), + onTap: () => context + .read<BoardBloc>() + .add(BoardEvent.startEditingHeader(widget.groupData.id)), child: FlowyText.medium( widget.groupData.headerData.groupName, fontSize: 14, - overflow: TextOverflow.clip, + overflow: TextOverflow.ellipsis, ), ), ), @@ -112,22 +107,31 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> { title = _buildTextField(context); } - return AppFlowyGroupHeader( - title: title, - icon: _buildHeaderIcon(boardCustomData), - addIcon: SizedBox( - height: 20, - width: 20, - child: FlowySvg( - FlowySvgs.add_s, - color: Theme.of(context).iconTheme.color, + return Padding( + padding: widget.margin, + child: SizedBox( + height: 50, + child: Row( + children: [ + _buildHeaderIcon(boardCustomData), + title, + const HSpace(6), + _groupOptionsButton(context), + const HSpace(4), + FlowyTooltip( + message: LocaleKeys.board_column_addToColumnTopTooltip.tr(), + child: FlowyIconButton( + width: 20, + icon: const FlowySvg(FlowySvgs.add_s), + iconColorOnHover: Theme.of(context).colorScheme.onSurface, + onPressed: () => context + .read<BoardBloc>() + .add(BoardEvent.createHeaderRow(widget.groupData.id)), + ), + ), + ], ), ), - onAddButtonClick: () => context - .read<BoardBloc>() - .add(BoardEvent.createHeaderRow(widget.groupData.id)), - height: 50, - margin: widget.margin ?? EdgeInsets.zero, ); }, ); @@ -154,7 +158,6 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> { filled: true, fillColor: Theme.of(context).colorScheme.surface, hoverColor: Colors.transparent, - // Magic number 4 makes the textField take up the same space as FlowyText contentPadding: EdgeInsets.symmetric( vertical: CardSizes.cardCellVPadding + 4, horizontal: 8, @@ -181,45 +184,93 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> { ); } - void _saveEdit() { - context.read<BoardBloc>().add( - BoardEvent.endEditingHeader( - widget.groupData.id, - _controller.text, + void _saveEdit() => context + .read<BoardBloc>() + .add(BoardEvent.endEditingHeader(widget.groupData.id, _controller.text)); + + Widget _buildHeaderIcon(GroupData customData) => + switch (customData.fieldType) { + FieldType.Checkbox => FlowySvg( + customData.asCheckboxGroup()!.isCheck + ? FlowySvgs.check_filled_s + : FlowySvgs.uncheck_s, + blendMode: BlendMode.dst, ), + _ => const SizedBox.shrink(), + }; + + Widget _groupOptionsButton(BuildContext context) { + return AppFlowyPopover( + clickHandler: PopoverClickHandler.gestureDetector, + margin: const EdgeInsets.fromLTRB(8, 8, 8, 4), + constraints: BoxConstraints.loose(const Size(168, 300)), + direction: PopoverDirection.bottomWithLeftAligned, + child: FlowyIconButton( + width: 20, + icon: const FlowySvg(FlowySvgs.details_horizontal_s), + iconColorOnHover: Theme.of(context).colorScheme.onSurface, + ), + popupBuilder: (popoverContext) { + final customGroupData = widget.groupData.customData as GroupData; + final menuItems = GroupOptions.values.toList(); + if (!customGroupData.fieldType.canEditHeader) { + menuItems.remove(GroupOptions.rename); + } + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + ...menuItems.map( + (action) => SizedBox( + height: GridSize.popoverItemHeight, + child: Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: FlowyButton( + leftIcon: FlowySvg(action.icon), + text: FlowyText.medium( + action.text, + overflow: TextOverflow.ellipsis, + ), + onTap: () { + action.call(context, customGroupData.group); + PopoverContainer.of(popoverContext).close(); + }, + ), + ), + ), + ) + ], ); + }, + ); } } -Widget? _buildHeaderIcon(GroupData customData) { - Widget? widget; - switch (customData.fieldType) { - case FieldType.Checkbox: - final group = customData.asCheckboxGroup()!; - widget = FlowySvg( - group.isCheck ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s, - blendMode: BlendMode.dst, - ); - break; - case FieldType.DateTime: - case FieldType.LastEditedTime: - case FieldType.CreatedTime: - case FieldType.MultiSelect: - case FieldType.Number: - case FieldType.RichText: - case FieldType.SingleSelect: - case FieldType.URL: - case FieldType.Checklist: - break; - } +enum GroupOptions { + rename, + hide; - if (widget != null) { - widget = SizedBox( - width: 20, - height: 20, - child: widget, - ); + void call(BuildContext context, GroupPB group) { + switch (this) { + case rename: + context + .read<BoardBloc>() + .add(BoardEvent.startEditingHeader(group.groupId)); + break; + case hide: + context + .read<BoardBloc>() + .add(BoardEvent.toggleGroupVisibility(group, false)); + break; + } } - return widget; + FlowySvgData get icon => switch (this) { + rename => FlowySvgs.edit_s, + hide => FlowySvgs.hide_s, + }; + + String get text => switch (this) { + rename => LocaleKeys.board_column_renameColumn.tr(), + hide => LocaleKeys.board_column_hideColumn.tr(), + }; } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/widgets/board_hidden_groups.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/widgets/board_hidden_groups.dart new file mode 100644 index 000000000000..c40434c5605c --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/widgets/board_hidden_groups.dart @@ -0,0 +1,483 @@ +import 'dart:io'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart'; +import 'package:appflowy/plugins/database_view/application/database_controller.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_info.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_controller.dart'; +import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:collection/collection.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class HiddenGroupsColumn extends StatelessWidget { + final EdgeInsets margin; + const HiddenGroupsColumn({super.key, required this.margin}); + + @override + Widget build(BuildContext context) { + final databaseController = context.read<BoardBloc>().databaseController; + return BlocSelector<BoardBloc, BoardState, BoardLayoutSettingPB?>( + selector: (state) => state.layoutSettings, + builder: (context, layoutSettings) { + if (layoutSettings == null) { + return const SizedBox.shrink(); + } + final isCollapsed = layoutSettings.collapseHiddenGroups; + return AnimatedSize( + alignment: AlignmentDirectional.topStart, + curve: Curves.easeOut, + duration: const Duration(milliseconds: 150), + child: isCollapsed + ? SizedBox( + height: 50, + child: Padding( + padding: const EdgeInsets.only(left: 40, right: 8), + child: Center( + child: _collapseExpandIcon(context, isCollapsed), + ), + ), + ) + : SizedBox( + width: 260, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 50, + child: Padding( + padding: EdgeInsets.only( + left: 40 + margin.left, + right: margin.right, + ), + child: Row( + children: [ + Expanded( + child: FlowyText.medium( + LocaleKeys + .board_hiddenGroupSection_sectionTitle + .tr(), + fontSize: 14, + overflow: TextOverflow.ellipsis, + color: Theme.of(context).hintColor, + ), + ), + _collapseExpandIcon(context, isCollapsed), + ], + ), + ), + ), + Expanded( + child: HiddenGroupList( + databaseController: databaseController, + ), + ), + ], + ), + ), + ); + }, + ); + } + + Widget _collapseExpandIcon(BuildContext context, bool isCollapsed) { + return FlowyTooltip( + message: isCollapsed + ? LocaleKeys.board_hiddenGroupSection_expandTooltip.tr() + : LocaleKeys.board_hiddenGroupSection_collapseTooltip.tr(), + child: FlowyIconButton( + width: 20, + height: 20, + iconColorOnHover: Theme.of(context).colorScheme.onSurface, + onPressed: () => context + .read<BoardBloc>() + .add(BoardEvent.toggleHiddenSectionVisibility(!isCollapsed)), + icon: FlowySvg( + isCollapsed + ? FlowySvgs.hamburger_s_s + : FlowySvgs.pull_left_outlined_s, + ), + ), + ); + } +} + +class HiddenGroupList extends StatelessWidget { + const HiddenGroupList({ + super.key, + required this.databaseController, + }); + + final DatabaseController databaseController; + + @override + Widget build(BuildContext context) { + final bloc = context.read<BoardBloc>(); + return BlocBuilder<BoardBloc, BoardState>( + builder: (_, state) => ReorderableListView.builder( + proxyDecorator: (child, index, animation) => Material( + color: Colors.transparent, + child: Stack( + children: [ + child, + MouseRegion( + cursor: Platform.isWindows + ? SystemMouseCursors.click + : SystemMouseCursors.grabbing, + child: const SizedBox.expand(), + ), + ], + ), + ), + buildDefaultDragHandles: false, + itemCount: state.hiddenGroups.length, + itemBuilder: (_, index) => Padding( + padding: const EdgeInsets.only(bottom: 4), + key: ValueKey("hiddenGroup${state.hiddenGroups[index].groupId}"), + child: HiddenGroupCard( + group: state.hiddenGroups[index], + index: index, + bloc: bloc, + ), + ), + onReorder: (oldIndex, newIndex) { + if (oldIndex < newIndex) { + newIndex--; + } + final fromGroupId = state.hiddenGroups[oldIndex].groupId; + final toGroupId = state.hiddenGroups[newIndex].groupId; + bloc.add(BoardEvent.reorderGroup(fromGroupId, toGroupId)); + }, + ), + ); + } +} + +class HiddenGroupCard extends StatefulWidget { + const HiddenGroupCard({ + super.key, + required this.group, + required this.index, + required this.bloc, + }); + + final GroupPB group; + final BoardBloc bloc; + final int index; + + @override + State<HiddenGroupCard> createState() => _HiddenGroupCardState(); +} + +class _HiddenGroupCardState extends State<HiddenGroupCard> { + final PopoverController _popoverController = PopoverController(); + + @override + Widget build(BuildContext context) { + final databaseController = widget.bloc.databaseController; + final primaryField = databaseController.fieldController.fieldInfos + .firstWhereOrNull((element) => element.isPrimary)!; + + return Padding( + padding: const EdgeInsets.only(left: 26), + child: AppFlowyPopover( + controller: _popoverController, + direction: PopoverDirection.bottomWithCenterAligned, + triggerActions: PopoverTriggerFlags.none, + constraints: const BoxConstraints(maxWidth: 234, maxHeight: 300), + popupBuilder: (popoverContext) => HiddenGroupPopupItemList( + bloc: widget.bloc, + viewId: databaseController.viewId, + groupId: widget.group.groupId, + primaryField: primaryField, + rowCache: databaseController.rowCache, + ), + child: HiddenGroupButtonContent( + popoverController: _popoverController, + groupId: widget.group.groupId, + index: widget.index, + bloc: widget.bloc, + ), + ), + ); + } +} + +class HiddenGroupButtonContent extends StatelessWidget { + final String groupId; + final int index; + final BoardBloc bloc; + const HiddenGroupButtonContent({ + super.key, + required this.popoverController, + required this.groupId, + required this.index, + required this.bloc, + }); + + final PopoverController popoverController; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(right: 8.0), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: popoverController.show, + child: FlowyHover( + builder: (context, isHovering) { + return BlocProvider<BoardBloc>.value( + value: bloc, + child: BlocBuilder<BoardBloc, BoardState>( + builder: (context, state) { + final group = state.hiddenGroups.firstWhereOrNull( + (g) => g.groupId == groupId, + ); + if (group == null) { + return const SizedBox.shrink(); + } + + return SizedBox( + height: 30, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 4, + vertical: 3, + ), + child: Row( + children: [ + HiddenGroupCardActions( + isVisible: isHovering, + index: index, + ), + const HSpace(4), + FlowyText.medium( + group.groupName, + overflow: TextOverflow.ellipsis, + ), + const HSpace(6), + Expanded( + child: FlowyText.medium( + group.rows.length.toString(), + overflow: TextOverflow.ellipsis, + color: Theme.of(context).hintColor, + ), + ), + if (isHovering) ...[ + FlowyIconButton( + width: 20, + icon: FlowySvg( + FlowySvgs.show_m, + color: Theme.of(context).hintColor, + ), + onPressed: () => context.read<BoardBloc>().add( + BoardEvent.toggleGroupVisibility( + group, + true, + ), + ), + ), + ], + ], + ), + ), + ); + }, + ), + ); + }, + ), + ), + ); + } +} + +class HiddenGroupCardActions extends StatelessWidget { + final bool isVisible; + final int index; + + const HiddenGroupCardActions({ + super.key, + required this.isVisible, + required this.index, + }); + + @override + Widget build(BuildContext context) { + return ReorderableDragStartListener( + index: index, + enabled: isVisible, + child: MouseRegion( + cursor: SystemMouseCursors.grab, + child: SizedBox( + height: 14, + width: 14, + child: isVisible + ? FlowySvg( + FlowySvgs.drag_element_s, + color: Theme.of(context).hintColor, + ) + : const SizedBox.shrink(), + ), + ), + ); + } +} + +class HiddenGroupPopupItemList extends StatelessWidget { + const HiddenGroupPopupItemList({ + required this.bloc, + required this.groupId, + required this.viewId, + required this.primaryField, + required this.rowCache, + super.key, + }); + + final BoardBloc bloc; + final String groupId; + final String viewId; + final FieldInfo primaryField; + final RowCache rowCache; + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: bloc, + child: BlocBuilder<BoardBloc, BoardState>( + builder: (context, state) { + final group = state.hiddenGroups.firstWhereOrNull( + (g) => g.groupId == groupId, + ); + if (group == null) { + return const SizedBox.shrink(); + } + final cells = <Widget>[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), + child: FlowyText.medium( + group.groupName, + fontSize: 10, + color: Theme.of(context).hintColor, + overflow: TextOverflow.ellipsis, + ), + ), + ...group.rows.map( + (item) { + final rowController = RowController( + rowMeta: item, + viewId: viewId, + rowCache: rowCache, + ); + final renderHook = RowCardRenderHook<String>(); + renderHook.addTextCellHook((cellData, _, __) { + return BlocBuilder<TextCellBloc, TextCellState>( + builder: (context, state) { + final text = cellData.isEmpty + ? LocaleKeys.grid_row_titlePlaceholder.tr() + : cellData; + + if (text.isEmpty) { + return const SizedBox.shrink(); + } + + return Align( + alignment: Alignment.centerLeft, + child: FlowyText.medium( + text, + textAlign: TextAlign.left, + fontSize: 11, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ); + }, + ); + }); + + return HiddenGroupPopupItem( + cellContext: rowCache.loadCells(item)[primaryField.id]!, + primaryField: primaryField, + rowController: rowController, + cellBuilder: CardCellBuilder<String>(rowController.cellCache), + renderHook: renderHook, + onPressed: () { + FlowyOverlay.show( + context: context, + builder: (BuildContext context) { + return RowDetailPage( + cellBuilder: GridCellBuilder( + cellCache: rowController.cellCache, + ), + rowController: rowController, + ); + }, + ); + PopoverContainer.of(context).close(); + }, + ); + }, + ) + ]; + + return ListView.separated( + itemBuilder: (context, index) => cells[index], + itemCount: cells.length, + separatorBuilder: (context, index) => + VSpace(GridSize.typeOptionSeparatorHeight), + shrinkWrap: true, + ); + }, + ), + ); + } +} + +class HiddenGroupPopupItem extends StatelessWidget { + const HiddenGroupPopupItem({ + super.key, + required this.cellContext, + required this.onPressed, + required this.cellBuilder, + required this.rowController, + required this.primaryField, + required this.renderHook, + }); + + final DatabaseCellContext cellContext; + final FieldInfo primaryField; + final RowController rowController; + final CardCellBuilder cellBuilder; + final RowCardRenderHook<String> renderHook; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 26, + child: FlowyButton( + margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + text: cellBuilder.buildCell( + cellContext: cellContext, + renderHook: renderHook, + hasNotes: !cellContext.rowMeta.isDocumentEmpty, + ), + onTap: onPressed, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_header_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_header_bloc.dart index 173a74bc8773..c854dbb33528 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_header_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_header_bloc.dart @@ -21,7 +21,7 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> { on<GridHeaderEvent>( (event, emit) async { await event.map( - initial: (_InitialHeader value) async { + initial: (_InitialHeader value) { _startListening(); add( GridHeaderEvent.didReceiveFieldUpdate(fieldController.fieldInfos), @@ -65,7 +65,7 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> { result.fold((l) {}, (err) => Log.error(err)); } - Future<void> _startListening() async { + void _startListening() { fieldController.addListener( onReceiveFields: (fields) => add(GridHeaderEvent.didReceiveFieldUpdate(fields)), diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/layout/sizes.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/layout/sizes.dart index 1acb6eccdc42..fbd05e952e86 100755 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/layout/sizes.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/layout/sizes.dart @@ -6,7 +6,7 @@ class GridSize { static double get scrollBarSize => 8 * scale; static double get headerHeight => 40 * scale; static double get footerHeight => 40 * scale; - static double get leadingHeaderPadding => 50 * scale; + static double get leadingHeaderPadding => 40 * scale; static double get trailHeaderPadding => 140 * scale; static double get headerContainerPadding => 0 * scale; static double get cellHPadding => 10 * scale; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart index 5a10f6d6c273..a1a573341dcd 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart @@ -59,4 +59,10 @@ extension FieldTypeListExtension on FieldType { FieldType.SingleSelect => true, _ => false, }; + + bool get canCreateNewGroup => switch (this) { + FieldType.MultiSelect => true, + FieldType.SingleSelect => true, + _ => false, + }; } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tab_bar_view.dart b/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tab_bar_view.dart index 87355800c84a..0d38a6d4ea29 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tab_bar_view.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tab_bar_view.dart @@ -10,7 +10,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../application/database_controller.dart'; -import '../grid/presentation/layout/sizes.dart'; import 'tab_bar_header.dart'; abstract class DatabaseTabBarItemBuilder { @@ -95,13 +94,11 @@ class _DatabaseTabBarViewState extends State<DatabaseTabBarView> { if (value) { return const SizedBox.shrink(); } - return SizedBox( + return const SizedBox( height: 30, child: Padding( - padding: EdgeInsets.symmetric( - horizontal: GridSize.leadingHeaderPadding, - ), - child: const TabBarHeader(), + padding: EdgeInsets.symmetric(horizontal: 40), + child: TabBarHeader(), ), ); }, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart index 885421096d80..48d611880f9e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart @@ -239,6 +239,7 @@ class PageManager { shrinkWrap: false, ); + // TODO(Xazin): Board should fill up full width return Padding( padding: builder.contentPadding, child: pluginWidget, diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/icon_button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/icon_button.dart index fd65bb94363e..318b0c95e989 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/icon_button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/icon_button.dart @@ -70,7 +70,7 @@ class FlowyIconButton extends StatelessWidget { shape: RoundedRectangleBorder(borderRadius: radius ?? Corners.s6Border), fillColor: fillColor, - hoverColor: hoverColor, + hoverColor: Colors.transparent, focusColor: Colors.transparent, splashColor: Colors.transparent, highlightColor: Colors.transparent, @@ -79,7 +79,6 @@ class FlowyIconButton extends StatelessWidget { child: FlowyHover( isSelected: isSelected != null ? () => isSelected! : null, style: HoverStyle( - // hoverColor is set in both [HoverStyle] and [RawMaterialButton] to avoid the conflicts between two layers hoverColor: hoverColor, foregroundColorOnHover: iconColorOnHover ?? Theme.of(context).iconTheme.color, @@ -88,9 +87,7 @@ class FlowyIconButton extends StatelessWidget { resetHoverOnRebuild: false, child: Padding( padding: iconPadding, - child: Center( - child: child, - ), + child: Center(child: child), ), ), ), @@ -100,11 +97,9 @@ class FlowyIconButton extends StatelessWidget { } class FlowyDropdownButton extends StatelessWidget { + const FlowyDropdownButton({super.key, this.onPressed}); + final VoidCallback? onPressed; - const FlowyDropdownButton({ - Key? key, - this.onPressed, - }) : super(key: key); @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 0be54f2fa729..26506fe39dde 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -45,11 +45,11 @@ packages: dependency: "direct main" description: path: "." - ref: "1a329c2" - resolved-ref: "1a329c21921c0d19871bea3237b7d80fe131f2ed" + ref: "2de4fe0" + resolved-ref: "2de4fe0b0245dcdf2c2bf43410661c28acbcc687" url: "https://github.com/AppFlowy-IO/appflowy-board.git" source: git - version: "0.1.0" + version: "0.1.1" appflowy_editor: dependency: "direct main" description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 8e9eb9aecff2..0692d318987d 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -43,7 +43,7 @@ dependencies: # path: packages/appflowy_board git: url: https://github.com/AppFlowy-IO/appflowy-board.git - ref: 1a329c2 + ref: 2de4fe0 appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git diff --git a/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_multi_select_field_test.dart b/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_multi_select_field_test.dart index a73e05f224f5..bd8399b1dfd7 100644 --- a/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_multi_select_field_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_multi_select_field_test.dart @@ -107,8 +107,8 @@ void main() { final groups = boardBloc.groupControllers.values.map((e) => e.group).toList(); - assert(groups[0].groupName == "B"); - assert(groups[1].groupName == "A"); - assert(groups[2].groupName == "No ${multiSelectField.name}"); + assert(groups[0].groupName == "No ${multiSelectField.name}"); + assert(groups[1].groupName == "B"); + assert(groups[2].groupName == "A"); }); } diff --git a/frontend/resources/flowy_icons/16x/hamburger_s.svg b/frontend/resources/flowy_icons/16x/hamburger_s.svg new file mode 100644 index 000000000000..ae63919cce22 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/hamburger_s.svg @@ -0,0 +1,7 @@ +<svg width="6" height="8" viewBox="0 0 6 8" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g id="Vector"> +<path d="M0 0.799951C0 0.46858 0.268629 0.199951 0.6 0.199951H5.4C5.73137 0.199951 6 0.46858 6 0.799951C6 1.13132 5.73137 1.39995 5.4 1.39995H0.600001C0.26863 1.39995 0 1.13132 0 0.799951Z" fill="#8F959E"/> +<path d="M0 3.99995C0 3.66858 0.268629 3.39995 0.6 3.39995H5.4C5.73137 3.39995 6 3.66858 6 3.99995C6 4.33132 5.73137 4.59995 5.4 4.59995H0.600001C0.26863 4.59995 0 4.33132 0 3.99995Z" fill="#8F959E"/> +<path d="M0 7.19995C0 6.86858 0.268629 6.59995 0.6 6.59995H5.4C5.73137 6.59995 6 6.86858 6 7.19995C6 7.53132 5.73137 7.79995 5.4 7.79995H0.600001C0.26863 7.79995 0 7.53132 0 7.19995Z" fill="#8F959E"/> +</g> +</svg> diff --git a/frontend/resources/flowy_icons/16x/pull_left_outlined.svg b/frontend/resources/flowy_icons/16x/pull_left_outlined.svg new file mode 100644 index 000000000000..a1cd5c0e1be7 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/pull_left_outlined.svg @@ -0,0 +1,6 @@ +<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g id="icon_pull-left_outlined"> +<path id="Union" d="M14.6515 9.58019H9.1031L10.7447 7.91716C10.9061 7.75372 10.9048 7.4864 10.7435 7.3229C10.5825 7.1597 10.3193 7.15862 10.1582 7.32177L7.66078 9.85165C7.62187 9.89109 7.60001 9.94456 7.60001 10.0003C7.60001 10.0561 7.62187 10.1095 7.66078 10.149L10.1566 12.6773C10.3184 12.8412 10.5808 12.8407 10.7426 12.6767C10.9046 12.5125 10.9053 12.2461 10.7432 12.082L9.10373 10.4213H14.6515C14.8808 10.4213 15.0667 10.233 15.0667 10.0007C15.0667 9.76848 14.8808 9.58019 14.6515 9.58019Z" fill="#8F959E" stroke="#8F959E" stroke-width="0.1" stroke-linecap="round"/> +<path id="Union_2" d="M5.19995 14.1126C5.19995 14.437 5.42384 14.7 5.69995 14.7C5.97606 14.7 6.19995 14.437 6.19995 14.1126V5.88753C6.19995 5.56307 5.97606 5.30005 5.69995 5.30005C5.42384 5.30005 5.19995 5.56307 5.19995 5.88753V14.1126Z" fill="#8F959E"/> +</g> +</svg> diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 95d30f92d2e9..a82f19998448 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -619,9 +619,9 @@ "createInlineMathEquation": "Create equation", "fonts": "Fonts", "toggleList": "Toggle list", - "quoteList":"Quote list", - "numberedList":"Numbered list", - "bulletedList":"Bulleted list", + "quoteList": "Quote list", + "numberedList": "Numbered list", + "bulletedList": "Bulleted list", "todoList": "Todo List", "callout": "Callout", "cover": { @@ -772,7 +772,15 @@ "column": { "createNewCard": "New", "renameGroupTooltip": "Press to rename group", - "createNewColumn": "Add a new group" + "createNewColumn": "Add a new group", + "addToColumnTopTooltip": "Add a new card at the top", + "renameColumn": "Rename", + "hideColumn": "Hide" + }, + "hiddenGroupSection": { + "sectionTitle": "Hidden Groups", + "collapseTooltip": "Hide the hidden groups", + "expandTooltip": "View the hidden groups" }, "menuName": "Board", "showUngrouped": "Show ungrouped items", @@ -1054,4 +1062,4 @@ "cardDetails": { "notesPlaceholder": "Enter a / to insert a block, or start typing" } -} +} \ No newline at end of file diff --git a/frontend/rust-lib/event-integration/tests/database/local_test/test.rs b/frontend/rust-lib/event-integration/tests/database/local_test/test.rs index c2775b70653c..ac247b0a1846 100644 --- a/frontend/rust-lib/event-integration/tests/database/local_test/test.rs +++ b/frontend/rust-lib/event-integration/tests/database/local_test/test.rs @@ -786,7 +786,8 @@ async fn hide_group_event_test() { assert!(error.is_none()); let groups = test.get_groups(&board_view.id).await; - assert_eq!(groups.len(), 3); + assert_eq!(groups.len(), 4); + assert_eq!(groups[0].is_visible, false); } // Update the database layout type from grid to board diff --git a/frontend/rust-lib/flowy-database2/src/entities/board_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/board_entities.rs index addab49033b5..5edefeb09e3c 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/board_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/board_entities.rs @@ -6,12 +6,16 @@ use crate::services::setting::BoardLayoutSetting; pub struct BoardLayoutSettingPB { #[pb(index = 1)] pub hide_ungrouped_column: bool, + + #[pb(index = 2)] + pub collapse_hidden_groups: bool, } impl From<BoardLayoutSetting> for BoardLayoutSettingPB { fn from(setting: BoardLayoutSetting) -> Self { Self { hide_ungrouped_column: setting.hide_ungrouped_column, + collapse_hidden_groups: setting.collapse_hidden_groups, } } } @@ -20,6 +24,7 @@ impl From<BoardLayoutSettingPB> for BoardLayoutSetting { fn from(setting: BoardLayoutSettingPB) -> Self { Self { hide_ungrouped_column: setting.hide_ungrouped_column, + collapse_hidden_groups: setting.collapse_hidden_groups, } } } diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs index baab8ee110f1..f3052006bfde 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs @@ -312,7 +312,6 @@ impl DatabaseViewEditor { .as_ref()? .get_all_groups() .into_iter() - .filter(|group| group.is_visible) .map(|group_data| GroupPB::from(group_data.clone())) .collect::<Vec<_>>(); tracing::trace!("Number of groups: {}", groups.len()); @@ -398,12 +397,16 @@ impl DatabaseViewEditor { pub async fn v_update_group(&self, changeset: GroupChangesets) -> FlowyResult<()> { let mut type_option_data = TypeOptionData::new(); - let old_field = if let Some(controller) = self.group_controller.write().await.as_mut() { + let (old_field, updated_groups) = if let Some(controller) = + self.group_controller.write().await.as_mut() + { let old_field = self.delegate.get_field(controller.field_id()); - type_option_data.extend(controller.apply_group_changeset(&changeset).await?); - old_field + let (updated_groups, new_type_option) = controller.apply_group_changeset(&changeset).await?; + type_option_data.extend(new_type_option); + + (old_field, updated_groups) } else { - None + (None, vec![]) }; if let Some(old_field) = old_field { @@ -413,6 +416,12 @@ impl DatabaseViewEditor { .update_field(&self.view_id, type_option_data, old_field) .await?; } + let notification = GroupChangesPB { + view_id: self.view_id.clone(), + update_groups: updated_groups, + ..Default::default() + }; + notify_did_update_num_of_groups(&self.view_id, notification).await; } Ok(()) diff --git a/frontend/rust-lib/flowy-database2/src/services/group/action.rs b/frontend/rust-lib/flowy-database2/src/services/group/action.rs index dbc11f26bcad..9af51f167bfe 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/action.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/action.rs @@ -168,7 +168,7 @@ pub trait GroupControllerOperation: Send + Sync { async fn apply_group_changeset( &mut self, changesets: &GroupChangesets, - ) -> FlowyResult<TypeOptionData>; + ) -> FlowyResult<(Vec<GroupPB>, TypeOptionData)>; } #[derive(Debug)] diff --git a/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs b/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs index 52392680d5e9..3b387940bdc8 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs @@ -173,6 +173,7 @@ where self.field.id.clone(), group.name.clone(), group.id.clone(), + group.visible, ); self.group_by_id.insert(group.id.clone(), group_data); let (index, group_data) = self.get_group(&group.id).unwrap(); @@ -338,7 +339,13 @@ where .get(&group.id) .cloned() .unwrap_or_else(|| "".to_owned()); - let group = GroupData::new(group.id, self.field.id.clone(), group.name, filter_content); + let group = GroupData::new( + group.id, + self.field.id.clone(), + group.name, + filter_content, + group.visible, + ); self.group_by_id.insert(group.id.clone(), group); }); @@ -351,6 +358,7 @@ where self.field.id.clone(), group_rev.name, filter_content.clone(), + group_rev.visible, ); Some(GroupPB::from(group)) }) diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller.rs index 239e2910360f..fd049759ac30 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller.rs @@ -11,7 +11,8 @@ use serde::Serialize; use flowy_error::FlowyResult; use crate::entities::{ - FieldType, GroupChangesPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB, RowMetaPB, + FieldType, GroupChangesPB, GroupPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB, + RowMetaPB, }; use crate::services::cell::{get_cell_protobuf, CellProtobufBlobParser}; use crate::services::field::{default_type_option_data_from_type, TypeOption, TypeOptionCellData}; @@ -45,10 +46,12 @@ pub trait GroupOperationInterceptor { type GroupTypeOption: TypeOption; async fn type_option_from_group_changeset( &self, - changeset: &GroupChangeset, - type_option: &Self::GroupTypeOption, - view_id: &str, - ) -> Option<TypeOptionData>; + _changeset: &GroupChangeset, + _type_option: &Self::GroupTypeOption, + _view_id: &str, + ) -> Option<TypeOptionData> { + None + } } /// C: represents the group configuration that impl [GroupConfigurationSerde] @@ -396,7 +399,7 @@ where async fn apply_group_changeset( &mut self, changeset: &GroupChangesets, - ) -> FlowyResult<TypeOptionData> { + ) -> FlowyResult<(Vec<GroupPB>, TypeOptionData)> { for group_changeset in changeset.changesets.iter() { self.context.update_group(group_changeset)?; } @@ -410,7 +413,16 @@ where type_option_data.extend(new_type_option_data); } } - Ok(type_option_data) + let updated_groups = changeset + .changesets + .iter() + .filter_map(|changeset| { + self + .get_group(&changeset.group_id) + .map(|(_, group)| GroupPB::from(group)) + }) + .collect::<Vec<_>>(); + Ok((updated_groups, type_option_data)) } } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs index 64a7706cc265..accd8d3f31fb 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use collab_database::fields::{Field, TypeOptionData}; +use collab_database::fields::Field; use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail}; use serde::{Deserialize, Serialize}; @@ -12,8 +12,8 @@ use crate::services::group::action::GroupCustomize; use crate::services::group::configuration::GroupContext; use crate::services::group::controller::{BaseGroupController, GroupController}; use crate::services::group::{ - move_group_row, GeneratedGroupConfig, GeneratedGroups, Group, GroupChangeset, - GroupOperationInterceptor, GroupsBuilder, MoveGroupRowContext, + move_group_row, GeneratedGroupConfig, GeneratedGroups, Group, GroupOperationInterceptor, + GroupsBuilder, MoveGroupRowContext, }; #[derive(Default, Serialize, Deserialize)] @@ -190,12 +190,4 @@ pub struct CheckboxGroupOperationInterceptorImpl {} #[async_trait] impl GroupOperationInterceptor for CheckboxGroupOperationInterceptorImpl { type GroupTypeOption = CheckboxTypeOption; - async fn type_option_from_group_changeset( - &self, - _changeset: &GroupChangeset, - _type_option: &Self::GroupTypeOption, - _view_id: &str, - ) -> Option<TypeOptionData> { - todo!() - } } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs index 316d628e28d3..1a3f2ee37115 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs @@ -7,7 +7,7 @@ use chrono::{ }; use chrono_tz::Tz; use collab_database::database::timestamp; -use collab_database::fields::{Field, TypeOptionData}; +use collab_database::fields::Field; use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail}; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; @@ -24,7 +24,7 @@ use crate::services::group::configuration::GroupContext; use crate::services::group::controller::{BaseGroupController, GroupController}; use crate::services::group::{ make_no_status_group, move_group_row, GeneratedGroupConfig, GeneratedGroups, Group, - GroupChangeset, GroupOperationInterceptor, GroupsBuilder, MoveGroupRowContext, + GroupOperationInterceptor, GroupsBuilder, MoveGroupRowContext, }; pub trait GroupConfigurationContentSerde: Sized + Send + Sync { @@ -458,14 +458,6 @@ pub struct DateGroupOperationInterceptorImpl {} #[async_trait] impl GroupOperationInterceptor for DateGroupOperationInterceptorImpl { type GroupTypeOption = DateTypeOption; - async fn type_option_from_group_changeset( - &self, - _changeset: &GroupChangeset, - _type_option: &Self::GroupTypeOption, - _view_id: &str, - ) -> Option<TypeOptionData> { - todo!() - } } #[cfg(test)] diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs index 1de57413127a..b3ba30127e42 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs @@ -6,7 +6,9 @@ use collab_database::rows::{Cells, Row, RowDetail}; use flowy_error::FlowyResult; -use crate::entities::{GroupChangesPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB}; +use crate::entities::{ + GroupChangesPB, GroupPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB, +}; use crate::services::group::action::{ DidMoveGroupRowResult, DidUpdateGroupRowResult, GroupControllerOperation, }; @@ -30,6 +32,7 @@ impl DefaultGroupController { field.id.clone(), "".to_owned(), "".to_owned(), + true, ); Self { field_id: field.id.clone(), @@ -129,8 +132,8 @@ impl GroupControllerOperation for DefaultGroupController { async fn apply_group_changeset( &mut self, _changeset: &GroupChangesets, - ) -> FlowyResult<TypeOptionData> { - Ok(TypeOptionData::default()) + ) -> FlowyResult<(Vec<GroupPB>, TypeOptionData)> { + Ok((Vec::new(), TypeOptionData::default())) } } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs index 475b871ffcc6..0b5b3539c762 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use async_trait::async_trait; -use collab_database::fields::{Field, TypeOptionData}; +use collab_database::fields::Field; use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail}; use serde::{Deserialize, Serialize}; @@ -17,8 +17,7 @@ use crate::services::group::configuration::GroupContext; use crate::services::group::controller::{BaseGroupController, GroupController}; use crate::services::group::{ make_no_status_group, move_group_row, GeneratedGroupConfig, GeneratedGroups, Group, - GroupChangeset, GroupOperationInterceptor, GroupTypeOptionCellOperation, GroupsBuilder, - MoveGroupRowContext, + GroupOperationInterceptor, GroupTypeOptionCellOperation, GroupsBuilder, MoveGroupRowContext, }; #[derive(Default, Serialize, Deserialize)] @@ -250,12 +249,4 @@ pub struct URLGroupOperationInterceptorImpl { #[async_trait::async_trait] impl GroupOperationInterceptor for URLGroupOperationInterceptorImpl { type GroupTypeOption = URLTypeOption; - async fn type_option_from_group_changeset( - &self, - _changeset: &GroupChangeset, - _type_option: &Self::GroupTypeOption, - _view_id: &str, - ) -> Option<TypeOptionData> { - todo!() - } } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/entities.rs b/frontend/rust-lib/flowy-database2/src/services/group/entities.rs index 0badcc1e49e1..253c12bac931 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/entities.rs @@ -148,13 +148,19 @@ pub struct GroupData { } impl GroupData { - pub fn new(id: String, field_id: String, name: String, filter_content: String) -> Self { + pub fn new( + id: String, + field_id: String, + name: String, + filter_content: String, + is_visible: bool, + ) -> Self { let is_default = id == field_id; Self { id, field_id, is_default, - is_visible: true, + is_visible, name, rows: vec![], filter_content, diff --git a/frontend/rust-lib/flowy-database2/src/services/setting/entities.rs b/frontend/rust-lib/flowy-database2/src/services/setting/entities.rs index 2e0ee02935b3..7cfe09372545 100644 --- a/frontend/rust-lib/flowy-database2/src/services/setting/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/setting/entities.rs @@ -93,6 +93,7 @@ pub const DEFAULT_SHOW_WEEK_NUMBERS: bool = true; #[derive(Debug, Clone, Default)] pub struct BoardLayoutSetting { pub hide_ungrouped_column: bool, + pub collapse_hidden_groups: bool, } impl BoardLayoutSetting { @@ -107,6 +108,9 @@ impl From<LayoutSetting> for BoardLayoutSetting { hide_ungrouped_column: setting .get_bool_value("hide_ungrouped_column") .unwrap_or_default(), + collapse_hidden_groups: setting + .get_bool_value("collapse_hidden_groups") + .unwrap_or_default(), } } } @@ -115,6 +119,7 @@ impl From<BoardLayoutSetting> for LayoutSetting { fn from(setting: BoardLayoutSetting) -> Self { LayoutSettingBuilder::new() .insert_bool_value("hide_ungrouped_column", setting.hide_ungrouped_column) + .insert_bool_value("collapse_hidden_groups", setting.collapse_hidden_groups) .build() } } diff --git a/frontend/rust-lib/flowy-database2/tests/database/layout_test/test.rs b/frontend/rust-lib/flowy-database2/tests/database/layout_test/test.rs index 7a8d5749eb27..41f2f88d0eef 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/layout_test/test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/layout_test/test.rs @@ -11,6 +11,7 @@ async fn board_layout_setting_test() { let default_board_setting = BoardLayoutSetting::new(); let new_board_setting = BoardLayoutSetting { hide_ungrouped_column: true, + ..default_board_setting }; let scripts = vec![ AssertBoardLayoutSetting { From 3708a5b86ade2509330a7d7a8cf46baaf3a4194c Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" <lucas.xu@appflowy.io> Date: Mon, 13 Nov 2023 18:08:39 +0800 Subject: [PATCH 49/56] feat: add alignment and indent/outdent toolbar item (#3927) * chore: rename mobile list toolbar item * feat: add alignment and indent/outdent toolbar item * feat: adjust link menu on mobile platform --- .../document/presentation/editor_page.dart | 5 +- .../image/mobile_image_toolbar_item.dart | 2 +- .../mobile_math_equation_toolbar_item.dart | 3 +- .../mobile_align_toolbar_item.dart | 103 ++++++++++++++++++ ...m.dart => mobile_blocks_toolbar_item.dart} | 5 +- .../mobile_indent_toolbar_items.dart | 24 ++++ .../presentation/editor_plugins/plugins.dart | 4 +- .../undo_redo/redo_mobile_toolbar_item.dart | 2 +- .../undo_redo/undo_mobile_toolbar_item.dart | 2 +- .../document/presentation/editor_style.dart | 14 +++ frontend/appflowy_flutter/pubspec.lock | 4 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- 12 files changed, 159 insertions(+), 11 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_align_toolbar_item.dart rename frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/{list_mobile_toolbar_item.dart => mobile_blocks_toolbar_item.dart} (95%) create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_indent_toolbar_items.dart diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index be877ea1ca0e..c6d74ad1eed6 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -286,12 +286,15 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> { textDecorationMobileToolbarItem, buildTextAndBackgroundColorMobileToolbarItem(), headingMobileToolbarItem, - customListMobileToolbarItem, + mobileBlocksToolbarItem, linkMobileToolbarItem, dividerMobileToolbarItem, imageMobileToolbarItem, mathEquationMobileToolbarItem, codeMobileToolbarItem, + mobileAlignToolbarItem, + mobileIndentToolbarItem, + mobileOutdentToolbarItem, undoMobileToolbarItem, redoMobileToolbarItem, ], diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/mobile_image_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/mobile_image_toolbar_item.dart index 10a756c02fe8..7d5c95ea51c1 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/mobile_image_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/mobile_image_toolbar_item.dart @@ -5,7 +5,7 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; final imageMobileToolbarItem = MobileToolbarItem.action( - itemIcon: const FlowySvg(FlowySvgs.m_toolbar_imae_lg), + itemIconBuilder: (_, __) => const FlowySvg(FlowySvgs.m_toolbar_imae_lg), actionHandler: (editorState, selection) async { final imagePlaceholderKey = GlobalKey<ImagePlaceholderState>(); await editorState.insertEmptyImageBlock(imagePlaceholderKey); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/mobile_math_equation_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/mobile_math_equation_toolbar_item.dart index 5e7f7dcc77fd..0c97ccf0ae2d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/mobile_math_equation_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/mobile_math_equation_toolbar_item.dart @@ -4,7 +4,8 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; final mathEquationMobileToolbarItem = MobileToolbarItem.action( - itemIcon: const SizedBox(width: 22, child: FlowySvg(FlowySvgs.math_lg)), + itemIconBuilder: (_, __) => + const SizedBox(width: 22, child: FlowySvg(FlowySvgs.math_lg)), actionHandler: (editorState, selection) async { if (!selection.isCollapsed) { return; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_align_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_align_toolbar_item.dart new file mode 100644 index 000000000000..6171c362e331 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_align_toolbar_item.dart @@ -0,0 +1,103 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +final mobileAlignToolbarItem = MobileToolbarItem.withMenu( + itemIconBuilder: (_, editorState) { + return onlyShowInTextType(editorState) + ? const FlowySvg( + FlowySvgs.toolbar_align_center_s, + size: Size.square(32), + ) + : null; + }, + itemMenuBuilder: (editorState, selection, _) { + return _MobileAlignMenu( + editorState: editorState, + selection: selection, + ); + }, +); + +class _MobileAlignMenu extends StatelessWidget { + const _MobileAlignMenu({ + required this.editorState, + required this.selection, + }); + + final Selection selection; + final EditorState editorState; + + @override + Widget build(BuildContext context) { + return GridView.count( + crossAxisCount: 3, + mainAxisSpacing: 8, + crossAxisSpacing: 8, + childAspectRatio: 3, + shrinkWrap: true, + children: [ + _buildAlignmentButton( + context, + 'left', + LocaleKeys.document_plugins_optionAction_left.tr(), + ), + _buildAlignmentButton( + context, + 'center', + LocaleKeys.document_plugins_optionAction_center.tr(), + ), + _buildAlignmentButton( + context, + 'right', + LocaleKeys.document_plugins_optionAction_right.tr(), + ), + ], + ); + } + + Widget _buildAlignmentButton( + BuildContext context, + String alignment, + String label, + ) { + final nodes = editorState.getNodesInSelection(selection); + if (nodes.isEmpty) { + const SizedBox.shrink(); + } + + bool isSatisfyCondition(bool Function(Object? value) test) { + return nodes.every( + (n) => test(n.attributes[blockComponentAlign]), + ); + } + + final data = switch (alignment) { + 'left' => FlowySvgs.toolbar_align_left_s, + 'center' => FlowySvgs.toolbar_align_center_s, + 'right' => FlowySvgs.toolbar_align_right_s, + _ => throw UnimplementedError(), + }; + final isSelected = isSatisfyCondition((value) => value == alignment); + + return MobileToolbarItemMenuBtn( + icon: FlowySvg(data, size: const Size.square(28)), + label: FlowyText(label), + isSelected: isSelected, + onPressed: () async { + await editorState.updateNode( + selection, + (node) => node.copyWith( + attributes: { + ...node.attributes, + blockComponentAlign: alignment, + }, + ), + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/list_mobile_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_blocks_toolbar_item.dart similarity index 95% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/list_mobile_toolbar_item.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_blocks_toolbar_item.dart index d1f633eb6044..8cece8ec97d3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/list_mobile_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_blocks_toolbar_item.dart @@ -6,8 +6,9 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -final customListMobileToolbarItem = MobileToolbarItem.withMenu( - itemIcon: const AFMobileIcon(afMobileIcons: AFMobileIcons.list), +final mobileBlocksToolbarItem = MobileToolbarItem.withMenu( + itemIconBuilder: (_, __) => + const AFMobileIcon(afMobileIcons: AFMobileIcons.list), itemMenuBuilder: (editorState, selection, _) { return _MobileListMenu( editorState: editorState, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_indent_toolbar_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_indent_toolbar_items.dart new file mode 100644 index 000000000000..fb07a38a25aa --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_indent_toolbar_items.dart @@ -0,0 +1,24 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; + +final mobileIndentToolbarItem = MobileToolbarItem.action( + itemIconBuilder: (_, editorState) { + return onlyShowInTextType(editorState) + ? const Icon(Icons.format_indent_increase_rounded) + : null; + }, + actionHandler: (editorState, selection) { + indentCommand.execute(editorState); + }, +); + +final mobileOutdentToolbarItem = MobileToolbarItem.action( + itemIconBuilder: (_, editorState) { + return onlyShowInTextType(editorState) + ? const Icon(Icons.format_indent_decrease_rounded) + : null; + }, + actionHandler: (editorState, selection) { + outdentCommand.execute(editorState); + }, +); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart index f1f816f8a01b..c019d6a5f9cc 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart @@ -26,7 +26,9 @@ export 'inline_math_equation/inline_math_equation.dart'; export 'inline_math_equation/inline_math_equation_toolbar_item.dart'; export 'math_equation/math_equation_block_component.dart'; export 'math_equation/mobile_math_equation_toolbar_item.dart'; -export 'mobile_toolbar_item/list_mobile_toolbar_item.dart'; +export 'mobile_toolbar_item/mobile_align_toolbar_item.dart'; +export 'mobile_toolbar_item/mobile_blocks_toolbar_item.dart'; +export 'mobile_toolbar_item/mobile_indent_toolbar_items.dart'; export 'openai/widgets/auto_completion_node_widget.dart'; export 'openai/widgets/smart_edit_node_widget.dart'; export 'openai/widgets/smart_edit_toolbar_item.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/redo_mobile_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/redo_mobile_toolbar_item.dart index 99b29f9b423e..abd445dbc83c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/redo_mobile_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/redo_mobile_toolbar_item.dart @@ -2,7 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; final redoMobileToolbarItem = MobileToolbarItem.action( - itemIcon: const FlowySvg(FlowySvgs.m_redo_m), + itemIconBuilder: (_, __) => const FlowySvg(FlowySvgs.m_redo_m), actionHandler: (editorState, selection) async { editorState.undoManager.redo(); }, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/undo_mobile_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/undo_mobile_toolbar_item.dart index cf132c14866e..bc9d27aeb775 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/undo_mobile_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/undo_mobile_toolbar_item.dart @@ -2,7 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; final undoMobileToolbarItem = MobileToolbarItem.action( - itemIcon: const FlowySvg(FlowySvgs.m_undo_m), + itemIconBuilder: (_, __) => const FlowySvg(FlowySvgs.m_undo_m), actionHandler: (editorState, selection) async { editorState.undoManager.undo(); }, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart index 402e4a5c9cf4..eab6c10fe3d5 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -7,6 +7,7 @@ import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart'; import 'package:appflowy/util/google_font_family_extension.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:collection/collection.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -264,6 +265,19 @@ class EditorStyleCustomizer { ); } + // customize the link on mobile + final href = attributes[AppFlowyRichTextKeys.href] as String?; + if (PlatformExtension.isMobile && href != null) { + return TextSpan( + style: textSpan.style, + text: text.text, + recognizer: TapGestureRecognizer() + ..onTap = () { + safeLaunchUrl(href); + }, + ); + } + return defaultTextSpanDecoratorForAttribute( context, node, diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 26506fe39dde..3ae7a7b31887 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -54,8 +54,8 @@ packages: dependency: "direct main" description: path: "." - ref: "50117b6" - resolved-ref: "50117b6900e4b239603ee48f6f3e7b7bc603c865" + ref: "009115d" + resolved-ref: "009115da836616e9fb2d0abd327753809a78b983" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "1.5.2" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 0692d318987d..3c86e546f3b6 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -47,7 +47,7 @@ dependencies: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: 50117b6 + ref: 009115d appflowy_popover: path: packages/appflowy_popover From 47f3702ca9319de016b90be721a0816c1589bdfc Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 13 Nov 2023 19:32:11 +0800 Subject: [PATCH 50/56] fix: integration test on kanban (#3928) --- .../database_view/board/application/board_bloc.dart | 9 ++++----- .../database_view/board/presentation/board_page.dart | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart index 5a4ee76e6b34..52cb0e7e77cc 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart @@ -6,7 +6,6 @@ import 'package:appflowy/plugins/database_view/application/field/field_info.dart import 'package:appflowy/plugins/database_view/application/group/group_service.dart'; import 'package:appflowy/plugins/database_view/application/row/row_service.dart'; import 'package:appflowy_board/appflowy_board.dart'; -import 'package:collection/collection.dart'; import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; @@ -257,11 +256,11 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> { databaseController.databaseLayoutSetting?.board.hideUngroupedColumn ?? false; - FieldType? get groupingFieldType { - final fieldInfo = databaseController.fieldController.fieldInfos - .firstWhereOrNull((field) => field.isGroupField); + FieldType get groupingFieldType { + final fieldInfo = + databaseController.fieldController.getField(groupList.first.fieldId)!; - return fieldInfo?.fieldType; + return fieldInfo.fieldType; } void initializeGroups(List<GroupPB> groups) { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart index 4bae94b30a75..716dfde52834 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart @@ -150,7 +150,7 @@ class _BoardContentState extends State<BoardContent> { child: BlocBuilder<BoardBloc, BoardState>( builder: (context, state) { final showCreateGroupButton = - context.read<BoardBloc>().groupingFieldType!.canCreateNewGroup; + context.read<BoardBloc>().groupingFieldType.canCreateNewGroup; return Padding( padding: const EdgeInsets.only(top: 8.0), child: AppFlowyBoard( From 7de2131431f6072019328a5bf8e415f876fbd6f5 Mon Sep 17 00:00:00 2001 From: Emil <49696490+Emilkyo@users.noreply.github.com> Date: Mon, 13 Nov 2023 14:36:41 +0300 Subject: [PATCH 51/56] chore: update Russian translations (#3823) --- frontend/resources/translations/ru-RU.json | 530 +++++++++++++++++++-- 1 file changed, 478 insertions(+), 52 deletions(-) diff --git a/frontend/resources/translations/ru-RU.json b/frontend/resources/translations/ru-RU.json index 49d369ae56b8..2f008110ea37 100644 --- a/frontend/resources/translations/ru-RU.json +++ b/frontend/resources/translations/ru-RU.json @@ -10,9 +10,11 @@ "and": "и", "blockActions": { "addBelowTooltip": "Нажмите, чтобы добавить ниже", - "addAboveCmd": "Alt+щелчок", - "addAboveMacCmd": "Option+щелчок", - "addAboveTooltip": "добавить выше" + "addAboveCmd": "Alt+клик", + "addAboveMacCmd": "Option+клик", + "addAboveTooltip": "добавить выше", + "dragTooltip": "Перетащите для перемещения", + "openMenuTooltip": "Нажмите, чтобы открыть меню" }, "signUp": { "buttonText": "Зарегистрироваться", @@ -24,11 +26,14 @@ "alreadyHaveAnAccount": "Уже есть аккаунт?", "emailHint": "Электронная почта", "passwordHint": "Пароль", - "repeatPasswordHint": "Повторите пароль" + "repeatPasswordHint": "Повторите пароль", + "signUpWith": "Зарегистрируйтесь с помощью:" }, "signIn": { "loginTitle": "Войти в @:appName", "loginButtonText": "Войти", + "loginStartWithAnonymous": "Начать анонимный сеанс", + "continueAnonymousUser": "Продолжить анонимный сеанс", "buttonText": "Авторизация", "forgotPassword": "Забыли пароль?", "emailHint": "Электронная почта", @@ -36,17 +41,32 @@ "dontHaveAnAccount": "Нет аккаунта?", "repeatPasswordEmptyError": "Повтор пароля не может быть пустым", "unmatchedPasswordError": "Пароли не совпадают", + "syncPromptMessage": "Синхронизация данных может занять некоторое время. Пожалуйста, не закрывайте эту страницу", + "or": "или", + "LogInWithGoogle": "Войти через Google", + "LogInWithGithub": "Войти через Github", + "LogInWithDiscord": "Войти через Discord", + "signInWith": "Войти с помощью:", "loginAsGuestButtonText": "Начать" }, "workspace": { + "chooseWorkspace": "Выберите рабочее пространство", "create": "Создать рабочее пространство", + "reset": "Сбросить рабочее пространство", + "resetWorkspacePrompt": "Сброс рабочей области приведет к удалению всех страниц и данных в ней. Вы уверены, что хотите сбросить рабочую область? Также, вы можете обратиться в службу поддержки для восстановления рабочей области.", "hint": "рабочее пространство", - "notFoundError": "Нет такого рабочего пространства" + "notFoundError": "Рабочее пространство не найдено", + "failedToLoad": "Что-то пошло не так! Не удалось загрузить рабочее пространство. Попробуйте закрыть открытый сеанс AppFlowy и повторите попытку.", + "errorActions": { + "reportIssue": "Сообщить о проблеме", + "reachOut": "Обратитесь в Discord" + } }, "shareAction": { "buttonText": "Поделиться", "workInProgress": "В разработке", "markdown": "Markdown", + "csv": "CSV", "copyLink": "Скопировать ссылку" }, "moreAction": { @@ -54,7 +74,8 @@ "medium": "средний", "large": "большой", "fontSize": "Размер шрифта", - "import": "Импортировать" + "import": "Импортировать", + "moreOptions": "Больше вариантов" }, "importPanel": { "textAndMarkdown": "Текст и Markdown", @@ -67,16 +88,25 @@ "rename": "Переименовать", "delete": "Удалить", "duplicate": "Дублировать", - "openNewTab": "Открыть в новой вкладке" + "unfavorite": "Удалить из избранного", + "favorite": "Добавить в избранное", + "openNewTab": "Открыть в новой вкладке", + "moveTo": "Переместить в", + "addToFavorites": "Добавить в избранное", + "copyLink": "Скопировать ссылку" }, "blankPageTitle": "Пустая страница", "newPageText": "Новая страница", + "newDocumentText": "Новый документ", + "newGridText": "Новая сетка", + "newCalendarText": "Новый календарь", + "newBoardText": "Новая доска", "trash": { "text": "Корзина", "restoreAll": "Восстановить всё", "deleteAll": "Очистить", "pageHeader": { - "fileName": "Имя", + "fileName": "Имя файла", "lastModified": "Последнее изменение", "created": "Создан" }, @@ -87,6 +117,11 @@ "confirmRestoreAll": { "title": "Вы уверены, что хотите восстановить все страницы в Корзине?", "caption": "Это действие не может быть отменено." + }, + "mobile": { + "actions": "Действия с корзиной", + "empty": "Корзина пуста", + "emptyDescription": "У вас нет удаленных файлов" } }, "deletePagePrompt": { @@ -103,15 +138,17 @@ "debug": { "name": "Отладочная информация", "success": "Скопировано в буфер обмена!", - "fail": "Не получилось скопировать" + "fail": "Не получилось скопировать отладочную информацию в буфер обмена" }, "feedback": "Обратная связь" }, "menuAppHeader": { + "moreButtonToolTip": "Удалить, переименовать и многое другое...", "addPageTooltip": "Быстро добавить новую страницу", "defaultNewPageName": "Без заголовка", "renameDialog": "Переименовать" }, + "noPagesInside": "Внутри нет страниц", "toolbar": { "undo": "Отменить", "redo": "Повторить", @@ -133,9 +170,9 @@ "tooltip": { "lightMode": "Переключить на светлую тему", "darkMode": "Переключить на тёмную тему", - "openAsPage": "Открыть как страницу", + "openAsPage": "Открыть как Страницу", "addNewRow": "Добавить новую строку", - "openMenu": "Открыть меню", + "openMenu": "Нажмите, чтобы открыть меню", "dragRow": "Долгое нажатие для изменения порядка строк", "viewDataBase": "Открыть базу данных", "referencePage": "Ссылки на {name}", @@ -143,7 +180,12 @@ }, "sideBar": { "closeSidebar": "Закрыть боковое меню", - "openSidebar": "Открыть боковое меню" + "openSidebar": "Открыть боковое меню", + "personal": "Личное", + "favorites": "Избранное", + "clickToHidePersonal": "Нажмите, чтобы скрыть личный раздел", + "clickToHideFavorites": "Нажмите, чтобы скрыть раздел избранного", + "addAPage": "Добавить страницу" }, "notifications": { "export": { @@ -158,13 +200,15 @@ "editContact": "Редактировать контакт" }, "button": { - "done": "Сделанный", + "ok": "ОК", + "done": "Готово", + "cancel": "Отмена", "signIn": "Войти", "signOut": "Выйти", "complete": "Завершить", "save": "Сохранить", "generate": "Сгенерировать", - "esc": "ESC", + "esc": "Esc", "keep": "Оставить", "tryAgain": "Повторить", "discard": "Отменить", @@ -175,28 +219,32 @@ "delete": "Удалить", "duplicate": "Дублировать", "putback": "Вернуть", - "OK": "OK", - "Done": "Завершить", - "Cancel": "Отмена" + "update": "Обновить", + "share": "Поделиться", + "removeFromFavorites": "Удалить из избранного", + "addToFavorites": "Добавить в избранное", + "rename": "Переименовать", + "helpCenter": "Центр помощи", + "tryAGain": "Повторить" }, "label": { "welcome": "Добро пожаловать!", "firstName": "Имя", "middleName": "Отчество", "lastName": "Фамилия", - "stepX": "Этап {X}" + "stepX": "Шаг {X}" }, "oAuth": { "err": { - "failedTitle": "Не удалось подключиться к вашей учетной записи.", + "failedTitle": "Не удалось подключиться к вашему аккаунту.", "failedMsg": "Убедитесь, что вы завершили вход в своём браузере." }, "google": { "title": "Вход через Google", - "instruction1": "Чтобы импортировать ваши Google Контакты, вам нужно будет авторизовать приложение через браузер.", - "instruction2": "Скопируйте этот код в буфер обмена (нажав кнопку или выделив текст):", - "instruction3": "Пройдите по ссылке и введите этот код:", - "instruction4": "Нажмите на кнопку ниже, когда завершите вход:" + "instruction1": "Чтобы импортировать ваши Google Контакты, вам нужно будет авторизоваться через браузер.", + "instruction2": "Скопируйте этот код в буфер обмена (нажав на иконку или выделив текст):", + "instruction3": "Перейдите по ссылке и введите приведенный выше код:", + "instruction4": "Нажмите кнопку ниже после завершения регистрации:" } }, "settings": { @@ -206,10 +254,31 @@ "language": "Язык", "user": "Пользователь", "files": "Файлы", + "notifications": "Уведомления", "open": "Открыть настройки", + "logout": "Выйти", + "logoutPrompt": "Вы уверены, что хотите выйти?", + "selfEncryptionLogoutPrompt": "Вы действительно хотите выйти? Убедитесь, что вы скопировали секрет шифрования", + "syncSetting": "Настройка синхронизации", + "enableSync": "Включить синхронизацию", + "enableEncrypt": "Шифрование данных", + "enableEncryptPrompt": "Активируйте шифрование для защиты ваших данных с этим секретом. Храните его безопасно; после включения, он не может быть отключён. В случае потери секрета ваши данные будут также потеряны. Нажмите, чтобы скопировать", + "inputEncryptPrompt": "Пожалуйста, введите ваш секрет шифрования для", + "clickToCopySecret": "Нажмите, чтобы скопировать секрет", + "inputTextFieldHint": "Ваш секрет", + "historicalUserList": "История входа пользователя", + "historicalUserListTooltip": "В этом списке отображаются ваши анонимные аккаунты. Вы можете нажать на аккаунт, чтобы посмотреть данные. Анонимный аккаунт создаётся нажатием кнопки «Начать».", + "openHistoricalUser": "Нажмите, чтобы открыть анонимный аккаунт", "supabaseSetting": "Настройка надбазы" }, + "notifications": { + "enableNotifications": { + "label": "Включить уведомления", + "hint": "Выключите, чтобы локальные уведомления не появлялись." + } + }, "appearance": { + "resetSetting": "Сбросить", "fontFamily": { "label": "Семейство шрифтов", "search": "Поиск" @@ -220,8 +289,23 @@ "dark": "Тёмная", "system": "Системная" }, + "layoutDirection": { + "label": "Направление макета", + "hint": "Управляйте потоком контента на экране слева направо или справа налево.", + "ltr": "Слева направо", + "rtl": "Справа налево" + }, + "textDirection": { + "label": "Направление текста по умолчанию", + "hint": "Укажите направление текста по умолчанию слева или справа.", + "ltr": "Слева направо", + "rtl": "Справа налево", + "auto": "Авто", + "fallback": "То же, что и направление макета" + }, "themeUpload": { "button": "Загрузить", + "uploadTheme": "Загрузить тему", "description": "Загрузите собственную тему AppFlowy, используя кнопку ниже.", "failure": "Загруженная тема имеет недопустимый формат.", "loading": "Подождите, пока мы проверим и загрузим вашу тему...", @@ -232,7 +316,21 @@ }, "theme": "Тема", "builtInsLabel": "Встроенные темы", - "pluginsLabel": "Плагины" + "pluginsLabel": "Плагины", + "dateFormat": { + "label": "Формат даты", + "local": "Локальный", + "us": "ММ-ДД-ГГГГ", + "iso": "ГГГГ-ММ-ДД", + "friendly": "Дружелюбный", + "dmy": "Д/М/Г" + }, + "timeFormat": { + "label": "Формат времени", + "twelveHour": "Двенадцать часов", + "twentyFourHour": "Двадцать четыре часа" + }, + "showNamingDialogWhenCreatingPage": "Показывать диалоговое окно именования при создании страницы" }, "files": { "copy": "Копировать", @@ -272,13 +370,42 @@ }, "user": { "name": "Имя", + "email": "Email", + "tooltipSelectIcon": "Выберите значок", "selectAnIcon": "Выбрать иконку", - "pleaseInputYourOpenAIKey": "Введите токен OpenAI" + "pleaseInputYourOpenAIKey": "Введите токен OpenAI", + "pleaseInputYourStabilityAIKey": "Пожалуйста, введите свой токен Stability AI", + "clickToLogout": "Нажмите, чтобы выйти из текущего пользователя" + }, + "shortcuts": { + "shortcutsLabel": "Ярлыки", + "command": "Команда", + "keyBinding": "Привязка клавиш", + "addNewCommand": "Добавить новую команду", + "updateShortcutStep": "Нажмите нужную комбинацию клавиш и нажмите ENTER.", + "shortcutIsAlreadyUsed": "Этот ярлык уже используется для: {conflict}", + "resetToDefault": "Сбросить сочетания клавиш по умолчанию", + "couldNotLoadErrorMsg": "Не удалось загрузить ярлыки. Попробуйте ещё раз", + "couldNotSaveErrorMsg": "Не удалось сохранить ярлыки. Попробуйте ещё раз" + }, + "mobile": { + "personalInfo": "Личная информация", + "username": "Имя пользователя", + "usernameEmptyError": "Имя пользователя не может быть пустым", + "about": "О нас", + "pushNotifications": "Всплывающее уведомление", + "support": "Поддержка", + "joinDiscord": "Присоединяйтесь к нам в Discord", + "privacyPolicy": "Политика Конфиденциальности", + "userAgreement": "Пользовательское Соглашение" } }, "grid": { "deleteView": "Вы уверены, что хотите удалить это представление?", "createView": "Новый", + "title": { + "placeholder": "Без названия" + }, "settings": { "filter": "Фильтр", "sort": "Сортировать", @@ -291,8 +418,7 @@ "filterBy": "Фильтровать по...", "typeAValue": "Введите значение...", "layout": "Вид", - "databaseLayout": "Вид базы данных", - "Properties": "Свойства" + "databaseLayout": "Вид базы данных" }, "textFilter": { "contains": "Содержит", @@ -336,6 +462,7 @@ }, "field": { "hide": "Скрыть", + "show": "Показать", "insertLeft": "Вставить слева", "insertRight": "Вставить справа", "duplicate": "Дублировать", @@ -353,6 +480,7 @@ "numberFormat": "Формат числа", "dateFormat": "Формат даты", "includeTime": "Время", + "isRange": "Дата окончания", "dateFormatFriendly": "День Месяц, Год", "dateFormatISO": "Год-Месяц-День", "dateFormatLocal": "Месяц/День/Год", @@ -362,46 +490,73 @@ "invalidTimeFormat": "Неверный формат", "timeFormatTwelveHour": "12 часов", "timeFormatTwentyFourHour": "24 часа", + "clearDate": "Очистить дату", "addSelectOption": "Добавить вариант", "optionTitle": "Варианты", "addOption": "Добавить", "editProperty": "Редактировать свойство", - "newProperty": "Добавить колонку", - "deleteFieldPromptMessage": "Вы уверены? Свойство будет удалено" + "newProperty": "Новое свойство", + "deleteFieldPromptMessage": "Вы уверены? Свойство будет удалено", + "newColumn": "Новый столбец" + }, + "rowPage": { + "newField": "Добавить новое поле", + "fieldDragEelementTooltip": "Нажмите, чтобы открыть меню", + "showHiddenFields": { + "one": "Показать {} скрытое поле", + "many": "Показать {} скрытых поля", + "other": "Показать {} скрытых поля" + }, + "hideHiddenFields": { + "one": "Скрыть {} скрытое поле", + "many": "Скрыто {} скрытых поля", + "other": "Скрыто {} скрытых поля" + } }, "sort": { "ascending": "По возрастанию", "descending": "По убыванию", + "deleteAllSorts": "Удалить все сортировки", "addSort": "Добавить сортировку", "deleteSort": "Удалить сортировку" }, "row": { "duplicate": "Дублировать", "delete": "Удалить", + "titlePlaceholder": "Без названия", "textPlaceholder": "Пусто", "copyProperty": "Свойство скопировано", "count": "Количество", "newRow": "Новая строка", - "action": "Действия" + "action": "Действия", + "add": "Нажмите, чтобы добавить ниже", + "drag": "Перетащите для перемещения" }, "selectOption": { "create": "Создать", - "purpleColor": "Фиолетовый", + "purpleColor": "Сиреневый", "pinkColor": "Розовый", - "lightPinkColor": "Светло-розовый", + "lightPinkColor": "Фиолетовый", "orangeColor": "Оранжевый", - "yellowColor": "Желтый", + "yellowColor": "Жёлтый", "limeColor": "Ярко-зелёный", "greenColor": "Зелёный", "aquaColor": "Бирюзовый", "blueColor": "Синий", - "deleteTag": "Удалить вариант", + "deleteTag": "Удалить тег", "colorPanelTitle": "Цвета", "panelTitle": "Выберите или создайте вариант", - "searchOption": "Поиск" + "searchOption": "Поиск", + "searchOrCreateOption": "Найдите или создайте вариант...", + "createNew": "Создать новую", + "orSelectOne": "Или выберите вариант" }, "checklist": { - "addNew": "Добавить элемент" + "taskHint": "Описание задачи", + "addNew": "Добавить элемент", + "submitNewTask": "Создать", + "hideComplete": "Скрыть выполненные задачи", + "showComplete": "Показать все задачи" }, "menuName": "Сетка", "referencedGridPrefix": "Просмотр" @@ -427,13 +582,14 @@ } }, "selectionMenu": { - "outline": "Контур" + "outline": "Контур", + "codeBlock": "Блок кода" }, "plugins": { "referencedBoard": "Связанные доски", "referencedGrid": "Связанные сетки", - "referencedCalendar": "Ссылочный календарь", - "autoGeneratorMenuItemName": "Генератор OpenAI", + "referencedCalendar": "Связанные календари", + "autoGeneratorMenuItemName": "OpenAI Генератор", "autoGeneratorTitleName": "OpenAI: попросить ИИ написать что угодно...", "autoGeneratorLearnMore": "Узнать больше", "autoGeneratorGenerate": "Генерировать", @@ -444,15 +600,15 @@ "openAI": "OpenAI", "smartEditFixSpelling": "Исправить правописание", "warning": "⚠️ Ответы ИИ могут быть неправильными или неточными.", - "smartEditSummarize": "Выделить суть", - "smartEditImproveWriting": "Улучшить", + "smartEditSummarize": "Обобщить", + "smartEditImproveWriting": "Исправить написание", "smartEditMakeLonger": "Продолжить", "smartEditCouldNotFetchResult": "Не могу получить ответ от OpenAI", "smartEditCouldNotFetchKey": "Не могу получить токен OpenAI", - "smartEditDisabled": "Подключить OpenAI", + "smartEditDisabled": "OpenAI", "discardResponse": "Хотите убрать ответы ИИ?", "createInlineMathEquation": "Создать уравнение", - "toggleList": "Переключить список", + "toggleList": "Выпадающий список", "cover": { "changeCover": "Сменить обложку", "colors": "Цвета", @@ -497,10 +653,24 @@ "defaultColor": "Цвет по умолчанию" }, "image": { - "copiedToPasteBoard": "Ссылка на изображение скопирована в буфер обмена" + "copiedToPasteBoard": "Ссылка на изображение скопирована в буфер обмена", + "addAnImage": "Добавить изображение" }, "outline": { "addHeadingToCreateOutline": "Добавьте заголовки, чтобы создать оглавление." + }, + "table": { + "addAfter": "Добавить после", + "addBefore": "Добавить до", + "delete": "Удалить", + "clear": "Очистить содержимое", + "duplicate": "Дублировать", + "bgColor": "Цвет фона" + }, + "contextMenu": { + "copy": "Копировать", + "cut": "Вырезать", + "paste": "Вставить" } }, "textBlock": { @@ -519,13 +689,28 @@ "label": "URL изображения", "placeholder": "Введите URL-адрес изображения" }, + "ai": { + "label": "Сгенерировать изображение через OpenAI", + "placeholder": "Пожалуйста, введите запрос для OpenAI чтобы сгенерировать изображение" + }, + "stability_ai": { + "label": "Сгенерировать изображение через Stability AI", + "placeholder": "Пожалуйста, введите запрос для Stability AI чтобы сгенерировать изображение" + }, "support": "Ограничение размера изображения составляет 5 МБ. Поддерживаемые форматы: JPEG, PNG, GIF, SVG", "error": { "invalidImage": "Недопустимое изображение", "invalidImageSize": "Размер изображения должен быть менее 5 МБ.", "invalidImageFormat": "Формат изображения не поддерживается. Поддерживаемые форматы: JPEG, PNG, GIF, SVG", "invalidImageUrl": "Недопустимый URL-адрес изображения" - } + }, + "embedLink": { + "label": "Вставить ссылку", + "placeholder": "Вставьте или введите ссылку на изображение" + }, + "searchForAnImage": "Поиск изображения", + "pleaseInputYourOpenAIKey": "пожалуйста, введите свой токен OpenAI на странице настроек", + "pleaseInputYourStabilityAIKey": "пожалуйста, введите свой токен Stability AI на странице настроек" }, "codeBlock": { "language": { @@ -535,6 +720,9 @@ }, "inlineLink": { "placeholder": "Вставьте или введите ссылку", + "openInNewTab": "Открыть в новой вкладке", + "copyLink": "Скопировать ссылку", + "removeLink": "Удалить ссылку", "url": { "label": "URL-адрес ссылки", "placeholder": "Введите URL ссылки" @@ -543,18 +731,40 @@ "label": "Название ссылки", "placeholder": "Введите название ссылки" } + }, + "mention": { + "placeholder": "Упомяните человека, страницу или дату...", + "page": { + "label": "Ссылка на страницу", + "tooltip": "Нажмите, чтобы открыть страницу" + } + }, + "toolbar": { + "resetToDefaultFont": "Восстановить по умолчанию" + }, + "errorBlock": { + "theBlockIsNotSupported": "Текущая версия не поддерживает этот блок.", + "blockContentHasBeenCopied": "Содержимое блока скопировано." } }, "board": { "menuName": "Доска", "referencedBoardPrefix": "Просмотр", "column": { + "createNewCard": "Новая", + "renameGroupTooltip": "Нажмите, чтобы переименовать группу", "create_new_card": "Создать" - } - }, + }, + "showUngrouped": "Показать несгруппированные элементы", + "ungroupedButtonText": "Разгруппировать", + "ungroupedButtonTooltip": "Содержит карточки, которые не принадлежат ни к одной группе.", + "ungroupedItemsTitle": "Нажмите, чтобы добавить на доску", + "groupBy": "Сгруппировать по" + }, "calendar": { "menuName": "Календарь", - "defaultNewCalendarTitle": "Безымянный", + "defaultNewCalendarTitle": "Без названия", + "newEventButtonTooltip": "Добавить новое событие", "navigation": { "today": "Сегодня", "jumpToday": "Перейти к сегодняшнему дню", @@ -566,10 +776,14 @@ "showWeekends": "Показывать выходные", "firstDayOfWeek": "Первый день недели", "layoutDateField": "Вид календаря", - "noDateTitle": "No Date", - "clickToAdd": "Click to add to the calendar", - "name": "Calendar layout", - "noDateHint": "Unscheduled events will show up here" + "noDateTitle": "Без даты", + "noDateHint": { + "zero": "Здесь будут отображаться незапланированные мероприятия.", + "one": "{} незапланированное событие", + "other": "{} незапланированные события" + }, + "clickToAdd": "Нажмите, чтобы добавить в календарь", + "name": "Макет календаря" }, "referencedCalendarPrefix": "Вид" }, @@ -594,5 +808,217 @@ "views": { "deleteContentTitle": "Вы уверены, что хотите удалить {pageType}?", "deleteContentCaption": "если вы удалите этот {pageType}, вы сможете восстановить его из корзины." + }, + "colors": { + "custom": "Пользовательский", + "default": "По умолчанию", + "red": "Красный", + "orange": "Оранжевый", + "yellow": "Жёлтый", + "green": "Зелёный", + "blue": "Синий", + "purple": "Фиолетовый", + "pink": "Розовый", + "brown": "Коричневый", + "gray": "Серый" + }, + "emoji": { + "search": "Поиск эмодзи", + "noRecent": "Нет недавних эмодзи", + "noEmojiFound": "Эмодзи не найдено", + "filter": "Фильтр", + "random": "Случайно", + "selectSkinTone": "Выберите оттенок кожи", + "remove": "Удалить эмодзи", + "categories": { + "smileys": "Смайлики и эмоции", + "people": "Люди и тело", + "animals": "Животные и природа", + "food": "Еда, напитки", + "activities": "Деятельность", + "places": "Путешествия и места", + "objects": "Объекты", + "symbols": "Символы", + "flags": "Флаги", + "nature": "Природа", + "frequentlyUsed": "Часто используемые" + } + }, + "inlineActions": { + "noResults": "Нет результатов", + "pageReference": "Ссылка на страницу", + "date": "Дата", + "reminder": { + "groupTitle": "Напоминание", + "shortKeyword": "напомнить" + } + }, + "datePicker": { + "dateTimeFormatTooltip": "Измените формат даты и времени в настройках" + }, + "relativeDates": { + "yesterday": "Вчера", + "today": "Сегодня", + "tomorrow": "Завтра", + "oneWeek": "1 неделя" + }, + "notificationHub": { + "title": "Уведомления", + "emptyTitle": "Всё схвачено!", + "emptyBody": "Никаких ожидающих уведомлений или действий. Наслаждайтесь спокойствием.", + "tabs": { + "inbox": "Входящие", + "upcoming": "Предстоящие" + }, + "actions": { + "markAllRead": "Отметить все как прочитанное", + "showAll": "Показать всё", + "showUnreads": "Не прочитано" + }, + "filters": { + "ascending": "По возрастанию", + "descending": "По убыванию", + "groupByDate": "Сгруппировать по дате", + "showUnreadsOnly": "Показать только непрочитанные", + "resetToDefault": "Восстановить по умолчанию" + } + }, + "reminderNotification": { + "title": "Напоминание", + "message": "Не забудьте проверить это, прежде чем забыть!", + "tooltipDelete": "Удалить", + "tooltipMarkRead": "Отметить как прочитанное", + "tooltipMarkUnread": "Отметить как непрочитанное" + }, + "findAndReplace": { + "find": "Найти", + "previousMatch": "Предыдущее совпадение", + "nextMatch": "Следующее совпадение", + "close": "Закрыть", + "replace": "Заменить", + "replaceAll": "Заменить всё", + "noResult": "Нет результатов", + "caseSensitive": "С учетом регистра" + }, + "error": { + "weAreSorry": "Мы сожалеем", + "loadingViewError": "У нас возникли проблемы при загрузке этого представления. Пожалуйста, проверьте ваше интернет-соединение, обновите приложение, и не стесняйтесь обратиться к команде, если проблема не исчезнет." + }, + "editor": { + "bold": "Жирный", + "bulletedList": "Маркированный список", + "checkbox": "Чекбокс", + "embedCode": "Встроить код", + "heading1": "H1", + "heading2": "H2", + "heading3": "Н3", + "highlight": "Выделить", + "color": "Цвет", + "image": "Изображение", + "italic": "Курсив", + "link": "Ссылка", + "numberedList": "Нумерованный список", + "quote": "Цитировать", + "strikethrough": "Зачёркнутый", + "text": "Текст", + "underline": "Подчёркнутый", + "fontColorDefault": "По умолчанию", + "fontColorGray": "Серый", + "fontColorBrown": "Коричневый", + "fontColorOrange": "Оранжевый", + "fontColorYellow": "Жёлтый", + "fontColorGreen": "Зелёный", + "fontColorBlue": "Синий", + "fontColorPurple": "Фиолетовый", + "fontColorPink": "Розовый", + "fontColorRed": "Красный", + "backgroundColorDefault": "Фон по умолчанию", + "backgroundColorGray": "Серый фон", + "backgroundColorBrown": "Коричневый фон", + "backgroundColorOrange": "Оранжевый фон", + "backgroundColorYellow": "Жёлтый фон", + "backgroundColorGreen": "Зелёный фон", + "backgroundColorBlue": "Синий фон", + "backgroundColorPurple": "Фиолетовый фон", + "backgroundColorPink": "Розовый фон", + "backgroundColorRed": "Красный фон", + "done": "Готово", + "cancel": "Отмена", + "tint1": "Оттенок 1", + "tint2": "Оттенок 2", + "tint3": "Оттенок 3", + "tint4": "Оттенок 4", + "tint5": "Оттенок 5", + "tint6": "Оттенок 6", + "tint7": "Оттенок 7", + "tint8": "Оттенок 8", + "tint9": "Оттенок 9", + "lightLightTint1": "Фиолетовый", + "lightLightTint2": "Розовый", + "lightLightTint3": "Светло-розовый", + "lightLightTint4": "Оранжевый", + "lightLightTint5": "Жёлтый", + "lightLightTint6": "Лайм", + "lightLightTint7": "Зелёный", + "lightLightTint8": "Бирюзовый", + "lightLightTint9": "Синий", + "urlHint": "URL", + "mobileHeading1": "Заголовок 1", + "mobileHeading2": "Заголовок 2", + "mobileHeading3": "Заголовок 3", + "textColor": "Цвет текста", + "backgroundColor": "Фоновый цвет", + "addYourLink": "Добавьте свою ссылку", + "openLink": "Открыть ссылку", + "copyLink": "Скопировать ссылку", + "removeLink": "Удалить ссылку", + "editLink": "Изменить ссылку", + "linkText": "Текст", + "linkTextHint": "Пожалуйста, введите текст", + "linkAddressHint": "Пожалуйста, введите URL", + "highlightColor": "Цвет выделения", + "clearHighlightColor": "Сбросить цвет выделения", + "customColor": "Пользовательский цвет", + "hexValue": "Hex значение", + "opacity": "Непрозрачность", + "resetToDefaultColor": "Сбросить цвет по умолчанию", + "ltr": "Слева направо", + "rtl": "Справа налево", + "auto": "Авто", + "cut": "Вырезать", + "copy": "Копировать", + "paste": "Вставить", + "find": "Найти", + "previousMatch": "Предыдущее совпадение", + "nextMatch": "Следующее совпадение", + "closeFind": "Закрыть", + "replace": "Заменить", + "replaceAll": "Заменить всё", + "regex": "Регулярное выражение", + "caseSensitive": "С учетом регистра", + "uploadImage": "Загрузить изображение", + "urlImage": "URL-изображение", + "incorrectLink": "Неверная ссылка", + "upload": "Загрузить", + "chooseImage": "Выберите изображение", + "loading": "Загрузка", + "imageLoadFailed": "Не удалось загрузить изображение", + "divider": "Разделитель", + "table": "Таблица", + "colAddBefore": "Добавить до", + "rowAddBefore": "Добавить до", + "colAddAfter": "Добавить после", + "rowAddAfter": "Добавить после", + "colRemove": "Удалить столбец", + "rowRemove": "Удалить строку", + "colDuplicate": "Дублировать", + "rowDuplicate": "Дублировать", + "colClear": "Очистить контент", + "rowClear": "Очистить содержимое строки", + "slashPlaceHolder": "Введите /, чтобы вставить блок, или начните вводить текст." + }, + "favorite": { + "noFavorite": "Нет избраной страницы", + "noFavoriteHintText": "Проведите по странице влево, чтобы добавить ее в избранное." } } \ No newline at end of file From e788c716027c49f7cd5deffdf099318e607a1546 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 13 Nov 2023 21:41:25 +0800 Subject: [PATCH 52/56] fix: padding issue with the select tags (#3929) * fix: padding issue with the select tags * chore: better solution from Lucas --- .../row/cells/select_option_cell/extension.dart | 6 ++---- .../flowy_infra_ui/lib/style_widget/text.dart | 12 ++++++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart index 5b4ec4f037fd..7f5b508fcac0 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart @@ -3,10 +3,8 @@ import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart' import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flowy_infra_ui/style_widget/icon_button.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; @@ -93,7 +91,7 @@ class SelectOptionTag extends StatelessWidget { @override Widget build(BuildContext context) { EdgeInsets padding = - const EdgeInsets.symmetric(vertical: 1.5, horizontal: 8.0); + const EdgeInsets.symmetric(vertical: 2, horizontal: 8.0); if (onRemove != null) { padding = padding.copyWith(right: 2.0); } diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart index 849538a36bdd..35d802984180 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; class FlowyText extends StatelessWidget { @@ -89,10 +91,12 @@ class FlowyText extends StatelessWidget { maxLines: maxLines, textAlign: textAlign, overflow: overflow ?? TextOverflow.clip, - textHeightBehavior: const TextHeightBehavior( - applyHeightToFirstAscent: false, - applyHeightToLastDescent: false, - ), + textHeightBehavior: Platform.isAndroid || Platform.isIOS + ? const TextHeightBehavior( + applyHeightToFirstAscent: false, + applyHeightToLastDescent: false, + ) + : null, style: Theme.of(context).textTheme.bodyMedium!.copyWith( fontSize: fontSize, fontWeight: fontWeight, From 75c26c807ca4b1579f64f8e17ca9ba8d1a0125e4 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Tue, 14 Nov 2023 14:01:46 +0800 Subject: [PATCH 53/56] feat: User profile notify (#3937) * chore: collab rev * feat: recv user change via websocket --- frontend/appflowy_flutter/dev.env | 12 ++- frontend/appflowy_tauri/src-tauri/Cargo.lock | 53 ++++++------- frontend/appflowy_tauri/src-tauri/Cargo.toml | 19 ++--- frontend/rust-lib/Cargo.lock | 75 +++++++------------ frontend/rust-lib/Cargo.toml | 19 ++--- frontend/rust-lib/flowy-document2/Cargo.toml | 1 + .../rust-lib/flowy-document2/src/manager.rs | 2 - .../af_cloud/impls/user/cloud_service_impl.rs | 17 +++-- .../flowy-server/src/af_cloud/server.rs | 24 +++++- .../flowy-server/src/supabase/api/user.rs | 12 +-- .../flowy-server/src/supabase/server.rs | 4 +- .../rust-lib/flowy-user-deps/src/cloud.rs | 8 +- frontend/rust-lib/flowy-user/src/manager.rs | 8 +- .../flowy-user/src/services/user_sql.rs | 4 +- 14 files changed, 130 insertions(+), 128 deletions(-) diff --git a/frontend/appflowy_flutter/dev.env b/frontend/appflowy_flutter/dev.env index c3a62060bd91..f51f1cbd9e20 100644 --- a/frontend/appflowy_flutter/dev.env +++ b/frontend/appflowy_flutter/dev.env @@ -31,10 +31,14 @@ SUPABASE_ANON_KEY= # APPFLOWY_CLOUD_WS_BASE_URL=wss://xxxxxxxxx # APPFLOWY_CLOUD_GOTRUE_URL=https://xxxxxxxxx # -# Local host machine(For local develop) -# APPFLOWY_CLOUD_BASE_URL=http://localhost:8000 -# APPFLOWY_CLOUD_WS_BASE_URL=ws://localhost:8000/ws -# APPFLOWY_CLOUD_GOTRUE_URL=http://localhost:9998 +# When using localhost for development, you must run AppFlowy Cloud locally +# first. Plese Please follow the instructions below: +# https://github.com/AppFlowy-IO/AppFlowy-Cloud#development +# +# After running AppFlowy Cloud locally, you can use the following settings: +# APPFLOWY_CLOUD_BASE_URL=http://localhost:8000 +# APPFLOWY_CLOUD_WS_BASE_URL=ws://localhost:8000/ws +# APPFLOWY_CLOUD_GOTRUE_URL=http://localhost:9998 APPFLOWY_CLOUD_BASE_URL= APPFLOWY_CLOUD_WS_BASE_URL= diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index c008877ee452..2791918d9dbd 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -138,7 +138,7 @@ checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2e14dcf129cab5e1c980655971cfb5ff321b0844#2e14dcf129cab5e1c980655971cfb5ff321b0844" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=fe977fc8285addd5386e940738cdffbbda9eb44e#fe977fc8285addd5386e940738cdffbbda9eb44e" dependencies = [ "anyhow", "reqwest", @@ -460,7 +460,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" dependencies = [ "borsh-derive", - "hashbrown 0.13.2", + "hashbrown 0.12.3", ] [[package]] @@ -768,7 +768,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2e14dcf129cab5e1c980655971cfb5ff321b0844#2e14dcf129cab5e1c980655971cfb5ff321b0844" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=fe977fc8285addd5386e940738cdffbbda9eb44e#fe977fc8285addd5386e940738cdffbbda9eb44e" dependencies = [ "anyhow", "app-error", @@ -863,7 +863,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" dependencies = [ "anyhow", "async-trait", @@ -883,7 +883,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" dependencies = [ "anyhow", "async-trait", @@ -913,7 +913,7 @@ dependencies = [ [[package]] name = "collab-derive" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" dependencies = [ "proc-macro2", "quote", @@ -925,7 +925,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" dependencies = [ "anyhow", "collab", @@ -945,7 +945,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" dependencies = [ "anyhow", "bytes", @@ -959,7 +959,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" dependencies = [ "anyhow", "chrono", @@ -1001,7 +1001,7 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" dependencies = [ "anyhow", "async-trait", @@ -1023,7 +1023,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" dependencies = [ "anyhow", "async-trait", @@ -1050,7 +1050,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" dependencies = [ "anyhow", "collab", @@ -1449,7 +1449,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2e14dcf129cab5e1c980655971cfb5ff321b0844#2e14dcf129cab5e1c980655971cfb5ff321b0844" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=fe977fc8285addd5386e940738cdffbbda9eb44e#fe977fc8285addd5386e940738cdffbbda9eb44e" dependencies = [ "anyhow", "app-error", @@ -2090,6 +2090,7 @@ dependencies = [ "indexmap 1.9.3", "lib-dispatch", "lib-infra", + "lru", "nanoid", "parking_lot", "protobuf", @@ -2806,7 +2807,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2e14dcf129cab5e1c980655971cfb5ff321b0844#2e14dcf129cab5e1c980655971cfb5ff321b0844" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=fe977fc8285addd5386e940738cdffbbda9eb44e#fe977fc8285addd5386e940738cdffbbda9eb44e" dependencies = [ "anyhow", "futures-util", @@ -2822,7 +2823,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2e14dcf129cab5e1c980655971cfb5ff321b0844#2e14dcf129cab5e1c980655971cfb5ff321b0844" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=fe977fc8285addd5386e940738cdffbbda9eb44e#fe977fc8285addd5386e940738cdffbbda9eb44e" dependencies = [ "anyhow", "app-error", @@ -2916,15 +2917,6 @@ dependencies = [ "ahash 0.7.6", ] -[[package]] -name = "hashbrown" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" -dependencies = [ - "ahash 0.8.3", -] - [[package]] name = "hashbrown" version = "0.14.0" @@ -3258,7 +3250,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2e14dcf129cab5e1c980655971cfb5ff321b0844#2e14dcf129cab5e1c980655971cfb5ff321b0844" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=fe977fc8285addd5386e940738cdffbbda9eb44e#fe977fc8285addd5386e940738cdffbbda9eb44e" dependencies = [ "anyhow", "reqwest", @@ -3634,11 +3626,11 @@ dependencies = [ [[package]] name = "lru" -version = "0.10.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03f1160296536f10c833a82dca22267d5486734230d47bf00bf435885814ba1e" +checksum = "1efa59af2ddfad1854ae27d75009d538d0998b4b2fd47083e743ac1a10e46c60" dependencies = [ - "hashbrown 0.13.2", + "hashbrown 0.14.0", ] [[package]] @@ -5001,13 +4993,14 @@ dependencies = [ [[package]] name = "realtime-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2e14dcf129cab5e1c980655971cfb5ff321b0844#2e14dcf129cab5e1c980655971cfb5ff321b0844" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=fe977fc8285addd5386e940738cdffbbda9eb44e#fe977fc8285addd5386e940738cdffbbda9eb44e" dependencies = [ "anyhow", "bincode", "bytes", "collab", "collab-entity", + "database-entity", "prost", "prost-build", "protoc-bin-vendored", @@ -5745,7 +5738,7 @@ dependencies = [ [[package]] name = "shared_entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2e14dcf129cab5e1c980655971cfb5ff321b0844#2e14dcf129cab5e1c980655971cfb5ff321b0844" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=fe977fc8285addd5386e940738cdffbbda9eb44e#fe977fc8285addd5386e940738cdffbbda9eb44e" dependencies = [ "anyhow", "app-error", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index 397511c258bf..a44ec29baa86 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -29,6 +29,7 @@ tokio = "1.34.0" tokio-stream = "0.1.14" async-trait = "0.1.74" chrono = { version = "0.4.31", default-features = false, features = ["clock"] } +lru = "0.12.0" [dependencies] serde_json.workspace = true @@ -55,7 +56,7 @@ custom-protocol = ["tauri/custom-protocol"] # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "2e14dcf129cab5e1c980655971cfb5ff321b0844" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "fe977fc8285addd5386e940738cdffbbda9eb44e" } # Please use the following script to update collab. # Working directory: frontend # @@ -65,14 +66,14 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "2e1 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "27575c570b3a6975d2efb577367a9c56cbf5a6e1" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "27575c570b3a6975d2efb577367a9c56cbf5a6e1" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "27575c570b3a6975d2efb577367a9c56cbf5a6e1" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "27575c570b3a6975d2efb577367a9c56cbf5a6e1" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "27575c570b3a6975d2efb577367a9c56cbf5a6e1" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "27575c570b3a6975d2efb577367a9c56cbf5a6e1" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "27575c570b3a6975d2efb577367a9c56cbf5a6e1" } -collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "27575c570b3a6975d2efb577367a9c56cbf5a6e1" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "da072600" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "da072600" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "da072600" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "da072600" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "da072600" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "da072600" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "da072600" } +collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "da072600" } diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index b3ae83609533..eb675cd1e4a7 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -124,7 +124,7 @@ checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2e14dcf129cab5e1c980655971cfb5ff321b0844#2e14dcf129cab5e1c980655971cfb5ff321b0844" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=fe977fc8285addd5386e940738cdffbbda9eb44e#fe977fc8285addd5386e940738cdffbbda9eb44e" dependencies = [ "anyhow", "reqwest", @@ -467,7 +467,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" dependencies = [ "borsh-derive", - "hashbrown 0.13.2", + "hashbrown 0.12.3", ] [[package]] @@ -666,7 +666,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2e14dcf129cab5e1c980655971cfb5ff321b0844#2e14dcf129cab5e1c980655971cfb5ff321b0844" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=fe977fc8285addd5386e940738cdffbbda9eb44e#fe977fc8285addd5386e940738cdffbbda9eb44e" dependencies = [ "anyhow", "app-error", @@ -730,7 +730,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" dependencies = [ "anyhow", "async-trait", @@ -750,7 +750,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" dependencies = [ "anyhow", "async-trait", @@ -780,7 +780,7 @@ dependencies = [ [[package]] name = "collab-derive" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" dependencies = [ "proc-macro2", "quote", @@ -792,7 +792,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" dependencies = [ "anyhow", "collab", @@ -812,7 +812,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" dependencies = [ "anyhow", "bytes", @@ -826,7 +826,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" dependencies = [ "anyhow", "chrono", @@ -868,7 +868,7 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" dependencies = [ "anyhow", "async-trait", @@ -890,7 +890,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" dependencies = [ "anyhow", "async-trait", @@ -917,7 +917,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=27575c570b3a6975d2efb577367a9c56cbf5a6e1#27575c570b3a6975d2efb577367a9c56cbf5a6e1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" dependencies = [ "anyhow", "collab", @@ -1150,7 +1150,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.11.2", + "phf 0.8.0", "smallvec", ] @@ -1276,7 +1276,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2e14dcf129cab5e1c980655971cfb5ff321b0844#2e14dcf129cab5e1c980655971cfb5ff321b0844" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=fe977fc8285addd5386e940738cdffbbda9eb44e#fe977fc8285addd5386e940738cdffbbda9eb44e" dependencies = [ "anyhow", "app-error", @@ -1911,6 +1911,7 @@ dependencies = [ "indexmap 1.9.3", "lib-dispatch", "lib-infra", + "lru", "nanoid", "parking_lot", "protobuf", @@ -2465,7 +2466,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2e14dcf129cab5e1c980655971cfb5ff321b0844#2e14dcf129cab5e1c980655971cfb5ff321b0844" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=fe977fc8285addd5386e940738cdffbbda9eb44e#fe977fc8285addd5386e940738cdffbbda9eb44e" dependencies = [ "anyhow", "futures-util", @@ -2481,7 +2482,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2e14dcf129cab5e1c980655971cfb5ff321b0844#2e14dcf129cab5e1c980655971cfb5ff321b0844" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=fe977fc8285addd5386e940738cdffbbda9eb44e#fe977fc8285addd5386e940738cdffbbda9eb44e" dependencies = [ "anyhow", "app-error", @@ -2520,15 +2521,6 @@ dependencies = [ "ahash 0.7.6", ] -[[package]] -name = "hashbrown" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" -dependencies = [ - "ahash 0.8.3", -] - [[package]] name = "hashbrown" version = "0.14.0" @@ -2842,7 +2834,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2e14dcf129cab5e1c980655971cfb5ff321b0844#2e14dcf129cab5e1c980655971cfb5ff321b0844" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=fe977fc8285addd5386e940738cdffbbda9eb44e#fe977fc8285addd5386e940738cdffbbda9eb44e" dependencies = [ "anyhow", "reqwest", @@ -3110,11 +3102,11 @@ checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "lru" -version = "0.10.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "718e8fae447df0c7e1ba7f5189829e63fd536945c8988d61444c19039f16b670" +checksum = "1efa59af2ddfad1854ae27d75009d538d0998b4b2fd47083e743ac1a10e46c60" dependencies = [ - "hashbrown 0.13.2", + "hashbrown 0.14.0", ] [[package]] @@ -3655,7 +3647,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros 0.8.0", + "phf_macros", "phf_shared 0.8.0", "proc-macro-hack", ] @@ -3675,7 +3667,6 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ - "phf_macros 0.11.2", "phf_shared 0.11.2", ] @@ -3743,19 +3734,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "phf_macros" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" -dependencies = [ - "phf_generator 0.11.2", - "phf_shared 0.11.2", - "proc-macro2", - "quote", - "syn 2.0.31", -] - [[package]] name = "phf_shared" version = "0.8.0" @@ -3959,7 +3937,7 @@ checksum = "8bdf592881d821b83d471f8af290226c8d51402259e9bb5be7f9f8bdebbb11ac" dependencies = [ "bytes", "heck 0.4.1", - "itertools 0.11.0", + "itertools 0.10.5", "log", "multimap", "once_cell", @@ -3980,7 +3958,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "265baba7fabd416cf5078179f7d2cbeca4ce7a9041111900675ea7c4cb8a4c32" dependencies = [ "anyhow", - "itertools 0.11.0", + "itertools 0.10.5", "proc-macro2", "quote", "syn 2.0.31", @@ -4319,13 +4297,14 @@ dependencies = [ [[package]] name = "realtime-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2e14dcf129cab5e1c980655971cfb5ff321b0844#2e14dcf129cab5e1c980655971cfb5ff321b0844" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=fe977fc8285addd5386e940738cdffbbda9eb44e#fe977fc8285addd5386e940738cdffbbda9eb44e" dependencies = [ "anyhow", "bincode", "bytes", "collab", "collab-entity", + "database-entity", "prost", "prost-build", "protoc-bin-vendored", @@ -4962,7 +4941,7 @@ dependencies = [ [[package]] name = "shared_entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=2e14dcf129cab5e1c980655971cfb5ff321b0844#2e14dcf129cab5e1c980655971cfb5ff321b0844" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=fe977fc8285addd5386e940738cdffbbda9eb44e#fe977fc8285addd5386e940738cdffbbda9eb44e" dependencies = [ "anyhow", "app-error", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index d20c8bac23db..12ebc4af8608 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -68,6 +68,7 @@ tokio = "1.34.0" tokio-stream = "0.1.14" async-trait = "0.1.74" chrono = { version = "0.4.31", default-features = false, features = ["clock"] } +lru = "0.12.0" [profile.dev] opt-level = 0 @@ -98,7 +99,7 @@ incremental = false # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "2e14dcf129cab5e1c980655971cfb5ff321b0844" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "fe977fc8285addd5386e940738cdffbbda9eb44e" } # Please use the following script to update collab. # Working directory: frontend # @@ -108,11 +109,11 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "2e1 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "27575c570b3a6975d2efb577367a9c56cbf5a6e1" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "27575c570b3a6975d2efb577367a9c56cbf5a6e1" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "27575c570b3a6975d2efb577367a9c56cbf5a6e1" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "27575c570b3a6975d2efb577367a9c56cbf5a6e1" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "27575c570b3a6975d2efb577367a9c56cbf5a6e1" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "27575c570b3a6975d2efb577367a9c56cbf5a6e1" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "27575c570b3a6975d2efb577367a9c56cbf5a6e1" } -collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "27575c570b3a6975d2efb577367a9c56cbf5a6e1" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "da072600" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "da072600" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "da072600" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "da072600" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "da072600" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "da072600" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "da072600" } +collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "da072600" } diff --git a/frontend/rust-lib/flowy-document2/Cargo.toml b/frontend/rust-lib/flowy-document2/Cargo.toml index ed89fcecf8b8..078903d1c3b6 100644 --- a/frontend/rust-lib/flowy-document2/Cargo.toml +++ b/frontend/rust-lib/flowy-document2/Cargo.toml @@ -34,6 +34,7 @@ uuid.workspace = true futures.workspace = true tokio-stream = { workspace = true, features = ["sync"] } scraper = "0.18.0" +lru.workspace = true [dev-dependencies] tempfile = "3.4.0" diff --git a/frontend/rust-lib/flowy-document2/src/manager.rs b/frontend/rust-lib/flowy-document2/src/manager.rs index 8c5a7c5d9925..24b8c58f0b92 100644 --- a/frontend/rust-lib/flowy-document2/src/manager.rs +++ b/frontend/rust-lib/flowy-document2/src/manager.rs @@ -22,9 +22,7 @@ use crate::reminder::DocumentReminderAction; pub trait DocumentUser: Send + Sync { fn user_id(&self) -> Result<i64, FlowyError>; - fn workspace_id(&self) -> Result<String, FlowyError>; - fn token(&self) -> Result<Option<String>, FlowyError>; // unused now. fn collab_db(&self, uid: i64) -> Result<Weak<RocksCollabDB>, FlowyError>; } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs index 99c64387a186..6be4e7036fae 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs @@ -5,9 +5,10 @@ use anyhow::{anyhow, Error}; use client_api::entity::workspace_dto::{CreateWorkspaceMember, WorkspaceMemberChangeset}; use client_api::entity::{AFRole, AFWorkspace, InsertCollabParams, OAuthProvider}; use collab_entity::CollabObject; +use parking_lot::RwLock; use flowy_error::{ErrorCode, FlowyError}; -use flowy_user_deps::cloud::UserCloudService; +use flowy_user_deps::cloud::{UserCloudService, UserUpdate, UserUpdateReceiver}; use flowy_user_deps::entities::*; use lib_infra::box_any::BoxAny; use lib_infra::future::FutureResult; @@ -21,11 +22,15 @@ use crate::supabase::define::{USER_DEVICE_ID, USER_SIGN_IN_URL}; pub(crate) struct AFCloudUserAuthServiceImpl<T> { server: T, + user_change_recv: RwLock<Option<tokio::sync::mpsc::Receiver<UserUpdate>>>, } impl<T> AFCloudUserAuthServiceImpl<T> { - pub(crate) fn new(server: T) -> Self { - Self { server } + pub(crate) fn new(server: T, user_change_recv: tokio::sync::mpsc::Receiver<UserUpdate>) -> Self { + Self { + server, + user_change_recv: RwLock::new(Some(user_change_recv)), + } } } @@ -212,12 +217,14 @@ where } fn get_user_awareness_updates(&self, _uid: i64) -> FutureResult<Vec<Vec<u8>>, Error> { - // TODO(nathan): implement the RESTful API for this FutureResult::new(async { Ok(vec![]) }) } + fn subscribe_user_update(&self) -> Option<UserUpdateReceiver> { + self.user_change_recv.write().take() + } + fn reset_workspace(&self, _collab_object: CollabObject) -> FutureResult<(), Error> { - // TODO(nathan): implement the RESTful API for this FutureResult::new(async { Ok(()) }) } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs index c5431583b12f..e95c0108806e 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use anyhow::Error; use client_api::collab_sync::collab_msg::CollabMessage; +use client_api::entity::UserMessage; use client_api::notify::{TokenState, TokenStateReceiver}; use client_api::ws::{ ConnectState, WSClient, WSClientConfig, WSConnectStateReceiver, WebSocketChannel, @@ -18,7 +19,7 @@ use flowy_error::{ErrorCode, FlowyError}; use flowy_folder_deps::cloud::FolderCloudService; use flowy_server_config::af_cloud_config::AFCloudConfiguration; use flowy_storage::FileStorageService; -use flowy_user_deps::cloud::UserCloudService; +use flowy_user_deps::cloud::{UserCloudService, UserUpdate}; use flowy_user_deps::entities::UserTokenState; use lib_dispatch::prelude::af_spawn; use lib_infra::future::FutureResult; @@ -119,9 +120,26 @@ impl AppFlowyServer for AFCloudServer { info!("{} cloud sync: {}", uid, enable); self.enable_sync.store(enable, Ordering::SeqCst); } + fn user_service(&self) -> Arc<dyn UserCloudService> { let server = AFServerImpl(self.get_client()); - Arc::new(AFCloudUserAuthServiceImpl::new(server)) + let mut user_change = self.ws_client.subscribe_user_changed(); + let (tx, rx) = tokio::sync::mpsc::channel(1); + tokio::spawn(async move { + while let Ok(user_message) = user_change.recv().await { + if let UserMessage::ProfileChange(change) = user_message { + let user_update = UserUpdate { + uid: change.uid, + name: change.name, + email: change.email, + encryption_sign: "".to_string(), + }; + let _ = tx.send(user_update).await; + } + } + }); + + Arc::new(AFCloudUserAuthServiceImpl::new(server, rx)) } fn folder_service(&self) -> Arc<dyn FolderCloudService> { @@ -158,7 +176,7 @@ impl AppFlowyServer for AFCloudServer { match weak_ws_client.upgrade() { None => Ok(None), Some(ws_client) => { - let channel = ws_client.subscribe(object_id).ok(); + let channel = ws_client.subscribe_collab(object_id).ok(); let connect_state_recv = ws_client.subscribe_connect_state(); Ok(channel.map(|c| (c, connect_state_recv, ws_client.is_connected()))) }, diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs index 94673eb8556b..8ce724389239 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs @@ -43,19 +43,19 @@ use crate::AppFlowyEncryption; pub struct SupabaseUserServiceImpl<T> { server: T, realtime_event_handlers: Vec<Box<dyn RealtimeEventHandler>>, - user_update_tx: Option<UserUpdateSender>, + user_update_rx: RwLock<Option<UserUpdateReceiver>>, } impl<T> SupabaseUserServiceImpl<T> { pub fn new( server: T, realtime_event_handlers: Vec<Box<dyn RealtimeEventHandler>>, - user_update_tx: Option<UserUpdateSender>, + user_update_rx: Option<UserUpdateReceiver>, ) -> Self { Self { server, realtime_event_handlers, - user_update_tx, + user_update_rx: RwLock::new(user_update_rx), } } } @@ -275,7 +275,7 @@ where } fn subscribe_user_update(&self) -> Option<UserUpdateReceiver> { - self.user_update_tx.as_ref().map(|tx| tx.subscribe()) + self.user_update_rx.write().take() } fn reset_workspace(&self, collab_object: CollabObject) -> FutureResult<(), Error> { @@ -531,8 +531,8 @@ impl RealtimeEventHandler for RealtimeUserHandler { if let Ok(user_event) = serde_json::from_value::<RealtimeUserEvent>(event.new.clone()) { let _ = self.0.send(UserUpdate { uid: user_event.uid, - name: user_event.name, - email: user_event.email, + name: Some(user_event.name), + email: Some(user_event.email), encryption_sign: user_event.encryption_sign, }); } diff --git a/frontend/rust-lib/flowy-server/src/supabase/server.rs b/frontend/rust-lib/flowy-server/src/supabase/server.rs index 7d917ae962f1..71bb3456c24f 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/server.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/server.rs @@ -137,7 +137,7 @@ impl AppFlowyServer for SupabaseServer { fn user_service(&self) -> Arc<dyn UserCloudService> { // handle the realtime collab update event. - let (user_update_tx, _) = tokio::sync::broadcast::channel(100); + let (user_update_tx, user_update_rx) = tokio::sync::mpsc::channel(1); let collab_update_handler = Box::new(RealtimeCollabUpdateHandler::new( Arc::downgrade(&self.collab_update_sender), @@ -152,7 +152,7 @@ impl AppFlowyServer for SupabaseServer { Arc::new(SupabaseUserServiceImpl::new( SupabaseServerServiceImpl(self.restful_postgres.clone()), handlers, - Some(user_update_tx), + Some(user_update_rx), )) } diff --git a/frontend/rust-lib/flowy-user-deps/src/cloud.rs b/frontend/rust-lib/flowy-user-deps/src/cloud.rs index b54892d9aaff..bfa800acdd6a 100644 --- a/frontend/rust-lib/flowy-user-deps/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-deps/src/cloud.rs @@ -148,13 +148,13 @@ pub trait UserCloudService: Send + Sync + 'static { ) -> FutureResult<(), Error>; } -pub type UserUpdateReceiver = tokio::sync::broadcast::Receiver<UserUpdate>; -pub type UserUpdateSender = tokio::sync::broadcast::Sender<UserUpdate>; +pub type UserUpdateReceiver = tokio::sync::mpsc::Receiver<UserUpdate>; +pub type UserUpdateSender = tokio::sync::mpsc::Sender<UserUpdate>; #[derive(Debug, Clone)] pub struct UserUpdate { pub uid: i64, - pub name: String, - pub email: String, + pub name: Option<String>, + pub email: Option<String>, pub encryption_sign: String, } diff --git a/frontend/rust-lib/flowy-user/src/manager.rs b/frontend/rust-lib/flowy-user/src/manager.rs index 2c17ae31902e..b9bc0352a593 100644 --- a/frontend/rust-lib/flowy-user/src/manager.rs +++ b/frontend/rust-lib/flowy-user/src/manager.rs @@ -108,7 +108,7 @@ impl UserManager { if let Ok(user_service) = user_manager.cloud_services.get_user_service() { if let Some(mut rx) = user_service.subscribe_user_update() { af_spawn(async move { - while let Ok(update) = rx.recv().await { + while let Some(update) = rx.recv().await { if let Some(user_manager) = weak_user_manager.upgrade() { if let Err(err) = user_manager.handler_user_update(update).await { error!("handler_user_update failed: {:?}", err); @@ -526,7 +526,7 @@ impl UserManager { // If the user profile is updated, save the new user profile if new_user_profile.updated_at > old_user_profile.updated_at { - check_encryption_sign(old_user_profile, &new_user_profile.encryption_type.sign()); + validate_encryption_sign(old_user_profile, &new_user_profile.encryption_type.sign()); // Save the new user profile let changeset = UserTableChangeset::from_user_profile(new_user_profile); let _ = upsert_user_profile_change(uid, self.database.get_pool(uid)?, changeset); @@ -722,7 +722,7 @@ impl UserManager { if session.user_id == user_update.uid { debug!("Receive user update: {:?}", user_update); let user_profile = self.get_user_profile(user_update.uid).await?; - if !check_encryption_sign(&user_profile, &user_update.encryption_sign) { + if !validate_encryption_sign(&user_profile, &user_update.encryption_sign) { return Ok(()); } @@ -767,7 +767,7 @@ impl UserManager { } } -fn check_encryption_sign(user_profile: &UserProfile, encryption_sign: &str) -> bool { +fn validate_encryption_sign(user_profile: &UserProfile, encryption_sign: &str) -> bool { // If the local user profile's encryption sign is not equal to the user update's encryption sign, // which means the user enable encryption in another device, we should logout the current user. let is_valid = user_profile.encryption_type.sign() == encryption_sign; diff --git a/frontend/rust-lib/flowy-user/src/services/user_sql.rs b/frontend/rust-lib/flowy-user/src/services/user_sql.rs index 74240ef3b3c5..9b1a252118f7 100644 --- a/frontend/rust-lib/flowy-user/src/services/user_sql.rs +++ b/frontend/rust-lib/flowy-user/src/services/user_sql.rs @@ -120,8 +120,8 @@ impl From<UserUpdate> for UserTableChangeset { fn from(value: UserUpdate) -> Self { UserTableChangeset { id: value.uid.to_string(), - name: Some(value.name), - email: Some(value.email), + name: value.name, + email: value.email, ..Default::default() } } From 4992f9c2810d6e1dd04119e737a892a42d3b4c60 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Tue, 14 Nov 2023 15:41:44 +0800 Subject: [PATCH 54/56] chore: fix-typo (#3934) --- .../database_field_settings_test.dart | 2 +- .../integration_test/database_view_test.dart | 2 +- .../util/database_test_op.dart | 4 +- .../{tar_bar_bloc.dart => tab_bar_bloc.dart} | 77 ++++++++++--------- .../plugins/database_view/board/board.dart | 2 +- .../board/presentation/board_page.dart | 2 +- .../database_view/calendar/calendar.dart | 2 +- .../calendar/presentation/calendar_page.dart | 2 +- .../lib/plugins/database_view/grid/grid.dart | 2 +- .../grid/presentation/grid_page.dart | 4 +- .../{tar_bar => tab_bar}/setting_menu.dart | 0 .../tab_bar_add_button.dart} | 0 .../{tar_bar => tab_bar}/tab_bar_header.dart | 28 +++---- .../{tar_bar => tab_bar}/tab_bar_view.dart | 20 ++--- .../workspace/application/view/view_ext.dart | 2 +- 15 files changed, 75 insertions(+), 74 deletions(-) rename frontend/appflowy_flutter/lib/plugins/database_view/application/{tar_bar_bloc.dart => tab_bar_bloc.dart} (78%) rename frontend/appflowy_flutter/lib/plugins/database_view/{tar_bar => tab_bar}/setting_menu.dart (100%) rename frontend/appflowy_flutter/lib/plugins/database_view/{tar_bar/tar_bar_add_button.dart => tab_bar/tab_bar_add_button.dart} (100%) rename frontend/appflowy_flutter/lib/plugins/database_view/{tar_bar => tab_bar}/tab_bar_header.dart (89%) rename frontend/appflowy_flutter/lib/plugins/database_view/{tar_bar => tab_bar}/tab_bar_view.dart (90%) diff --git a/frontend/appflowy_flutter/integration_test/database_field_settings_test.dart b/frontend/appflowy_flutter/integration_test/database_field_settings_test.dart index b22993821103..909d13b49a45 100644 --- a/frontend/appflowy_flutter/integration_test/database_field_settings_test.dart +++ b/frontend/appflowy_flutter/integration_test/database_field_settings_test.dart @@ -1,5 +1,5 @@ import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart'; -import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart'; +import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_add_button.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pbenum.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; diff --git a/frontend/appflowy_flutter/integration_test/database_view_test.dart b/frontend/appflowy_flutter/integration_test/database_view_test.dart index 88c04eb45c52..740d1232ec5d 100644 --- a/frontend/appflowy_flutter/integration_test/database_view_test.dart +++ b/frontend/appflowy_flutter/integration_test/database_view_test.dart @@ -1,4 +1,4 @@ -import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart'; +import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_add_button.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart index 907f41b57be0..98ae23b2a2b6 100644 --- a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart @@ -36,8 +36,8 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/so import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/filter_button.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_layout.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/sort_button.dart'; -import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_header.dart'; -import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart'; +import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_header.dart'; +import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_add_button.dart'; import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart'; import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/tar_bar_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/tab_bar_bloc.dart similarity index 78% rename from frontend/appflowy_flutter/lib/plugins/database_view/application/tar_bar_bloc.dart rename to frontend/appflowy_flutter/lib/plugins/database_view/application/tab_bar_bloc.dart index 4b4697add2f2..7b87373aea4f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/tar_bar_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/tab_bar_bloc.dart @@ -1,5 +1,5 @@ -import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart'; -import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart'; +import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_view.dart'; +import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_add_button.dart'; import 'package:appflowy/workspace/application/view/prelude.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy_backend/log.dart'; @@ -11,14 +11,15 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'database_controller.dart'; import 'database_view_service.dart'; -part 'tar_bar_bloc.freezed.dart'; +part 'tab_bar_bloc.freezed.dart'; -class GridTabBarBloc extends Bloc<GridTabBarEvent, GridTabBarState> { - GridTabBarBloc({ +class DatabaseTabBarBloc + extends Bloc<DatabaseTabBarEvent, DatabaseTabBarState> { + DatabaseTabBarBloc({ bool isInlineView = false, required ViewPB view, - }) : super(GridTabBarState.initial(view)) { - on<GridTabBarEvent>( + }) : super(DatabaseTabBarState.initial(view)) { + on<DatabaseTabBarEvent>( (event, emit) async { event.when( initial: () { @@ -31,7 +32,7 @@ class GridTabBarBloc extends Bloc<GridTabBarEvent, GridTabBarState> { tabBars: [ ...state.tabBars, ...childViews.map( - (newChildView) => TarBar(view: newChildView), + (newChildView) => DatabaseTabBar(view: newChildView), ), ], tabBarControllerByViewId: _extendsTabBarController(childViews), @@ -64,7 +65,7 @@ class GridTabBarBloc extends Bloc<GridTabBarEvent, GridTabBarState> { if (updatePB.createChildViews.isNotEmpty) { final allTabBars = [ ...state.tabBars, - ...updatePB.createChildViews.map((e) => TarBar(view: e)) + ...updatePB.createChildViews.map((e) => DatabaseTabBar(view: e)) ]; emit( state.copyWith( @@ -115,7 +116,7 @@ class GridTabBarBloc extends Bloc<GridTabBarEvent, GridTabBarState> { ); if (index != -1) { final allTabBars = [...state.tabBars]; - final updatedTabBar = TarBar(view: updatedView); + final updatedTabBar = DatabaseTabBar(view: updatedView); allTabBars[index] = updatedTabBar; emit(state.copyWith(tabBars: allTabBars)); } @@ -136,24 +137,24 @@ class GridTabBarBloc extends Bloc<GridTabBarEvent, GridTabBarState> { void _listenInlineViewChanged() { final controller = state.tabBarControllerByViewId[state.parentView.id]; controller?.onViewUpdated = (newView) { - add(GridTabBarEvent.viewDidUpdate(newView)); + add(DatabaseTabBarEvent.viewDidUpdate(newView)); }; // Only listen the child view changes when the parent view is inline. controller?.onViewChildViewChanged = (update) { - add(GridTabBarEvent.didUpdateChildViews(update)); + add(DatabaseTabBarEvent.didUpdateChildViews(update)); }; } /// Create tab bar controllers for the new views and return the updated map. - Map<String, DatabaseTarBarController> _extendsTabBarController( + Map<String, DatabaseTabBarController> _extendsTabBarController( List<ViewPB> newViews, ) { final tabBarControllerByViewId = {...state.tabBarControllerByViewId}; for (final view in newViews) { - final controller = DatabaseTarBarController(view: view); + final controller = DatabaseTabBarController(view: view); controller.onViewUpdated = (newView) { - add(GridTabBarEvent.viewDidUpdate(newView)); + add(DatabaseTabBarEvent.viewDidUpdate(newView)); }; tabBarControllerByViewId[view.id] = controller; @@ -191,7 +192,7 @@ class GridTabBarBloc extends Bloc<GridTabBarEvent, GridTabBarState> { return; } viewsOrFail.fold( - (views) => add(GridTabBarEvent.didLoadChildViews(views)), + (views) => add(DatabaseTabBarEvent.didLoadChildViews(views)), (err) => Log.error(err), ); }); @@ -199,40 +200,40 @@ class GridTabBarBloc extends Bloc<GridTabBarEvent, GridTabBarState> { } @freezed -class GridTabBarEvent with _$GridTabBarEvent { - const factory GridTabBarEvent.initial() = _Initial; - const factory GridTabBarEvent.didLoadChildViews( +class DatabaseTabBarEvent with _$DatabaseTabBarEvent { + const factory DatabaseTabBarEvent.initial() = _Initial; + const factory DatabaseTabBarEvent.didLoadChildViews( List<ViewPB> childViews, ) = _DidLoadChildViews; - const factory GridTabBarEvent.selectView(String viewId) = _DidSelectView; - const factory GridTabBarEvent.createView(AddButtonAction action) = + const factory DatabaseTabBarEvent.selectView(String viewId) = _DidSelectView; + const factory DatabaseTabBarEvent.createView(AddButtonAction action) = _CreateView; - const factory GridTabBarEvent.renameView(String viewId, String newName) = + const factory DatabaseTabBarEvent.renameView(String viewId, String newName) = _RenameView; - const factory GridTabBarEvent.deleteView(String viewId) = _DeleteView; - const factory GridTabBarEvent.didUpdateChildViews( + const factory DatabaseTabBarEvent.deleteView(String viewId) = _DeleteView; + const factory DatabaseTabBarEvent.didUpdateChildViews( ChildViewUpdatePB updatePB, ) = _DidUpdateChildViews; - const factory GridTabBarEvent.viewDidUpdate(ViewPB view) = _ViewDidUpdate; + const factory DatabaseTabBarEvent.viewDidUpdate(ViewPB view) = _ViewDidUpdate; } @freezed -class GridTabBarState with _$GridTabBarState { - const factory GridTabBarState({ +class DatabaseTabBarState with _$DatabaseTabBarState { + const factory DatabaseTabBarState({ required ViewPB parentView, required int selectedIndex, - required List<TarBar> tabBars, - required Map<String, DatabaseTarBarController> tabBarControllerByViewId, - }) = _GridTabBarState; + required List<DatabaseTabBar> tabBars, + required Map<String, DatabaseTabBarController> tabBarControllerByViewId, + }) = _DatabaseTabBarState; - factory GridTabBarState.initial(ViewPB view) { - final tabBar = TarBar(view: view); - return GridTabBarState( + factory DatabaseTabBarState.initial(ViewPB view) { + final tabBar = DatabaseTabBar(view: view); + return DatabaseTabBarState( parentView: view, selectedIndex: 0, tabBars: [tabBar], tabBarControllerByViewId: { - view.id: DatabaseTarBarController( + view.id: DatabaseTabBarController( view: view, ) }, @@ -240,7 +241,7 @@ class GridTabBarState with _$GridTabBarState { } } -class TarBar extends Equatable { +class DatabaseTabBar extends Equatable { final ViewPB view; final DatabaseTabBarItemBuilder _builder; @@ -248,7 +249,7 @@ class TarBar extends Equatable { DatabaseTabBarItemBuilder get builder => _builder; ViewLayoutPB get layout => view.layout; - TarBar({ + DatabaseTabBar({ required this.view, }) : _builder = view.tarBarItem(); @@ -261,14 +262,14 @@ typedef OnViewChildViewChanged = void Function( ChildViewUpdatePB childViewUpdate, ); -class DatabaseTarBarController { +class DatabaseTabBarController { ViewPB view; final DatabaseController controller; final ViewListener viewListener; OnViewUpdated? onViewUpdated; OnViewChildViewChanged? onViewChildViewChanged; - DatabaseTarBarController({ + DatabaseTabBarController({ required this.view, }) : controller = DatabaseController(view: view), viewListener = ViewListener(viewId: view.id) { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/board.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/board.dart index 10a25d7e5273..17aa09b78893 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/board.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/board.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart'; +import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_view.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart index 716dfde52834..fd9b497135e4 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart @@ -8,7 +8,7 @@ import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; import 'package:appflowy/plugins/database_view/application/row/row_controller.dart'; import 'package:appflowy/plugins/database_view/board/presentation/widgets/board_column_header.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart'; -import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart'; +import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_view.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/calendar.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/calendar.dart index 4a85ff373907..10c2e5414cbc 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/calendar.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/calendar.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart'; +import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_view.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart index c205f3d79220..45ad2309e267 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart @@ -4,7 +4,7 @@ import 'package:appflowy/plugins/database_view/application/database_controller.d import 'package:appflowy/plugins/database_view/calendar/application/calendar_bloc.dart'; import 'package:appflowy/plugins/database_view/calendar/application/unschedule_event_bloc.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart'; +import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_view.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/grid.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/grid.dart index 6bce729003f6..06273bc471a7 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/grid.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/grid.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart'; +import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_view.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart index 99c4151bcc7f..ced36703d613 100755 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart @@ -1,7 +1,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database_view/application/row/row_service.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_setting_bar.dart'; -import 'package:appflowy/plugins/database_view/tar_bar/setting_menu.dart'; +import 'package:appflowy/plugins/database_view/tab_bar/setting_menu.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart'; import 'package:appflowy_backend/log.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -20,7 +20,7 @@ import '../../application/row/row_controller.dart'; import '../application/grid_bloc.dart'; import '../../application/database_controller.dart'; import 'grid_scroll.dart'; -import '../../tar_bar/tab_bar_view.dart'; +import '../../tab_bar/tab_bar_view.dart'; import 'layout/layout.dart'; import 'layout/sizes.dart'; import 'widgets/row/row.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/setting_menu.dart b/frontend/appflowy_flutter/lib/plugins/database_view/tab_bar/setting_menu.dart similarity index 100% rename from frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/setting_menu.dart rename to frontend/appflowy_flutter/lib/plugins/database_view/tab_bar/setting_menu.dart diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tar_bar_add_button.dart b/frontend/appflowy_flutter/lib/plugins/database_view/tab_bar/tab_bar_add_button.dart similarity index 100% rename from frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tar_bar_add_button.dart rename to frontend/appflowy_flutter/lib/plugins/database_view/tab_bar/tab_bar_add_button.dart diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tab_bar_header.dart b/frontend/appflowy_flutter/lib/plugins/database_view/tab_bar/tab_bar_header.dart similarity index 89% rename from frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tab_bar_header.dart rename to frontend/appflowy_flutter/lib/plugins/database_view/tab_bar/tab_bar_header.dart index 9f9f5e2886cc..b83664b72548 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tab_bar_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/tab_bar/tab_bar_header.dart @@ -12,8 +12,8 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../application/tar_bar_bloc.dart'; -import 'tar_bar_add_button.dart'; +import '../application/tab_bar_bloc.dart'; +import 'tab_bar_add_button.dart'; class TabBarHeader extends StatefulWidget { const TabBarHeader({super.key}); @@ -40,14 +40,14 @@ class _TabBarHeaderState extends State<TabBarHeader> { Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - BlocBuilder<GridTabBarBloc, GridTabBarState>( + BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>( builder: (context, state) { return const Flexible( child: DatabaseTabBar(), ); }, ), - BlocBuilder<GridTabBarBloc, GridTabBarState>( + BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>( builder: (context, state) { return SizedBox( width: 200, @@ -66,7 +66,7 @@ class _TabBarHeaderState extends State<TabBarHeader> { ); } - Widget pageSettingBarFromState(GridTabBarState state) { + Widget pageSettingBarFromState(DatabaseTabBarState state) { if (state.tabBars.length < state.selectedIndex) { return const SizedBox.shrink(); } @@ -92,7 +92,7 @@ class _DatabaseTabBarState extends State<DatabaseTabBar> { @override Widget build(BuildContext context) { - return BlocBuilder<GridTabBarBloc, GridTabBarState>( + return BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>( builder: (context, state) { final children = state.tabBars.indexed.map((indexed) { final isSelected = state.selectedIndex == indexed.$1; @@ -102,8 +102,8 @@ class _DatabaseTabBarState extends State<DatabaseTabBar> { view: tabBar.view, isSelected: isSelected, onTap: (selectedView) { - context.read<GridTabBarBloc>().add( - GridTabBarEvent.selectView(selectedView.id), + context.read<DatabaseTabBarBloc>().add( + DatabaseTabBarEvent.selectView(selectedView.id), ); }, ); @@ -122,8 +122,8 @@ class _DatabaseTabBarState extends State<DatabaseTabBar> { ), AddDatabaseViewButton( onTap: (action) async { - context.read<GridTabBarBloc>().add( - GridTabBarEvent.createView(action), + context.read<DatabaseTabBarBloc>().add( + DatabaseTabBarEvent.createView(action), ); }, ), @@ -231,8 +231,8 @@ class TabBarItemButton extends StatelessWidget { title: LocaleKeys.menuAppHeader_renameDialog.tr(), value: view.name, confirm: (newValue) { - context.read<GridTabBarBloc>().add( - GridTabBarEvent.renameView(view.id, newValue), + context.read<DatabaseTabBarBloc>().add( + DatabaseTabBarEvent.renameView(view.id, newValue), ); }, ).show(context); @@ -241,8 +241,8 @@ class TabBarItemButton extends StatelessWidget { NavigatorAlertDialog( title: LocaleKeys.grid_deleteView.tr(), confirm: () { - context.read<GridTabBarBloc>().add( - GridTabBarEvent.deleteView(view.id), + context.read<DatabaseTabBarBloc>().add( + DatabaseTabBarEvent.deleteView(view.id), ); }, ).show(context); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tab_bar_view.dart b/frontend/appflowy_flutter/lib/plugins/database_view/tab_bar/tab_bar_view.dart similarity index 90% rename from frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tab_bar_view.dart rename to frontend/appflowy_flutter/lib/plugins/database_view/tab_bar/tab_bar_view.dart index 0d38a6d4ea29..1d80c56a2b18 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tab_bar_view.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/tab_bar/tab_bar_view.dart @@ -1,4 +1,4 @@ -import 'package:appflowy/plugins/database_view/application/tar_bar_bloc.dart'; +import 'package:appflowy/plugins/database_view/application/tab_bar_bloc.dart'; import 'package:appflowy/plugins/database_view/widgets/share_button.dart'; import 'package:appflowy/plugins/util.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; @@ -63,14 +63,14 @@ class _DatabaseTabBarViewState extends State<DatabaseTabBarView> { @override Widget build(BuildContext context) { - return BlocProvider<GridTabBarBloc>( - create: (context) => GridTabBarBloc(view: widget.view) + return BlocProvider<DatabaseTabBarBloc>( + create: (context) => DatabaseTabBarBloc(view: widget.view) ..add( - const GridTabBarEvent.initial(), + const DatabaseTabBarEvent.initial(), ), child: MultiBlocListener( listeners: [ - BlocListener<GridTabBarBloc, GridTabBarState>( + BlocListener<DatabaseTabBarBloc, DatabaseTabBarState>( listenWhen: (p, c) => p.selectedIndex != c.selectedIndex, listener: (context, state) { _pageController?.animateToPage( @@ -83,7 +83,7 @@ class _DatabaseTabBarViewState extends State<DatabaseTabBarView> { ], child: Column( children: [ - BlocBuilder<GridTabBarBloc, GridTabBarState>( + BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>( builder: (context, state) { return ValueListenableBuilder<bool>( valueListenable: state @@ -105,13 +105,13 @@ class _DatabaseTabBarViewState extends State<DatabaseTabBarView> { ); }, ), - BlocBuilder<GridTabBarBloc, GridTabBarState>( + BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>( builder: (context, state) { return pageSettingBarExtensionFromState(state); }, ), Expanded( - child: BlocBuilder<GridTabBarBloc, GridTabBarState>( + child: BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>( builder: (context, state) { return PageView( pageSnapping: false, @@ -128,7 +128,7 @@ class _DatabaseTabBarViewState extends State<DatabaseTabBarView> { ); } - List<Widget> pageContentFromState(GridTabBarState state) { + List<Widget> pageContentFromState(DatabaseTabBarState state) { return state.tabBars.map((tabBar) { final controller = state.tabBarControllerByViewId[tabBar.viewId]!.controller; @@ -141,7 +141,7 @@ class _DatabaseTabBarViewState extends State<DatabaseTabBarView> { }).toList(); } - Widget pageSettingBarExtensionFromState(GridTabBarState state) { + Widget pageSettingBarExtensionFromState(DatabaseTabBarState state) { if (state.tabBars.length < state.selectedIndex) { return const SizedBox.shrink(); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart index 35a7fe310b2a..6958eadd996b 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart @@ -2,7 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart'; import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart'; -import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart'; +import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_view.dart'; import 'package:appflowy/plugins/document/document.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; From 1c500fbfc57e72b1dc7add00f117d26ee454c0f5 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Tue, 14 Nov 2023 17:21:09 +0800 Subject: [PATCH 55/56] feat: add lru cache for document/database instance (#3938) --- frontend/appflowy_tauri/src-tauri/Cargo.lock | 19 +++---- frontend/appflowy_tauri/src-tauri/Cargo.toml | 16 +++--- frontend/rust-lib/Cargo.lock | 19 +++---- frontend/rust-lib/Cargo.toml | 16 +++--- frontend/rust-lib/flowy-database2/Cargo.toml | 1 + .../rust-lib/flowy-database2/src/manager.rs | 54 +++++++++---------- .../src/services/database/database_editor.rs | 4 +- .../rust-lib/flowy-document2/src/manager.rs | 42 +++++++-------- 8 files changed, 83 insertions(+), 88 deletions(-) diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 2791918d9dbd..40bdee05183b 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -863,7 +863,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881" dependencies = [ "anyhow", "async-trait", @@ -883,7 +883,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881" dependencies = [ "anyhow", "async-trait", @@ -913,7 +913,7 @@ dependencies = [ [[package]] name = "collab-derive" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881" dependencies = [ "proc-macro2", "quote", @@ -925,7 +925,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881" dependencies = [ "anyhow", "collab", @@ -945,7 +945,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881" dependencies = [ "anyhow", "bytes", @@ -959,7 +959,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881" dependencies = [ "anyhow", "chrono", @@ -1001,7 +1001,7 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881" dependencies = [ "anyhow", "async-trait", @@ -1023,7 +1023,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881" dependencies = [ "anyhow", "async-trait", @@ -1050,7 +1050,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881" dependencies = [ "anyhow", "collab", @@ -2012,6 +2012,7 @@ dependencies = [ "lazy_static", "lib-dispatch", "lib-infra", + "lru", "nanoid", "parking_lot", "protobuf", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index a44ec29baa86..079e2cc4db2d 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -66,14 +66,14 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "fe9 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "da072600" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "da072600" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "da072600" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "da072600" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "da072600" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "da072600" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "da072600" } -collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "da072600" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b8097fa891bdbb5826d6e480460c50ab66d26881" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b8097fa891bdbb5826d6e480460c50ab66d26881" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b8097fa891bdbb5826d6e480460c50ab66d26881" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b8097fa891bdbb5826d6e480460c50ab66d26881" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b8097fa891bdbb5826d6e480460c50ab66d26881" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b8097fa891bdbb5826d6e480460c50ab66d26881" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b8097fa891bdbb5826d6e480460c50ab66d26881" } +collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b8097fa891bdbb5826d6e480460c50ab66d26881" } diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index eb675cd1e4a7..b3d42fb21c83 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -730,7 +730,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881" dependencies = [ "anyhow", "async-trait", @@ -750,7 +750,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881" dependencies = [ "anyhow", "async-trait", @@ -780,7 +780,7 @@ dependencies = [ [[package]] name = "collab-derive" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881" dependencies = [ "proc-macro2", "quote", @@ -792,7 +792,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881" dependencies = [ "anyhow", "collab", @@ -812,7 +812,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881" dependencies = [ "anyhow", "bytes", @@ -826,7 +826,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881" dependencies = [ "anyhow", "chrono", @@ -868,7 +868,7 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881" dependencies = [ "anyhow", "async-trait", @@ -890,7 +890,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881" dependencies = [ "anyhow", "async-trait", @@ -917,7 +917,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=da072600#da07260061c6ace8bca0bee1504f333fb8061713" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881" dependencies = [ "anyhow", "collab", @@ -1833,6 +1833,7 @@ dependencies = [ "lazy_static", "lib-dispatch", "lib-infra", + "lru", "nanoid", "parking_lot", "protobuf", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 12ebc4af8608..5de9fe026d3e 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -109,11 +109,11 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "fe9 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "da072600" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "da072600" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "da072600" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "da072600" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "da072600" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "da072600" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "da072600" } -collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "da072600" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b8097fa891bdbb5826d6e480460c50ab66d26881" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b8097fa891bdbb5826d6e480460c50ab66d26881" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b8097fa891bdbb5826d6e480460c50ab66d26881" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b8097fa891bdbb5826d6e480460c50ab66d26881" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b8097fa891bdbb5826d6e480460c50ab66d26881" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b8097fa891bdbb5826d6e480460c50ab66d26881" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b8097fa891bdbb5826d6e480460c50ab66d26881" } +collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b8097fa891bdbb5826d6e480460c50ab66d26881" } diff --git a/frontend/rust-lib/flowy-database2/Cargo.toml b/frontend/rust-lib/flowy-database2/Cargo.toml index edffb0d202d3..ec46cf468e3d 100644 --- a/frontend/rust-lib/flowy-database2/Cargo.toml +++ b/frontend/rust-lib/flowy-database2/Cargo.toml @@ -44,6 +44,7 @@ chrono-tz = "0.8.2" csv = "1.1.6" strum = "0.25" strum_macros = "0.25" +lru.workspace = true [dev-dependencies] event-integration = { path = "../event-integration", default-features = false } diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index 633ca5e3146b..480c0e993fe2 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -1,9 +1,9 @@ -use std::collections::HashMap; +use std::num::NonZeroUsize; use std::sync::{Arc, Weak}; use collab::core::collab::{CollabRawData, MutexCollab}; use collab_database::blocks::BlockEvent; -use collab_database::database::{DatabaseData, YrsDocAction}; +use collab_database::database::{DatabaseData, MutexDatabase, YrsDocAction}; use collab_database::error::DatabaseError; use collab_database::user::{ CollabFuture, CollabObjectUpdate, CollabObjectUpdateByOid, DatabaseCollabService, @@ -12,7 +12,8 @@ use collab_database::user::{ use collab_database::views::{CreateDatabaseParams, CreateViewParams, DatabaseLayout}; use collab_entity::CollabType; use futures::executor::block_on; -use tokio::sync::RwLock; +use lru::LruCache; +use tokio::sync::{Mutex, RwLock}; use tracing::{event, instrument, trace}; use collab_integrate::collab_builder::AppFlowyCollabBuilder; @@ -42,7 +43,7 @@ pub struct DatabaseManager { user: Arc<dyn DatabaseUser>, workspace_database: Arc<RwLock<Option<Arc<WorkspaceDatabase>>>>, task_scheduler: Arc<RwLock<TaskDispatcher>>, - editors: RwLock<HashMap<String, Arc<DatabaseEditor>>>, + editors: Mutex<LruCache<String, Arc<DatabaseEditor>>>, collab_builder: Arc<AppFlowyCollabBuilder>, cloud_service: Arc<dyn DatabaseCloudService>, } @@ -54,11 +55,12 @@ impl DatabaseManager { collab_builder: Arc<AppFlowyCollabBuilder>, cloud_service: Arc<dyn DatabaseCloudService>, ) -> Self { + let editors = Mutex::new(LruCache::new(NonZeroUsize::new(5).unwrap())); Self { user: database_user, workspace_database: Default::default(), task_scheduler, - editors: Default::default(), + editors, collab_builder, cloud_service, } @@ -83,7 +85,8 @@ impl DatabaseManager { // Clear all existing tasks self.task_scheduler.write().await.clear_task(); // Release all existing editors - self.editors.write().await.clear(); + self.editors.lock().await.clear(); + *self.workspace_database.write().await = None; let collab_db = self.user.collab_db(uid)?; let collab_builder = UserDatabaseCollabServiceImpl { @@ -134,11 +137,8 @@ impl DatabaseManager { ); let workspace_database = WorkspaceDatabase::open(uid, collab, collab_db, config, collab_builder); - subscribe_block_event(&workspace_database); *self.workspace_database.write().await = Some(Arc::new(workspace_database)); - // Remove all existing editors - self.editors.write().await.clear(); Ok(()) } @@ -186,44 +186,42 @@ impl DatabaseManager { } pub async fn get_database(&self, database_id: &str) -> FlowyResult<Arc<DatabaseEditor>> { - if let Some(editor) = self.editors.read().await.get(database_id) { - return Ok(editor.clone()); + if let Some(editor) = self.editors.lock().await.get(database_id).cloned() { + return Ok(editor); } self.open_database(database_id).await } pub async fn open_database(&self, database_id: &str) -> FlowyResult<Arc<DatabaseEditor>> { trace!("create new editor for database {}", database_id); - let mut editors = self.editors.write().await; - - let wdb = self.get_workspace_database().await?; - let database = wdb + let database = self + .get_workspace_database() + .await? .get_database(database_id) .await .ok_or_else(FlowyError::collab_not_sync)?; + // Subscribe the [BlockEvent] + subscribe_block_event(&database); + let editor = Arc::new(DatabaseEditor::new(database, self.task_scheduler.clone()).await?); - editors.insert(database_id.to_string(), editor.clone()); + self + .editors + .lock() + .await + .put(database_id.to_string(), editor.clone()); Ok(editor) } #[tracing::instrument(level = "debug", skip_all)] pub async fn close_database_view<T: AsRef<str>>(&self, view_id: T) -> FlowyResult<()> { - // TODO(natan): defer closing the database if the sync is not finished let view_id = view_id.as_ref(); let wdb = self.get_workspace_database().await?; let database_id = wdb.get_database_id_with_view_id(view_id); - if database_id.is_some() { - wdb.close_database(database_id.as_ref().unwrap()); - } - if let Some(database_id) = database_id { - let mut editors = self.editors.write().await; + let mut editors = self.editors.lock().await; if let Some(editor) = editors.get(&database_id) { - if editor.close_view_editor(view_id).await { - editor.close().await; - editors.remove(&database_id); - } + editor.close_view_editor(view_id).await; } } @@ -369,8 +367,8 @@ impl DatabaseManager { } /// Send notification to all clients that are listening to the given object. -fn subscribe_block_event(workspace_database: &WorkspaceDatabase) { - let mut block_event_rx = workspace_database.subscribe_block_event(); +fn subscribe_block_event(database: &Arc<MutexDatabase>) { + let mut block_event_rx = database.lock().subscribe_block_event(); af_spawn(async move { while let Ok(event) = block_event_rx.recv().await { match event { diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index e307f6ac0a5f..9a9476576d2b 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -113,13 +113,13 @@ impl DatabaseEditor { }) } + /// Returns bool value indicating whether the database is empty. + /// #[tracing::instrument(level = "debug", skip_all)] pub async fn close_view_editor(&self, view_id: &str) -> bool { self.database_views.close_view(view_id).await } - pub async fn close(&self) {} - pub async fn get_layout_type(&self, view_id: &str) -> DatabaseLayout { let view = self.database_views.get_view_editor(view_id).await.ok(); if let Some(editor) = view { diff --git a/frontend/rust-lib/flowy-document2/src/manager.rs b/frontend/rust-lib/flowy-document2/src/manager.rs index 24b8c58f0b92..feba726ffe4f 100644 --- a/frontend/rust-lib/flowy-document2/src/manager.rs +++ b/frontend/rust-lib/flowy-document2/src/manager.rs @@ -1,5 +1,6 @@ +use std::num::NonZeroUsize; +use std::sync::Arc; use std::sync::Weak; -use std::{collections::HashMap, sync::Arc}; use collab::core::collab::{CollabRawData, MutexCollab}; use collab_document::blocks::DocumentData; @@ -7,7 +8,8 @@ use collab_document::document::Document; use collab_document::document_data::{default_document_collab_data, default_document_data}; use collab_document::YrsDocAction; use collab_entity::CollabType; -use parking_lot::RwLock; +use lru::LruCache; +use parking_lot::Mutex; use tracing::{event, instrument}; use collab_integrate::collab_builder::AppFlowyCollabBuilder; @@ -30,7 +32,7 @@ pub trait DocumentUser: Send + Sync { pub struct DocumentManager { pub user: Arc<dyn DocumentUser>, collab_builder: Arc<AppFlowyCollabBuilder>, - documents: Arc<RwLock<HashMap<String, Arc<MutexDocument>>>>, + documents: Arc<Mutex<LruCache<String, Arc<MutexDocument>>>>, #[allow(dead_code)] cloud_service: Arc<dyn DocumentCloudService>, storage_service: Weak<dyn FileStorageService>, @@ -43,17 +45,18 @@ impl DocumentManager { cloud_service: Arc<dyn DocumentCloudService>, storage_service: Weak<dyn FileStorageService>, ) -> Self { + let documents = Arc::new(Mutex::new(LruCache::new(NonZeroUsize::new(10).unwrap()))); Self { user, collab_builder, - documents: Default::default(), + documents, cloud_service, storage_service, } } pub async fn initialize(&self, _uid: i64, _workspace_id: String) -> FlowyResult<()> { - self.documents.write().clear(); + self.documents.lock().clear(); Ok(()) } @@ -101,8 +104,8 @@ impl DocumentManager { /// Return the document #[tracing::instrument(level = "debug", skip(self), err)] pub async fn get_document(&self, doc_id: &str) -> FlowyResult<Arc<MutexDocument>> { - if let Some(doc) = self.documents.read().get(doc_id) { - return Ok(doc.clone()); + if let Some(doc) = self.documents.lock().get(doc_id).cloned() { + return Ok(doc); } let mut updates = vec![]; if !self.is_doc_exist(doc_id)? { @@ -133,6 +136,7 @@ impl DocumentManager { } let uid = self.user.user_id()?; + event!(tracing::Level::DEBUG, "Initialize document: {}", doc_id); let collab = self.collab_for_document(uid, doc_id, updates).await?; let document = Arc::new(MutexDocument::open(doc_id, collab)?); @@ -140,8 +144,8 @@ impl DocumentManager { // and we don't want to subscribe to the document changes if we open the same document again. self .documents - .write() - .insert(doc_id.to_string(), document.clone()); + .lock() + .put(doc_id.to_string(), document.clone()); Ok(document) } @@ -162,7 +166,8 @@ impl DocumentManager { #[instrument(level = "debug", skip(self), err)] pub fn close_document(&self, doc_id: &str) -> FlowyResult<()> { - self.documents.write().remove(doc_id); + // TODO(nathan): remove the document from lru cache. Currently, we don't remove it from the cache. + // The lru will pop the least recently used document when the cache is full. Ok(()) } @@ -173,7 +178,9 @@ impl DocumentManager { txn.delete_doc(uid, &doc_id)?; Ok(()) }); - self.documents.write().remove(doc_id); + + // When deleting a document, we need to remove it from the cache. + self.documents.lock().pop(doc_id); } Ok(()) } @@ -213,19 +220,6 @@ impl DocumentManager { .build(uid, doc_id, CollabType::Document, updates, db) .await?; Ok(collab) - - // let doc_id = doc_id.to_string(); - // let (tx, rx) = oneshot::channel(); - // let collab_builder = self.collab_builder.clone(); - // tokio::spawn(async move { - // let collab = collab_builder - // .build(uid, &doc_id, CollabType::Document, updates, db) - // .await - // .unwrap(); - // let _ = tx.send(collab); - // }); - // - // Ok(rx.await.unwrap()) } fn is_doc_exist(&self, doc_id: &str) -> FlowyResult<bool> { From 6a9866b9d27830d94e9623c32ea09abd4a280386 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" <lucas.xu@appflowy.io> Date: Tue, 14 Nov 2023 22:33:07 +0800 Subject: [PATCH 56/56] chore: upgrade flutter to 3.13.9 (#3936) --- .github/workflows/flutter_ci.yaml | 2 +- .github/workflows/mobile_ci.yaml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/rust_coverage.yml | 2 +- frontend/appflowy_flutter/README.md | 4 +- .../integration_test/database_share_test.dart | 6 +- .../document_copy_and_paste_test.dart | 2 +- .../document_with_cover_image_test.dart | 2 +- .../util/editor_test_operations.dart | 2 +- .../integration_test/util/emoji.dart | 2 +- .../util/mock/mock_openai_repository.dart | 2 +- frontend/appflowy_flutter/ios/Podfile.lock | 2 +- .../ios/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- .../bottom_sheet_view_item_body.dart | 2 +- .../bottom_sheet_view_item_header.dart | 2 +- .../details_placeholder_page.dart | 2 +- .../favorite/mobile_favorite_folder.dart | 2 +- .../presentation/home/mobile_folders.dart | 2 +- .../home/mobile_home_page_header.dart | 6 +- .../home/mobile_home_trash_page.dart | 4 +- .../mobile_home_personal_folder.dart | 2 +- .../mobile_home_recent_views.dart | 2 +- .../page_item/mobile_view_item.dart | 2 +- .../setting/appearance_setting_group.dart | 2 +- .../edit_username_bottom_sheet.dart | 2 +- .../personal_info_setting_group.dart | 2 +- .../widgets/flowy_mobile_state_container.dart | 4 +- .../show_flowy_mobile_bottom_sheet.dart | 2 +- .../lib/plugins/base/emoji/emoji_picker.dart | 2 +- .../base/emoji/emoji_picker_header.dart | 2 +- .../plugins/base/emoji/emoji_picker_i18n.dart | 2 +- .../plugins/base/emoji/emoji_search_bar.dart | 2 +- .../plugins/base/emoji/emoji_skin_tone.dart | 2 +- .../lib/plugins/base/icon/icon_picker.dart | 2 +- .../application/cell/cell_controller.dart | 2 +- .../application/field/field_controller.dart | 2 +- .../application/row/row_list.dart | 2 +- .../application/tab_bar_bloc.dart | 9 +- .../widgets/board_column_header.dart | 2 +- .../widgets/board_hidden_groups.dart | 2 +- .../calendar/application/calendar_bloc.dart | 2 +- .../presentation/calendar_event_card.dart | 2 +- .../calendar/presentation/calendar_page.dart | 2 +- .../header/field_type_option_editor.dart | 2 +- .../header/type_option/select_option.dart | 4 +- .../grid/presentation/widgets/shortcuts.dart | 2 +- .../cells/select_option_cell/extension.dart | 2 +- .../row/cells/text_cell/text_cell.dart | 2 +- .../document/presentation/editor_page.dart | 8 +- .../actions/block_action_option_button.dart | 2 +- .../base/built_in_page_widget.dart | 2 +- .../base/insert_page_command.dart | 2 +- .../base/link_to_page_widget.dart | 18 ++-- .../editor_state_paste_node_extension.dart | 2 +- .../editor_plugins/header/cover_editor.dart | 2 +- .../header/cover_editor_bloc.dart | 2 +- .../header/custom_cover_picker.dart | 4 +- .../header/document_header_node_widget.dart | 4 +- .../image/open_ai_image_widget.dart | 2 +- .../image/stability_ai_image_widget.dart | 2 +- .../mention/slash_menu_items.dart | 2 +- .../widgets/auto_completion_node_widget.dart | 4 +- .../openai/widgets/loading.dart | 2 +- .../widgets/smart_edit_node_widget.dart | 2 +- .../document/presentation/editor_style.dart | 2 +- .../presentation/export_page_widget.dart | 2 +- .../presentation/more/more_button.dart | 2 +- .../handlers/date_reference.dart | 2 +- .../handlers/inline_page_reference.dart | 2 +- .../handlers/reminder_reference.dart | 2 +- .../widgets/inline_actions_handler.dart | 2 +- .../lib/plugins/trash/trash_page.dart | 2 +- .../appflowy_flutter/lib/startup/startup.dart | 2 +- .../auth/af_cloud_auth_service.dart | 2 +- .../auth/supabase_auth_service.dart | 4 +- .../auth/supabase_mock_auth_service.dart | 2 +- .../user/presentation/historical_user.dart | 2 +- .../presentation/screens/sign_up_screen.dart | 2 +- .../screens/workspace_error_screen.dart | 2 +- .../presentation/widgets/folder_widget.dart | 2 +- .../menu/sidebar/folder/favorite_folder.dart | 2 +- .../menu/sidebar/folder/personal_folder.dart | 4 +- .../home/menu/sidebar/sidebar.dart | 2 +- .../home/menu/view/view_item.dart | 2 +- .../settings/settings_dialog.dart | 2 +- .../widgets/emoji_picker/src/emoji_lists.dart | 32 +++---- .../emoji_picker/src/emoji_picker.dart | 2 +- .../widgets/setting_third_party_login.dart | 2 +- .../settings_appearance/color_scheme.dart | 4 +- .../direction_setting.dart | 4 +- .../font_family_setting.dart | 2 +- .../settings_customize_shortcuts_view.dart | 4 +- ...settings_file_customize_location_view.dart | 17 +++- .../settings_file_exporter_widget.dart | 41 +++++---- .../widgets/settings_file_system_view.dart | 2 +- .../widgets/settings_notifications_view.dart | 2 +- .../settings/widgets/settings_user_view.dart | 2 +- .../settings/widgets/sync_setting_view.dart | 6 +- .../presentation/widgets/dialogs.dart | 8 +- .../presentation/widgets/pop_up_action.dart | 2 +- .../macos/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- .../appflowy_popover/lib/src/popover.dart | 10 +- .../packages/flowy_infra/pubspec.yaml | 2 +- frontend/appflowy_flutter/pubspec.lock | 92 ++++++++++--------- frontend/appflowy_flutter/pubspec.yaml | 8 +- .../shortcuts_test/shortcuts_cubit_test.dart | 8 +- .../editor/transaction_adapter_test.dart | 2 +- frontend/rust-lib/Cargo.lock | 33 ++++++- .../freezed/generate_freezed.sh | 6 +- frontend/scripts/docker-buildfiles/Dockerfile | 2 +- .../scripts/install_dev_env/install_ios.sh | 12 +-- .../scripts/install_dev_env/install_linux.sh | 12 +-- .../scripts/install_dev_env/install_macos.sh | 12 +-- .../install_dev_env/install_windows.sh | 12 +-- 116 files changed, 312 insertions(+), 260 deletions(-) diff --git a/.github/workflows/flutter_ci.yaml b/.github/workflows/flutter_ci.yaml index aa3dd6b90980..b64a51a00e31 100644 --- a/.github/workflows/flutter_ci.yaml +++ b/.github/workflows/flutter_ci.yaml @@ -23,7 +23,7 @@ on: env: CARGO_TERM_COLOR: always - FLUTTER_VERSION: "3.10.4" + FLUTTER_VERSION: "3.13.9" RUST_TOOLCHAIN: "1.70" CARGO_MAKE_VERSION: "0.36.6" diff --git a/.github/workflows/mobile_ci.yaml b/.github/workflows/mobile_ci.yaml index 43c06aba9f83..17657eafadfd 100644 --- a/.github/workflows/mobile_ci.yaml +++ b/.github/workflows/mobile_ci.yaml @@ -18,7 +18,7 @@ on: - "!frontend/appflowy_tauri/**" env: - FLUTTER_VERSION: "3.10.1" + FLUTTER_VERSION: "3.13.9" RUST_TOOLCHAIN: "1.70" concurrency: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eea2f7edfea0..243062ac2b81 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,7 @@ on: - "*" env: - FLUTTER_VERSION: "3.10.1" + FLUTTER_VERSION: "3.13.9" RUST_TOOLCHAIN: "1.70" jobs: diff --git a/.github/workflows/rust_coverage.yml b/.github/workflows/rust_coverage.yml index 2121fcf6ae25..7d36e625e582 100644 --- a/.github/workflows/rust_coverage.yml +++ b/.github/workflows/rust_coverage.yml @@ -11,7 +11,7 @@ on: env: CARGO_TERM_COLOR: always - FLUTTER_VERSION: "3.10.1" + FLUTTER_VERSION: "3.13.9" RUST_TOOLCHAIN: "1.70" jobs: diff --git a/frontend/appflowy_flutter/README.md b/frontend/appflowy_flutter/README.md index 8ae646031f69..116cd63f22b5 100644 --- a/frontend/appflowy_flutter/README.md +++ b/frontend/appflowy_flutter/README.md @@ -1,7 +1,7 @@ <h1 align="center" style="margin:0"> AppFlowy_Flutter</h1> <div align="center"> - <img src="https://img.shields.io/badge/Flutter-v3.10.1-blue"/> - <img src="https://img.shields.io/badge/Rust-v1.65-orange"/> + <img src="https://img.shields.io/badge/Flutter-v3.13.19-blue"/> + <img src="https://img.shields.io/badge/Rust-v1.70-orange"/> </div> > Documentation for Contributors diff --git a/frontend/appflowy_flutter/integration_test/database_share_test.dart b/frontend/appflowy_flutter/integration_test/database_share_test.dart index bd42847f82c1..3582a6c74625 100644 --- a/frontend/appflowy_flutter/integration_test/database_share_test.dart +++ b/frontend/appflowy_flutter/integration_test/database_share_test.dart @@ -34,7 +34,7 @@ void main() { false, false, false, - false + false, ]; for (final (index, content) in checkboxCells.indexed) { await tester.assertCheckboxCell( @@ -54,7 +54,7 @@ void main() { '10', '11', '12', - '' + '', ]; for (final (index, content) in numberCells.indexed) { await tester.assertCellContent( @@ -152,7 +152,7 @@ void main() { 'Jun 16, 2023', '', '', - '' + '', ]; for (final (index, content) in dateCells.indexed) { await tester.assertDateCellInGrid( diff --git a/frontend/appflowy_flutter/integration_test/document/document_copy_and_paste_test.dart b/frontend/appflowy_flutter/integration_test/document/document_copy_and_paste_test.dart index c4c34841926e..902255055c87 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_copy_and_paste_test.dart +++ b/frontend/appflowy_flutter/integration_test/document/document_copy_and_paste_test.dart @@ -219,7 +219,7 @@ void main() { expect(node.delta!.toJson(), [ { 'insert': text, - 'attributes': {'href': url} + 'attributes': {'href': url}, } ]); }, diff --git a/frontend/appflowy_flutter/integration_test/document/document_with_cover_image_test.dart b/frontend/appflowy_flutter/integration_test/document/document_with_cover_image_test.dart index b656fc116745..e92b849cd530 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_with_cover_image_test.dart +++ b/frontend/appflowy_flutter/integration_test/document/document_with_cover_image_test.dart @@ -2,8 +2,8 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:emoji_mart/emoji_mart.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; diff --git a/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart b/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart index 7f089d518a7d..73dd494922ae 100644 --- a/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart +++ b/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart @@ -12,9 +12,9 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emo import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:easy_localization/easy_localization.dart'; -import 'package:emoji_mart/emoji_mart.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; import 'package:flutter_test/flutter_test.dart'; import 'util.dart'; diff --git a/frontend/appflowy_flutter/integration_test/util/emoji.dart b/frontend/appflowy_flutter/integration_test/util/emoji.dart index f0e5c693a615..d439a9b3f7b6 100644 --- a/frontend/appflowy_flutter/integration_test/util/emoji.dart +++ b/frontend/appflowy_flutter/integration_test/util/emoji.dart @@ -1,4 +1,4 @@ -import 'package:emoji_mart/emoji_mart.dart'; +import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; import 'package:flutter_test/flutter_test.dart'; import 'base.dart'; diff --git a/frontend/appflowy_flutter/integration_test/util/mock/mock_openai_repository.dart b/frontend/appflowy_flutter/integration_test/util/mock/mock_openai_repository.dart index 66d63b248b21..2941bcb49b37 100644 --- a/frontend/appflowy_flutter/integration_test/util/mock/mock_openai_repository.dart +++ b/frontend/appflowy_flutter/integration_test/util/mock/mock_openai_repository.dart @@ -15,7 +15,7 @@ class MyMockClient extends Mock implements http.Client { if (requestType == 'POST' && requestUri == OpenAIRequestType.textCompletion.uri) { final responseHeaders = <String, String>{ - 'content-type': 'text/event-stream' + 'content-type': 'text/event-stream', }; final responseBody = Stream.fromIterable([ utf8.encode( diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index cf59c986e6b0..b358068db882 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -171,7 +171,7 @@ SPEC CHECKSUMS: device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 - file_picker: ce3938a0df3cc1ef404671531facef740d03f920 + file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c diff --git a/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj b/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj index cd5e7de15d61..50fd566bf0b0 100644 --- a/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj +++ b/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj @@ -143,7 +143,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { diff --git a/frontend/appflowy_flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/frontend/appflowy_flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 3db53b6e1fb7..b52b2e698b7e 100644 --- a/frontend/appflowy_flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/frontend/appflowy_flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <Scheme - LastUpgradeVersion = "1300" + LastUpgradeVersion = "1430" version = "1.3"> <BuildAction parallelizeBuildables = "YES" diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart index d4278317ba40..5a4806796c99 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart @@ -98,7 +98,7 @@ class MobileViewItemBottomSheetBody extends StatelessWidget { ? MobileViewItemBottomSheetBodyAction.removeFromFavorites : MobileViewItemBottomSheetBodyAction.addToFavorites, ), - ) + ), ], ); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_header.dart index 9474653789c4..4643247b214a 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_header.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_header.dart @@ -47,7 +47,7 @@ class MobileViewItemBottomSheetHeader extends StatelessWidget { onPressed: () { context.pop(); }, - ) + ), ], ); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/details_placeholder_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/details_placeholder_page.dart index 84a0f20086dd..cef64bc19cb0 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/details_placeholder_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/details_placeholder_page.dart @@ -91,7 +91,7 @@ class DetailsPlaceholderScreenState extends State<DetailsPlaceholderScreen> { style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18), ), ), - ] + ], ], ), ); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart index 933292f38f70..07a36f2a47a5 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart @@ -34,7 +34,7 @@ class MobileFavoritePageFolder extends StatelessWidget { ), BlocProvider( create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()), - ) + ), ], child: MultiBlocListener( listeners: [ diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart index d6d1c14366f1..f1d10ddc6788 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart @@ -34,7 +34,7 @@ class MobileFolders extends StatelessWidget { ), BlocProvider( create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()), - ) + ), ], child: MultiBlocListener( listeners: [ diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart index 98777385b0e0..3cc4108b453a 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart @@ -66,7 +66,7 @@ class MobileHomePageHeader extends StatelessWidget { icon: const Icon( Icons.arrow_drop_down, ), - ) + ), ], ), FlowyText.regular( @@ -76,7 +76,7 @@ class MobileHomePageHeader extends StatelessWidget { fontSize: 12, color: theme.colorScheme.onSurface, overflow: TextOverflow.ellipsis, - ) + ), ], ), ), @@ -87,7 +87,7 @@ class MobileHomePageHeader extends StatelessWidget { icon: const FlowySvg( FlowySvgs.m_setting_m, ), - ) + ), ], ), ); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart index 945bdfeee86d..1870402b01c6 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart @@ -51,7 +51,7 @@ class MobileHomeTrashPage extends StatelessWidget { trashBloc: trashBloc, type: _TrashActionType.restoreAll, ), - ) + ), ], ), ); @@ -214,7 +214,7 @@ class _DeletedFilesListView extends StatelessWidget { gravity: ToastGravity.BOTTOM, ); }, - ) + ), ], ), ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder.dart index 72a2897b31e7..c57d3b259222 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder.dart @@ -64,7 +64,7 @@ class MobilePersonalFolder extends StatelessWidget { MobilePaneActionType.more, ]), ), - ) + ), ], ); }, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart index 32156f74f19a..b4dbd0491037 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart @@ -48,7 +48,7 @@ class _MobileRecentFolderState extends State<MobileRecentFolder> { // the recent views are in reverse order recentViews: recentViews, ), - const VSpace(12.0) + const VSpace(12.0), ], ); }, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart index f6d9c44ef1cf..251332a7b231 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart @@ -312,7 +312,7 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> { fontSize: 18.0, overflow: TextOverflow.ellipsis, ), - ) + ), ]; // hover action diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance_setting_group.dart index d040f4cd27a1..d85e5fe2731f 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance_setting_group.dart @@ -40,7 +40,7 @@ class _AppearanceSettingGroupState extends State<AppearanceSettingGroup> { color: theme.colorScheme.onSurface, ), ), - const Icon(Icons.chevron_right) + const Icon(Icons.chevron_right), ], ), onTap: () { diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/edit_username_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/edit_username_bottom_sheet.dart index 4a68f411f575..026fb8466c62 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/edit_username_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/edit_username_bottom_sheet.dart @@ -71,7 +71,7 @@ class _EditUsernameBottomSheetState extends State<EditUsernameBottomSheet> { onPressed: () { widget.context.pop(); }, - ) + ), ], ), const SizedBox( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/personal_info_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/personal_info_setting_group.dart index 397621248c51..a2807732a156 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/personal_info_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/personal_info_setting_group.dart @@ -62,7 +62,7 @@ class PersonalInfoSettingGroup extends StatelessWidget { }, ); }, - ) + ), ], ); }, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_state_container.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_state_container.dart index ad5aa3c894b8..383906955285 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_state_container.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_state_container.dart @@ -92,8 +92,8 @@ class FlowyMobileStateContainer extends StatelessWidget { ], ); }, - ) - ] + ), + ], ], ), ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart index b71a664eb585..78086f413fc8 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart @@ -48,7 +48,7 @@ class _BottomSheetTitle extends StatelessWidget { onPressed: () { context.pop(); }, - ) + ), ], ); } diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart index fb9a55652fbd..4989bdb04321 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart @@ -1,9 +1,9 @@ import 'package:appflowy/plugins/base/emoji/emoji_picker_header.dart'; import 'package:appflowy/plugins/base/emoji/emoji_search_bar.dart'; import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart'; -import 'package:emoji_mart/emoji_mart.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; // use a global value to store the selected emoji to prevent reloading every time. EmojiData? _cachedEmojiData; diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_header.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_header.dart index 19b3ad939a82..9619f00d30dc 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_header.dart @@ -1,7 +1,7 @@ import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:emoji_mart/emoji_mart.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; class FlowyEmojiHeader extends StatelessWidget { const FlowyEmojiHeader({ diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_i18n.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_i18n.dart index 0c4fb066aaf5..13ba942f4947 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_i18n.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_i18n.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:emoji_mart/emoji_mart.dart'; +import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; class FlowyEmojiPickerI18n extends EmojiPickerI18n { @override diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_search_bar.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_search_bar.dart index c7cf0a094374..34e897901af3 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_search_bar.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_search_bar.dart @@ -3,10 +3,10 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:emoji_mart/emoji_mart.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; typedef EmojiKeywordChangedCallback = void Function(String keyword); typedef EmojiSkinToneChanged = void Function(EmojiSkinTone skinTone); diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_skin_tone.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_skin_tone.dart index a3f934ac898a..e8da112660b4 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_skin_tone.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_skin_tone.dart @@ -1,10 +1,10 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:emoji_mart/emoji_mart.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; // use a temporary global value to store last selected skin tone EmojiSkinTone? lastSelectedEmojiSkinTone; diff --git a/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart index 6b27e5a86560..818a3bcbf706 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart @@ -111,7 +111,7 @@ class _FlowyIconPickerState extends State<FlowyIconPicker> LocaleKeys.emoji_emojiTab.tr(), ), ), - ) + ), ], ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart index b37e990f21fa..40c644eec315 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart @@ -219,7 +219,7 @@ class CellController<T, D> extends Equatable { @override List<Object> get props => [ _cellCache.get(_cacheKey) ?? "", - _cellContext.rowId + _cellContext.fieldInfo.id + _cellContext.rowId + _cellContext.fieldInfo.id, ]; } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_controller.dart index 7556640fb9e6..4c660dc25cc5 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_controller.dart @@ -392,7 +392,7 @@ class FieldController { } final List<FieldInfo> newFields = fieldInfos; final Map<String, FieldIdPB> deletedFieldMap = { - for (final fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder + for (final fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder, }; newFields.retainWhere((field) => (deletedFieldMap[field.id] == null)); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_list.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_list.dart index 4fd489dd985f..47663f0a44c3 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_list.dart @@ -100,7 +100,7 @@ class RowList { final List<RowInfo> newRows = []; final DeletedIndexs deletedIndex = []; final Map<String, String> deletedRowByRowId = { - for (var rowId in rowIds) rowId: rowId + for (final rowId in rowIds) rowId: rowId, }; _rowInfos.asMap().forEach((index, RowInfo rowInfo) { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/tab_bar_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/tab_bar_bloc.dart index 7b87373aea4f..91bf95f9a64d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/tab_bar_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/tab_bar_bloc.dart @@ -1,5 +1,5 @@ -import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_view.dart'; import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_add_button.dart'; +import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_view.dart'; import 'package:appflowy/workspace/application/view/prelude.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy_backend/log.dart'; @@ -65,7 +65,8 @@ class DatabaseTabBarBloc if (updatePB.createChildViews.isNotEmpty) { final allTabBars = [ ...state.tabBars, - ...updatePB.createChildViews.map((e) => DatabaseTabBar(view: e)) + ...updatePB.createChildViews + .map((e) => DatabaseTabBar(view: e)), ]; emit( state.copyWith( @@ -80,7 +81,7 @@ class DatabaseTabBarBloc if (updatePB.deleteChildViews.isNotEmpty) { final allTabBars = [...state.tabBars]; final tabBarControllerByViewId = { - ...state.tabBarControllerByViewId + ...state.tabBarControllerByViewId, }; var newSelectedIndex = state.selectedIndex; for (final viewId in updatePB.deleteChildViews) { @@ -235,7 +236,7 @@ class DatabaseTabBarState with _$DatabaseTabBarState { tabBarControllerByViewId: { view.id: DatabaseTabBarController( view: view, - ) + ), }, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/widgets/board_column_header.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/widgets/board_column_header.dart index b3fe562e4817..67c01a51a3f2 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/widgets/board_column_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/widgets/board_column_header.dart @@ -237,7 +237,7 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> { ), ), ), - ) + ), ], ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/widgets/board_hidden_groups.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/widgets/board_hidden_groups.dart index c40434c5605c..b5673de40074 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/widgets/board_hidden_groups.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/widgets/board_hidden_groups.dart @@ -431,7 +431,7 @@ class HiddenGroupPopupItemList extends StatelessWidget { }, ); }, - ) + ), ]; return ListView.separated( diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart index f4a2cbdf7aa0..0a08d33a7448 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart @@ -283,7 +283,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> { return; } fieldInfoByFieldId = { - for (var fieldInfo in fieldInfos) fieldInfo.field.id: fieldInfo + for (final fieldInfo in fieldInfos) fieldInfo.field.id: fieldInfo, }; }, onRowsCreated: (rowIds) async { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_event_card.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_event_card.dart index 80b26e9b2ef5..b7301bc880f5 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_event_card.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_event_card.dart @@ -230,7 +230,7 @@ class _EventCardState extends State<EventCard> { color: Theme.of(context).hintColor, overflow: TextOverflow.ellipsis, ), - ) + ), ], ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart index 45ad2309e267..a3618aab6cee 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart @@ -435,7 +435,7 @@ class UnscheduleEventsList extends StatelessWidget { PopoverContainer.of(context).close(); }, ), - ) + ), ]; return ListView.separated( diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart index e5b241883454..6ea45ce5bb2f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart @@ -50,7 +50,7 @@ class FieldTypeOptionEditor extends StatelessWidget { final List<Widget> children = [ SwitchFieldButton(popoverMutex: popoverMutex), - if (typeOptionWidget != null) typeOptionWidget + if (typeOptionWidget != null) typeOptionWidget, ]; return ListView( diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/select_option.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/select_option.dart index 9dd452913993..e3399e682c51 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/select_option.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/select_option.dart @@ -48,7 +48,7 @@ class SelectOptionTypeOptionWidget extends StatelessWidget { const VSpace(10), if (state.options.isEmpty && !state.isEditingOption) const _AddOptionButton(), - _OptionList(popoverMutex: popoverMutex) + _OptionList(popoverMutex: popoverMutex), ]; return ListView.builder( @@ -77,7 +77,7 @@ class OptionTitle extends StatelessWidget { child: FlowyText.medium( LocaleKeys.grid_field_optionTitle.tr(), ), - ) + ), ]; if (state.options.isNotEmpty && !state.isEditingOption) { children.add(const Spacer()); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/shortcuts.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/shortcuts.dart index 1e38c5647d4a..d3f7e7e9c462 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/shortcuts.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/shortcuts.dart @@ -19,7 +19,7 @@ class GridShortcuts extends StatelessWidget { } Map<ShortcutActivator, Intent> bindKeys(List<LogicalKeyboardKey> keys) { - return {for (var key in keys) LogicalKeySet(key): KeyboardKeyIdent(key)}; + return {for (final key in keys) LogicalKeySet(key): KeyboardKeyIdent(key)}; } Map<Type, Action<Intent>> bindActions() { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart index 7f5b508fcac0..4349616c4934 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart @@ -123,7 +123,7 @@ class SelectOptionTag extends StatelessWidget { FlowySvgs.close_s, ), ), - ] + ], ], ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/text_cell/text_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/text_cell/text_cell.dart index 09a78f91fbfd..84de14129b21 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/text_cell/text_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/text_cell/text_cell.dart @@ -126,7 +126,7 @@ class _GridTextCellState extends GridEditableTextCell<GridTextCell> { isDense: true, ), ), - ) + ), ], ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index c6d74ad1eed6..3148c5164c6a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -265,13 +265,17 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> { child: MobileFloatingToolbar( editorState: editorState, editorScrollController: editorScrollController, - toolbarBuilder: (context, anchor) { + toolbarBuilder: (context, anchor, closeToolbar) { return AdaptiveTextSelectionToolbar.editable( clipboardStatus: ClipboardStatus.pasteable, - onCopy: () => copyCommand.execute(editorState), + onCopy: () { + copyCommand.execute(editorState); + closeToolbar(); + }, onCut: () => cutCommand.execute(editorState), onPaste: () => pasteCommand.execute(editorState), onSelectAll: () => selectAllCommand.execute(editorState), + onLiveTextInput: null, anchors: TextSelectionToolbarAnchors( primaryAnchor: anchor, ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart index 8a262e671ccd..e4e1f954f8f3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart @@ -79,7 +79,7 @@ class BlockOptionButton extends StatelessWidget { ), TextSpan( text: LocaleKeys.document_plugins_optionAction_toOpenMenu.tr(), - ) + ), ], ), onTap: () { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart index c3fc935f4617..ded85f476937 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart @@ -170,7 +170,7 @@ class _BuiltInPageWidgetState extends State<BuiltInPageWidget> { } controller.close(); }, - ) + ), ], ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart index fba3c8644ce0..759855e102d5 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart @@ -84,7 +84,7 @@ extension InsertDatabase on EditorState { MentionBlockKeys.mention: { MentionBlockKeys.type: MentionType.page.name, MentionBlockKeys.pageId: view.id, - } + }, }, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart index 0a87e2e189e3..fcc971665fcf 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart @@ -44,13 +44,15 @@ void showLinkToPageMenu( await editorState.insertReferencePage(viewPB, pageType); linkToPageMenuEntry.remove(); } on FlowyError catch (e) { - Dialogs.show( - child: FlowyErrorPage.message( - e.msg, - howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), - ), - context, - ); + if (context.mounted) { + Dialogs.show( + child: FlowyErrorPage.message( + e.msg, + howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), + ), + context, + ); + } } }, ), @@ -149,7 +151,7 @@ class _LinkToPageMenuState extends State<LinkToPageMenu> { LogicalKeyboardKey.arrowUp, LogicalKeyboardKey.arrowDown, LogicalKeyboardKey.tab, - LogicalKeyboardKey.enter + LogicalKeyboardKey.enter, ]; if (!acceptedKeys.contains(event.logicalKey)) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart index 369256eb84b9..a925182a3613 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart @@ -119,7 +119,7 @@ extension PasteNodes on EditorState { if (nodes.last.children.isNotEmpty) { return [ ...path, - ...calculatePath([0], nodes.last.children.toList()) + ...calculatePath([0], nodes.last.children.toList()), ]; } return path; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor.dart index 919451adac3f..ea3011205071 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor.dart @@ -19,7 +19,7 @@ const String kLocalImagesKey = 'local_images'; List<String> get builtInAssetImages => [ "assets/images/app_flowy_abstract_cover_1.jpg", - "assets/images/app_flowy_abstract_cover_2.jpg" + "assets/images/app_flowy_abstract_cover_2.jpg", ]; class ChangeCoverPopover extends StatefulWidget { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor_bloc.dart index 12a64a889c9c..c6c6fcc6e395 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor_bloc.dart @@ -102,7 +102,7 @@ class ChangeCoverPopoverBloc transaction.updateNode(node, { DocumentHeaderBlockKeys.coverType: CoverType.none.toString(), DocumentHeaderBlockKeys.icon: - node.attributes[DocumentHeaderBlockKeys.icon] + node.attributes[DocumentHeaderBlockKeys.icon], }); return editorState.apply(transaction); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/custom_cover_picker.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/custom_cover_picker.dart index 83d39b3bef1c..3dade8ae1a99 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/custom_cover_picker.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/custom_cover_picker.dart @@ -148,7 +148,7 @@ class _NetworkImageUrlInputState extends State<NetworkImageUrlInput> { title: LocaleKeys.document_plugins_cover_add.tr(), borderRadius: Corners.s8Border, ), - ) + ), ], ); } @@ -322,7 +322,7 @@ class _CoverImagePreviewWidgetState extends State<CoverImagePreviewWidget> { (l) => _buildImageDeleteButton(context), (r) => Container(), ) - : Container() + : Container(), ], ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart index 2905a3271523..e18e74de6bbc 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart @@ -173,7 +173,7 @@ class _DocumentHeaderNodeWidgetState extends State<DocumentHeaderNodeWidget> { DocumentHeaderBlockKeys.coverDetails: widget.node.attributes[DocumentHeaderBlockKeys.coverDetails], DocumentHeaderBlockKeys.icon: - widget.node.attributes[DocumentHeaderBlockKeys.icon] + widget.node.attributes[DocumentHeaderBlockKeys.icon], }; if (cover != null) { attributes[DocumentHeaderBlockKeys.coverType] = cover.$1.toString(); @@ -398,7 +398,7 @@ class DocumentCoverState extends State<DocumentCover> { width: double.infinity, child: _buildCoverImage(), ), - if (!isOverlayButtonsHidden) _buildCoverOverlayButtons(context) + if (!isOverlayButtonsHidden) _buildCoverOverlayButtons(context), ], ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/open_ai_image_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/open_ai_image_widget.dart index 2c1ec6f1f826..37a2a0694649 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/open_ai_image_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/open_ai_image_widget.dart @@ -88,7 +88,7 @@ class _OpenAIImageWidgetState extends State<OpenAIImageWidget> { ); }, ), - ) + ), ], ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/stability_ai_image_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/stability_ai_image_widget.dart index 7488fb09055b..d0d589d206f7 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/stability_ai_image_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/stability_ai_image_widget.dart @@ -104,7 +104,7 @@ class _StabilityAIImageWidgetState extends State<StabilityAIImageWidget> { ); }, ), - ) + ), ], ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/slash_menu_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/slash_menu_items.dart index c2e8bf0769f0..75b418e3e92b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/slash_menu_items.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/slash_menu_items.dart @@ -37,7 +37,7 @@ Future<void> _insertDateReference(EditorState editorState) async { MentionBlockKeys.mention: { MentionBlockKeys.type: MentionType.date.name, MentionBlockKeys.date: DateTime.now().toIso8601String(), - } + }, }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/auto_completion_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/auto_completion_node_widget.dart index 121cb0776d91..e9d87a9d34e2 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/auto_completion_node_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/auto_completion_node_widget.dart @@ -157,7 +157,7 @@ class _AutoCompletionBlockComponentState onKeep: _onExit, onRewrite: _onRewrite, onDiscard: _onDiscard, - ) + ), ], ], ), @@ -477,7 +477,7 @@ class AutoCompletionHeader extends StatelessWidget { onTap: () async { await openLearnMorePage(); }, - ) + ), ], ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart index 820c2db84c1f..34c53d4ebcc1 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart @@ -18,7 +18,7 @@ class Loading { children: [ Center( child: CircularProgressIndicator(), - ) + ), ], ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_node_widget.dart index 51b0292d9eaf..7381489ff019 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_node_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_node_widget.dart @@ -254,7 +254,7 @@ class _SmartEditInputWidgetState extends State<SmartEditInputWidget> { onTap: () async { await openLearnMorePage(); }, - ) + ), ], ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart index eab6c10fe3d5..8a4d16b53dd2 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -132,7 +132,7 @@ class EditorStyleCustomizer { fontSize + 8, fontSize + 4, fontSize + 2, - fontSize + fontSize, ]; return TextStyle( fontSize: fontSizes.elementAtOrNull(level - 1) ?? fontSize, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/export_page_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/export_page_widget.dart index 1da2e9da9927..c8609c114e1b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/export_page_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/export_page_widget.dart @@ -31,7 +31,7 @@ class ExportPageWidget extends StatelessWidget { width: 100, height: 30, onPressed: onTap, - ) + ), ], ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/more/more_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/more/more_button.dart index 202e820bb515..53d79ed67924 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/more/more_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/more/more_button.dart @@ -35,7 +35,7 @@ class DocumentMoreButton extends StatelessWidget { BlocProvider.value( value: context.read<DocumentAppearanceCubit>(), child: const FontSizeSwitcher(), - ) + ), ], ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/date_reference.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/date_reference.dart index 9f84abec9ad6..75ac0ead2893 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/date_reference.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/date_reference.dart @@ -124,7 +124,7 @@ class DateReferenceService { MentionBlockKeys.mention: { MentionBlockKeys.type: MentionType.date.name, MentionBlockKeys.date: date.toIso8601String(), - } + }, }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart index 4657f4d46f00..0904105dbae0 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart @@ -109,7 +109,7 @@ class InlinePageReferenceService { MentionBlockKeys.mention: { MentionBlockKeys.type: MentionType.page.name, MentionBlockKeys.pageId: view.id, - } + }, }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart index efbe880a8cbe..6b5f29af99f6 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart @@ -150,7 +150,7 @@ class ReminderReferenceService { MentionBlockKeys.type: MentionType.reminder.name, MentionBlockKeys.date: date.toIso8601String(), MentionBlockKeys.uid: reminder.id, - } + }, }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart index af58ea17cc47..4d8a64e6dac4 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart @@ -226,7 +226,7 @@ class _InlineActionsHandlerState extends State<InlineActionsHandler> { ![ ...moveKeys, LogicalKeyboardKey.arrowLeft, - LogicalKeyboardKey.arrowRight + LogicalKeyboardKey.arrowRight, ].contains(event.logicalKey)) { /// Prevents dismissal of context menu by notifying the parent /// that the selection change occurred from the handler. diff --git a/frontend/appflowy_flutter/lib/plugins/trash/trash_page.dart b/frontend/appflowy_flutter/lib/plugins/trash/trash_page.dart index 9b0631b9b035..2af389562dcf 100644 --- a/frontend/appflowy_flutter/lib/plugins/trash/trash_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/trash/trash_page.dart @@ -110,7 +110,7 @@ class _TrashPageState extends State<TrashPage> { onTap: () => context.read<TrashBloc>().add(const TrashEvent.deleteAll()), ), - ) + ), ], ), ); diff --git a/frontend/appflowy_flutter/lib/startup/startup.dart b/frontend/appflowy_flutter/lib/startup/startup.dart index 37a6fa6bcb54..c6a939890c69 100644 --- a/frontend/appflowy_flutter/lib/startup/startup.dart +++ b/frontend/appflowy_flutter/lib/startup/startup.dart @@ -75,7 +75,7 @@ class FlowyRunner { InitSupabaseTask(), InitAppFlowyCloudTask(), const InitAppWidgetTask(), - const InitPlatformServiceTask() + const InitPlatformServiceTask(), ], ], ); diff --git a/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart index 6f1988e67f19..9e08959e6da6 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart @@ -100,7 +100,7 @@ class AFCloudAuthService implements AuthService { authType: AuthTypePB.AFCloud, map: { AuthServiceMapKeys.signInURL: uri.toString(), - AuthServiceMapKeys.deviceId: deviceId + AuthServiceMapKeys.deviceId: deviceId, }, ); final result = await UserEventOauthSignIn(payload) diff --git a/frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart index ad6021aef279..0263f7bcaffe 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart @@ -97,7 +97,7 @@ class SupabaseAuthService implements AuthService { map: { AuthServiceMapKeys.uuid: userId, AuthServiceMapKeys.email: userEmail, - AuthServiceMapKeys.deviceId: await getDeviceId() + AuthServiceMapKeys.deviceId: await getDeviceId(), }, ); }, @@ -140,7 +140,7 @@ class SupabaseAuthService implements AuthService { map: { AuthServiceMapKeys.uuid: userId, AuthServiceMapKeys.email: userEmail, - AuthServiceMapKeys.deviceId: await getDeviceId() + AuthServiceMapKeys.deviceId: await getDeviceId(), }, ); }, diff --git a/frontend/appflowy_flutter/lib/user/application/auth/supabase_mock_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/supabase_mock_auth_service.dart index 20509f346e31..5457cfc5a4a1 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/supabase_mock_auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/supabase_mock_auth_service.dart @@ -61,7 +61,7 @@ class MockAuthService implements AuthService { map: { AuthServiceMapKeys.uuid: uuid, AuthServiceMapKeys.email: email, - AuthServiceMapKeys.deviceId: 'MockDeviceId' + AuthServiceMapKeys.deviceId: 'MockDeviceId', }, ); diff --git a/frontend/appflowy_flutter/lib/user/presentation/historical_user.dart b/frontend/appflowy_flutter/lib/user/presentation/historical_user.dart index 9e74e2c0ae1a..52f2d498615b 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/historical_user.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/historical_user.dart @@ -48,7 +48,7 @@ class HistoricalUserList extends StatelessWidget { }, itemCount: state.historicalUsers.length, ), - ) + ), ], ); } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_up_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_up_screen.dart index 97b66e6d2faf..3995af40c0b9 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_up_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_up_screen.dart @@ -81,7 +81,7 @@ class SignUpForm extends StatelessWidget { if (context.read<SignUpBloc>().state.isSubmitting) ...[ const SizedBox(height: 8), const LinearProgressIndicator(value: null), - ] + ], ], ), ); diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_error_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_error_screen.dart index 7c3219ded919..948914867ad5 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_error_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_error_screen.dart @@ -129,7 +129,7 @@ class WorkspaceErrorDescription extends StatelessWidget { "Error code: ${state.initialError.code.value.toString()}", fontSize: 12, maxLines: 1, - ) + ), ], ); }, diff --git a/frontend/appflowy_flutter/lib/user/presentation/widgets/folder_widget.dart b/frontend/appflowy_flutter/lib/user/presentation/widgets/folder_widget.dart index 0945c2f69cf9..4896e63c5afa 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/widgets/folder_widget.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/widgets/folder_widget.dart @@ -185,7 +185,7 @@ class CreateFolderWidgetState extends State<CreateFolderWidget> { } }, ), - ) + ), ], ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/favorite_folder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/favorite_folder.dart index a064f567004c..5e99051cce6b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/favorite_folder.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/favorite_folder.dart @@ -63,7 +63,7 @@ class FavoriteFolder extends StatelessWidget { onTertiarySelected: (view) => context.read<TabsBloc>().openTab(view), ), - ) + ), ], ); }, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart index 0b07ceb12b73..b272c0557ede 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart @@ -62,7 +62,7 @@ class PersonalFolder extends StatelessWidget { onTertiarySelected: (view) => context.read<TabsBloc>().openTab(view), ), - ) + ), ], ); }, @@ -136,7 +136,7 @@ class _PersonalFolderHeaderState extends State<PersonalFolderHeader> { ); }, ), - ] + ], ], ), ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart index 55c491a120c3..d4064f19082d 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart @@ -51,7 +51,7 @@ class HomeSideBar extends StatelessWidget { ), BlocProvider( create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()), - ) + ), ], child: MultiBlocListener( listeners: [ diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart index 6a49b6a5bdd7..91fce938ebf0 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart @@ -324,7 +324,7 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> { widget.view.name, overflow: TextOverflow.ellipsis, ), - ) + ), ]; // hover action diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart index 82f096347f90..7fec6a28b3bf 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -76,7 +76,7 @@ class SettingsDialog extends StatelessWidget { context.read<SettingsDialogBloc>().state.page, context.read<SettingsDialogBloc>().state.userProfile, ), - ) + ), ], ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/emoji_lists.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/emoji_lists.dart index 6cb3681206cd..6cf6f234864f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/emoji_lists.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/emoji_lists.dart @@ -373,7 +373,7 @@ final Map<String, String> smileys = Map.fromIterables([ 'Rescue Worker’s Helmet', 'Lipstick', 'Ring', - 'Briefcase' + 'Briefcase', ], [ '😀', '😃', @@ -734,7 +734,7 @@ final Map<String, String> smileys = Map.fromIterables([ '⛑', '💄', '💍', - '💼' + '💼', ]); /// Map of all possible emojis along with their names in [Category.ANIMALS] @@ -919,7 +919,7 @@ final Map<String, String> animals = Map.fromIterables([ 'Christmas Tree', 'Sparkles', 'Tanabata Tree', - 'Pine Decoration' + 'Pine Decoration', ], [ '🐶', '🐱', @@ -1101,7 +1101,7 @@ final Map<String, String> animals = Map.fromIterables([ '🎄', '✨', '🎋', - '🎍' + '🎍', ]); /// Map of all possible emojis along with their names in [Category.FOODS] @@ -1210,7 +1210,7 @@ final Map<String, String> foods = Map.fromIterables([ 'Chopsticks', 'Fork and Knife With Plate', 'Fork and Knife', - 'Spoon' + 'Spoon', ], [ '🍇', '🍈', @@ -1316,7 +1316,7 @@ final Map<String, String> foods = Map.fromIterables([ '🥢', '🍽', '🍴', - '🥄' + '🥄', ]); /// Map of all possible emojis along with their names in [Category.TRAVEL] @@ -1445,7 +1445,7 @@ final Map<String, String> travel = Map.fromIterables([ 'Passport Control', 'Customs', 'Baggage Claim', - 'Left Luggage' + 'Left Luggage', ], [ '🚣', '🗾', @@ -1571,7 +1571,7 @@ final Map<String, String> travel = Map.fromIterables([ '🛂', '🛃', '🛄', - '🛅' + '🛅', ]); /// Map of all possible emojis along with their names in [Category.ACTIVITIES] @@ -1667,7 +1667,7 @@ final Map<String, String> activities = Map.fromIterables([ 'Violin', 'Drum', 'Clapper Board', - 'Bow and Arrow' + 'Bow and Arrow', ], [ '🕴', '🧗', @@ -1760,7 +1760,7 @@ final Map<String, String> activities = Map.fromIterables([ '🎻', '🥁', '🎬', - '🏹' + '🏹', ]); /// Map of all possible emojis along with their names in [Category.OBJECTS] @@ -1962,7 +1962,7 @@ final Map<String, String> objects = Map.fromIterables([ 'Coffin', 'Funeral Urn', 'Moai', - 'Potable Water' + 'Potable Water', ], [ '💌', '🕳', @@ -2161,7 +2161,7 @@ final Map<String, String> objects = Map.fromIterables([ '⚰', '⚱', '🗿', - '🚰' + '🚰', ]); /// Map of all possible emojis along with their names in [Category.SYMBOLS] @@ -2424,7 +2424,7 @@ final Map<String, String> symbols = Map.fromIterables([ 'Red Triangle Pointed Down', 'Diamond With a Dot', 'White Square Button', - 'Black Square Button' + 'Black Square Button', ], [ '💘', '💝', @@ -2684,7 +2684,7 @@ final Map<String, String> symbols = Map.fromIterables([ '🔻', '💠', '🔳', - '🔲' + '🔲', ]); /// Map of all possible emojis along with their names in [Category.FLAGS] @@ -2953,7 +2953,7 @@ final Map<String, String> flags = Map.fromIterables([ 'Flag: Mayotte', 'Flag: South Africa', 'Flag: Zambia', - 'Flag: Zimbabwe' + 'Flag: Zimbabwe', ], [ '🏁', '🚩', @@ -3219,5 +3219,5 @@ final Map<String, String> flags = Map.fromIterables([ '🇾🇹', '🇿🇦', '🇿🇲', - '🇿🇼' + '🇿🇼', ]); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/emoji_picker.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/emoji_picker.dart index a1e4d6007190..0dd72ad54733 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/emoji_picker.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/emoji_picker.dart @@ -226,7 +226,7 @@ class EmojiPickerState extends State<EmojiPicker> { EmojiCategoryGroup( EmojiCategory.FLAGS, await _getAvailableEmojis(emoji_list.flags, title: 'flags'), - ) + ), ]); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart index b00cac5e618a..e823fa755054 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart @@ -49,7 +49,7 @@ class SettingThirdPartyLogin extends StatelessWidget { fontSize: 16, ), const HSpace(6), - indicator + indicator, ], ), const VSpace(6), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/color_scheme.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/color_scheme.dart index 88b3792f5a65..20d0a12727c9 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/color_scheme.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/color_scheme.dart @@ -122,7 +122,7 @@ class ColorSchemeUploadPopover extends StatelessWidget { false, ), ) - .toList() + .toList(), ], ], ), @@ -168,7 +168,7 @@ class ColorSchemeUploadPopover extends StatelessWidget { width: 20, onPressed: () => bloc.add(DynamicPluginEvent.removePlugin(name: theme)), - ) + ), ], ), ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/direction_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/direction_setting.dart index 134af35e2e1b..561b6dc5553e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/direction_setting.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/direction_setting.dart @@ -34,7 +34,7 @@ class LayoutDirectionSetting extends StatelessWidget { _layoutDirectionItemButton(context, LayoutDirection.rtlLayout), ], ), - ) + ), ], ); } @@ -98,7 +98,7 @@ class TextDirectionSetting extends StatelessWidget { _textDirectionItemButton(context, AppFlowyTextDirection.auto), ], ), - ) + ), ], ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/font_family_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/font_family_setting.dart index fa098069716c..dadafe51d4f7 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/font_family_setting.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance/font_family_setting.dart @@ -45,7 +45,7 @@ class _ThemeFontFamilySettingState extends State<ThemeFontFamilySetting> { trailing: [ FontFamilyDropDown( currentFontFamily: widget.currentFontFamily, - ) + ), ], ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart index 7938c75bc8d9..84da378a2ec5 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart @@ -127,12 +127,12 @@ class ShortcutsListTile extends StatelessWidget { onPressed: () { showKeyListenerDialog(context); }, - ) + ), ], ), Divider( color: Theme.of(context).dividerColor, - ) + ), ], ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart index 976f813a5115..5200b7062362 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart @@ -2,18 +2,19 @@ import 'dart:io'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/startup/entry_point.dart'; -import 'package:flowy_infra/file_picker/file_picker_service.dart'; import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/file_picker/file_picker_service.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:url_launcher/url_launcher.dart'; + import '../../../../generated/locale_keys.g.dart'; import '../../../../startup/launch_configuration.dart'; import '../../../../startup/startup.dart'; @@ -172,7 +173,10 @@ class _ChangeStoragePathButtonState extends State<_ChangeStoragePathButton> { onPressed: () async { // pick the new directory and reload app final path = await getIt<FilePickerService>().getDirectoryPath(); - if (path == null || !mounted || widget.usingPath == path) { + if (path == null || widget.usingPath == path) { + return; + } + if (!mounted) { return; } await context.read<SettingsLocationCubit>().setCustomPath(path); @@ -245,7 +249,10 @@ class _RecoverDefaultStorageButtonState // reset to the default directory and reload app final directory = await appFlowyApplicationDataDirectory(); final path = directory.path; - if (!mounted || widget.usingPath == path) { + if (widget.usingPath == path) { + return; + } + if (!mounted) { return; } await context diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_exporter_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_exporter_widget.dart index ee4a88d1c6e9..6dd5eb259607 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_exporter_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_exporter_widget.dart @@ -2,21 +2,22 @@ import 'dart:io'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/export/document_exporter.dart'; -import 'package:appflowy/workspace/presentation/home/toast.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:flowy_infra/file_picker/file_picker_service.dart'; import 'package:appflowy/workspace/application/settings/settings_file_exporter_cubit.dart'; import 'package:appflowy/workspace/application/settings/share/export_service.dart'; +import 'package:appflowy/workspace/presentation/home/toast.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart'; import 'package:dartz/dartz.dart' as dartz; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/file_picker/file_picker_service.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:path/path.dart' as p; + import '../../../../generated/locale_keys.g.dart'; class FileExporterWidget extends StatefulWidget { @@ -69,13 +70,13 @@ class _FileExporterWidgetState extends State<FileExporterWidget> { .selectOrDeselectAllItems(); }, ), - ) + ), ], ), const VSpace(8), const Expanded(child: _ExpandedList()), const VSpace(8), - _buildButtons() + _buildButtons(), ], ), ); @@ -107,18 +108,20 @@ class _FileExporterWidgetState extends State<FileExporterWidget> { final views = cubit!.state.selectedViews; final result = await _AppFlowyFileExporter.exportToPath(exportPath, views); - if (result.$1 && mounted) { - // success - showSnackBarMessage( - context, - LocaleKeys.settings_files_exportFileSuccess.tr(), - ); - } else { - showSnackBarMessage( - context, - LocaleKeys.settings_files_exportFileFail.tr() + - result.$2.join('\n'), - ); + if (mounted) { + if (result.$1) { + // success + showSnackBarMessage( + context, + LocaleKeys.settings_files_exportFileSuccess.tr(), + ); + } else { + showSnackBarMessage( + context, + LocaleKeys.settings_files_exportFileFail.tr() + + result.$2.join('\n'), + ); + } } } else { showSnackBarMessage( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_system_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_system_view.dart index fe13da536065..4adac5da0fcc 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_system_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_system_view.dart @@ -16,7 +16,7 @@ class _SettingsFileSystemViewState extends State<SettingsFileSystemView> { late final _items = [ const SettingsFileLocationCustomizer(), // disable export data for v0.2.0 in release mode. - if (kDebugMode) const SettingsExportFileWidget() + if (kDebugMode) const SettingsExportFileWidget(), ]; @override diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_notifications_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_notifications_view.dart index 3b9a40478e72..0d74175f1a45 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_notifications_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_notifications_view.dart @@ -33,7 +33,7 @@ class SettingsNotificationsView extends StatelessWidget { .read<NotificationSettingsCubit>() .toggleNotificationsEnabled(); }, - ) + ), ], ), ], diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart index ee15bfbc318e..cf045d41b77f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart @@ -56,7 +56,7 @@ class SettingsUserView extends StatelessWidget { _buildUserIconSetting(context), if (isCloudEnabled && user.authType != AuthTypePB.Local) ...[ const VSpace(12), - UserEmailInput(user.email) + UserEmailInput(user.email), ], const VSpace(12), _renderCurrentOpenaiKey(context), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/sync_setting_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/sync_setting_view.dart index 18bac66a11a9..f362a8e515ec 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/sync_setting_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/sync_setting_view.dart @@ -87,7 +87,7 @@ class EnableEncrypt extends StatelessWidget { .add(CloudSettingEvent.enableEncrypt(value)); }, value: state.config.enableEncrypt, - ) + ), ], ), Column( @@ -128,7 +128,7 @@ class EnableEncrypt extends StatelessWidget { ), ), ], - ) + ), ], ); }, @@ -154,7 +154,7 @@ class EnableSync extends StatelessWidget { ); }, value: state.config.enableSync, - ) + ), ], ); }, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart index 4a41460942d8..fc4778fe9f68 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart @@ -88,7 +88,7 @@ class _NavigatorTextFieldDialogState extends State<NavigatorTextFieldDialog> { } Navigator.of(context).pop(); }, - ) + ), ], ), ); @@ -149,8 +149,8 @@ class _CreateFlowyAlertDialog extends State<NavigatorAlertDialog> { widget.cancel?.call(); Navigator.of(context).pop(); }, - ) - ] + ), + ], ], ), ); @@ -209,7 +209,7 @@ class NavigatorOkCancelDialog extends StatelessWidget { }, okTitle: okTitle?.toUpperCase(), cancelTitle: cancelTitle?.toUpperCase(), - ) + ), ], ), ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart index 6fc14bda6472..182cab88f172 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart @@ -235,7 +235,7 @@ class HoverButton extends StatelessWidget { children: [ if (leftIcon != null) ...[ leftIcon!, - HSpace(ActionListSizes.itemHPadding) + HSpace(ActionListSizes.itemHPadding), ], Expanded( child: FlowyText.medium( diff --git a/frontend/appflowy_flutter/macos/Runner.xcodeproj/project.pbxproj b/frontend/appflowy_flutter/macos/Runner.xcodeproj/project.pbxproj index e34603ede3cf..0357a297b3e9 100644 --- a/frontend/appflowy_flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/frontend/appflowy_flutter/macos/Runner.xcodeproj/project.pbxproj @@ -206,7 +206,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { diff --git a/frontend/appflowy_flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/frontend/appflowy_flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 656a4d8b2a81..10d57fd28e3a 100644 --- a/frontend/appflowy_flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/frontend/appflowy_flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <Scheme - LastUpgradeVersion = "1300" + LastUpgradeVersion = "1430" version = "1.3"> <BuildAction parallelizeBuildables = "YES" diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart index 2065e06beebd..af7a3766bdb1 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart +++ b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart @@ -176,10 +176,14 @@ class PopoverState extends State<Popover> { _rootEntry.addEntry(context, this, newEntry, widget.asBarrier); } - void close() { + void close({ + bool notify = true, + }) { if (_rootEntry.contains(this)) { _rootEntry.removeEntry(this); - widget.onClose?.call(); + if (notify) { + widget.onClose?.call(); + } } } @@ -193,7 +197,7 @@ class PopoverState extends State<Popover> { @override void deactivate() { - close(); + close(notify: false); super.deactivate(); } diff --git a/frontend/appflowy_flutter/packages/flowy_infra/pubspec.yaml b/frontend/appflowy_flutter/packages/flowy_infra/pubspec.yaml index 59de25ceb1c5..c812fcde8312 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/flowy_infra/pubspec.yaml @@ -19,7 +19,7 @@ dependencies: uuid: ">=2.2.2" bloc: ^8.1.2 freezed_annotation: ^2.1.0 - file_picker: ^5.3.1 + file_picker: ^6.1.1 file: ^6.1.4 dev_dependencies: diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 3ae7a7b31887..05d56cb48732 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -54,11 +54,11 @@ packages: dependency: "direct main" description: path: "." - ref: "009115d" - resolved-ref: "009115da836616e9fb2d0abd327753809a78b983" + ref: "4f073f3" + resolved-ref: "4f073f3381a05a2379144f282c6f65462c4ce9c6" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git - version: "1.5.2" + version: "2.0.0-beta.1" appflowy_popover: dependency: "direct main" description: @@ -262,10 +262,10 @@ packages: dependency: "direct main" description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.17.2" connectivity_plus: dependency: "direct main" description: @@ -402,15 +402,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.2" - emoji_mart: - dependency: "direct main" - description: - path: "." - ref: "067f718" - resolved-ref: "067f7188965c8fcb7be02ce174ce2b6757f288ee" - url: "https://github.com/LucasXu0/emoji_mart.git" - source: git - version: "0.0.1" envied: dependency: "direct main" description: @@ -471,10 +462,10 @@ packages: dependency: transitive description: name: file_picker - sha256: "9d6e95ec73abbd31ec54d0e0df8a961017e165aba1395e462e5b31ea0c165daf" + sha256: "4e42aacde3b993c5947467ab640882c56947d9d27342a5b6f2895b23956954a6" url: "https://pub.dev" source: hosted - version: "5.3.1" + version: "6.1.1" file_selector_linux: dependency: transitive description: @@ -584,6 +575,15 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_emoji_mart: + dependency: "direct main" + description: + path: "." + ref: "140b530" + resolved-ref: "140b53091ce7ad971e97c1d5a53fe0875596326d" + url: "https://github.com/LucasXu0/emoji_mart.git" + source: git + version: "1.0.2" flutter_lints: dependency: "direct dev" description: @@ -894,10 +894,10 @@ packages: dependency: "direct main" description: name: intl - sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" url: "https://pub.dev" source: hosted - version: "0.18.0" + version: "0.18.1" intl_utils: dependency: transitive description: @@ -1038,18 +1038,18 @@ packages: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.5.0" meta: dependency: transitive description: @@ -1214,10 +1214,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: d3f80b32e83ec208ac95253e0cd4d298e104fbc63cb29c5c69edaed43b0c69d6 + sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96" url: "https://pub.dev" source: hosted - version: "2.1.6" + version: "2.1.7" percent_indicator: dependency: "direct main" description: @@ -1406,10 +1406,10 @@ packages: dependency: transitive description: name: rich_clipboard_windows - sha256: fa2a28e75ce4bcc9efc6d5d0e9788b76716cdaf3b7063c141fe8af12a315f414 + sha256: "633198bcd74642bb03c4a628c7e350ee18bb391cd8c6132152f7c97ab250e901" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.3" run_with_network_images: dependency: "direct dev" description: @@ -1611,10 +1611,10 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" sqflite: dependency: transitive description: @@ -1756,26 +1756,26 @@ packages: dependency: transitive description: name: test - sha256: "3dac9aecf2c3991d09b9cdde4f98ded7b30804a88a0d7e4e7e1678e78d6b97f4" + sha256: "13b41f318e2a5751c3169137103b60c584297353d4b1761b66029bae6411fe46" url: "https://pub.dev" source: hosted - version: "1.24.1" + version: "1.24.3" test_api: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.6.0" test_core: dependency: transitive description: name: test_core - sha256: "5138dbffb77b2289ecb12b81c11ba46036590b72a64a7a90d6ffb880f1a29e93" + sha256: "99806e9e6d95c7b059b7a0fc08f07fc53fabe54a829497f0d9676299f1e8637e" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.5.3" textfield_tags: dependency: "direct main" description: @@ -1989,10 +1989,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f6deed8ed625c52864792459709183da231ebf66ff0cf09e69b573227c377efe + sha256: c620a6f783fa22436da68e42db7ebbf18b8c44b9a46ab911f666ff09ffd9153f url: "https://pub.dev" source: hosted - version: "11.3.0" + version: "11.7.1" watcher: dependency: transitive description: @@ -2001,6 +2001,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + url: "https://pub.dev" + source: hosted + version: "0.1.4-beta" web_socket_channel: dependency: transitive description: @@ -2061,18 +2069,18 @@ packages: dependency: transitive description: name: win32 - sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c" + sha256: f2add6fa510d3ae152903412227bda57d0d5a8da61d2c39c1fb022c9429a41c0 url: "https://pub.dev" source: hosted - version: "4.1.4" + version: "5.0.6" win32_registry: dependency: transitive description: name: win32_registry - sha256: "1c52f994bdccb77103a6231ad4ea331a244dbcef5d1f37d8462f713143b0bfae" + sha256: e4506d60b7244251bc59df15656a3093501c37fb5af02105a944d73eb95be4c9 url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" window_manager: dependency: "direct main" description: @@ -2114,5 +2122,5 @@ packages: source: hosted version: "1.1.1" sdks: - dart: ">=3.0.0 <4.0.0" - flutter: ">=3.10.1" + dart: ">=3.1.5 <4.0.0" + flutter: ">=3.13.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 3c86e546f3b6..b15ccd5c0444 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -18,7 +18,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev version: 0.3.8 environment: - sdk: ">=3.0.0 <4.0.0" + sdk: ">=3.1.5 <4.0.0" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -47,7 +47,7 @@ dependencies: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: 009115d + ref: 4f073f3 appflowy_popover: path: packages/appflowy_popover @@ -111,10 +111,10 @@ dependencies: go_router: ^10.1.2 string_validator: ^1.0.0 unsplash_client: ^2.1.1 - emoji_mart: + flutter_emoji_mart: git: url: https://github.com/LucasXu0/emoji_mart.git - ref: "067f718" + ref: "140b530" # Notifications # TODO: Consider implementing custom package diff --git a/frontend/appflowy_flutter/test/bloc_test/shortcuts_test/shortcuts_cubit_test.dart b/frontend/appflowy_flutter/test/bloc_test/shortcuts_test/shortcuts_cubit_test.dart index 3d88d5fbe503..1b896cbd3dd3 100644 --- a/frontend/appflowy_flutter/test/bloc_test/shortcuts_test/shortcuts_cubit_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/shortcuts_test/shortcuts_cubit_test.dart @@ -58,7 +58,7 @@ void main() { expect: () => <dynamic>[ const ShortcutsState(status: ShortcutsStatus.updating), isA<ShortcutsState>() - .having((w) => w.status, 'status', ShortcutsStatus.failure) + .having((w) => w.status, 'status', ShortcutsStatus.failure), ], ); @@ -101,7 +101,7 @@ void main() { expect: () => <dynamic>[ const ShortcutsState(status: ShortcutsStatus.updating), isA<ShortcutsState>() - .having((w) => w.status, 'status', ShortcutsStatus.failure) + .having((w) => w.status, 'status', ShortcutsStatus.failure), ], ); @@ -112,7 +112,7 @@ void main() { expect: () => <dynamic>[ const ShortcutsState(status: ShortcutsStatus.updating), isA<ShortcutsState>() - .having((w) => w.status, 'status', ShortcutsStatus.success) + .having((w) => w.status, 'status', ShortcutsStatus.success), ], ); }); @@ -140,7 +140,7 @@ void main() { expect: () => <dynamic>[ const ShortcutsState(status: ShortcutsStatus.updating), isA<ShortcutsState>() - .having((w) => w.status, 'status', ShortcutsStatus.failure) + .having((w) => w.status, 'status', ShortcutsStatus.failure), ], ); diff --git a/frontend/appflowy_flutter/test/unit_test/editor/transaction_adapter_test.dart b/frontend/appflowy_flutter/test/unit_test/editor/transaction_adapter_test.dart index 3649a3b6139c..7f25a55353c9 100644 --- a/frontend/appflowy_flutter/test/unit_test/editor/transaction_adapter_test.dart +++ b/frontend/appflowy_flutter/test/unit_test/editor/transaction_adapter_test.dart @@ -108,7 +108,7 @@ void main() { root: Node( type: 'page', children: [ - paragraphNode(children: [paragraphNode(text: '1')]) + paragraphNode(children: [paragraphNode(text: '1')]), ], ), ); diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index b3d42fb21c83..7770891ec326 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -467,7 +467,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" dependencies = [ "borsh-derive", - "hashbrown 0.12.3", + "hashbrown 0.13.2", ] [[package]] @@ -1150,7 +1150,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.8.0", + "phf 0.11.2", "smallvec", ] @@ -2522,6 +2522,15 @@ dependencies = [ "ahash 0.7.6", ] +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash 0.8.3", +] + [[package]] name = "hashbrown" version = "0.14.0" @@ -3648,7 +3657,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros", + "phf_macros 0.8.0", "phf_shared 0.8.0", "proc-macro-hack", ] @@ -3668,6 +3677,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ + "phf_macros 0.11.2", "phf_shared 0.11.2", ] @@ -3735,6 +3745,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn 2.0.31", +] + [[package]] name = "phf_shared" version = "0.8.0" @@ -3938,7 +3961,7 @@ checksum = "8bdf592881d821b83d471f8af290226c8d51402259e9bb5be7f9f8bdebbb11ac" dependencies = [ "bytes", "heck 0.4.1", - "itertools 0.10.5", + "itertools 0.11.0", "log", "multimap", "once_cell", @@ -3959,7 +3982,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "265baba7fabd416cf5078179f7d2cbeca4ce7a9041111900675ea7c4cb8a4c32" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.11.0", "proc-macro2", "quote", "syn 2.0.31", diff --git a/frontend/scripts/code_generation/freezed/generate_freezed.sh b/frontend/scripts/code_generation/freezed/generate_freezed.sh index 24e4e55fa3bc..1cf5f0fe497e 100755 --- a/frontend/scripts/code_generation/freezed/generate_freezed.sh +++ b/frontend/scripts/code_generation/freezed/generate_freezed.sh @@ -4,7 +4,7 @@ no_pub_get=false while getopts 's' flag; do case "${flag}" in - s) no_pub_get=true ;; + s) no_pub_get=true ;; esac done @@ -23,7 +23,7 @@ if [ "$no_pub_get" = false ]; then flutter packages pub get >/dev/null 2>&1 fi -dart run build_runner clean && dart run build_runner build -d +dart run build_runner build -d echo "Done generating files for appflowy_flutter" echo "Generating files for packages" @@ -39,7 +39,7 @@ for d in */; do if [ "$no_pub_get" = false ]; then flutter packages pub get >/dev/null 2>&1 fi - dart run build_runner clean && dart run build_runner build -d + dart run build_runner build -d echo "Done running build command in $d" else echo "No pubspec.yaml found in $d, it can\'t be a Dart project. Skipping." diff --git a/frontend/scripts/docker-buildfiles/Dockerfile b/frontend/scripts/docker-buildfiles/Dockerfile index eecf5fd48596..15a4d2bd58b7 100644 --- a/frontend/scripts/docker-buildfiles/Dockerfile +++ b/frontend/scripts/docker-buildfiles/Dockerfile @@ -39,7 +39,7 @@ RUN source ~/.cargo/env && \ RUN sudo pacman -S --noconfirm git tar gtk3 RUN curl -sSfL \ --output flutter.tar.xz \ - https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.10.1-stable.tar.xz && \ + https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.13.9-stable.tar.xz && \ tar -xf flutter.tar.xz && \ rm flutter.tar.xz RUN flutter config --enable-linux-desktop diff --git a/frontend/scripts/install_dev_env/install_ios.sh b/frontend/scripts/install_dev_env/install_ios.sh index bffa905a76e3..5fc820d5b91a 100644 --- a/frontend/scripts/install_dev_env/install_ios.sh +++ b/frontend/scripts/install_dev_env/install_ios.sh @@ -44,9 +44,9 @@ printMessage "Setting up Flutter" # Get the current Flutter version FLUTTER_VERSION=$(flutter --version | grep -oE 'Flutter [^ ]+' | grep -oE '[^ ]+$') -# Check if the current version is 3.10.1 -if [ "$FLUTTER_VERSION" = "3.10.1" ]; then - echo "Flutter version is already 3.10.1" +# Check if the current version is 3.13.9 +if [ "$FLUTTER_VERSION" = "3.13.9" ]; then + echo "Flutter version is already 3.13.9" else # Get the path to the Flutter SDK FLUTTER_PATH=$(which flutter) @@ -55,12 +55,12 @@ else current_dir=$(pwd) cd $FLUTTER_PATH - # Use git to checkout version 3.10.1 of Flutter - git checkout 3.10.1 + # Use git to checkout version 3.13.9 of Flutter + git checkout 3.13.9 # Get back to current working directory cd "$current_dir" - echo "Switched to Flutter version 3.10.1" + echo "Switched to Flutter version 3.13.9" fi # Enable linux desktop diff --git a/frontend/scripts/install_dev_env/install_linux.sh b/frontend/scripts/install_dev_env/install_linux.sh index ae874e6bb8fc..b7de2dbd64f1 100755 --- a/frontend/scripts/install_dev_env/install_linux.sh +++ b/frontend/scripts/install_dev_env/install_linux.sh @@ -38,9 +38,9 @@ fi printMessage "Setting up Flutter" # Get the current Flutter version FLUTTER_VERSION=$(flutter --version | grep -oP 'Flutter \K\S+') -# Check if the current version is 3.10.1 -if [ "$FLUTTER_VERSION" = "3.10.1" ]; then - echo "Flutter version is already 3.10.1" +# Check if the current version is 3.13.9 +if [ "$FLUTTER_VERSION" = "3.13.9" ]; then + echo "Flutter version is already 3.13.9" else # Get the path to the Flutter SDK FLUTTER_PATH=$(which flutter) @@ -49,12 +49,12 @@ else current_dir=$(pwd) cd $FLUTTER_PATH - # Use git to checkout version 3.10.1 of Flutter - git checkout 3.10.1 + # Use git to checkout version 3.13.9 of Flutter + git checkout 3.13.9 # Get back to current working directory cd "$current_dir" - echo "Switched to Flutter version 3.10.1" + echo "Switched to Flutter version 3.13.9" fi # Enable linux desktop diff --git a/frontend/scripts/install_dev_env/install_macos.sh b/frontend/scripts/install_dev_env/install_macos.sh index c9cf2c6ccec2..32cb6c1af47b 100755 --- a/frontend/scripts/install_dev_env/install_macos.sh +++ b/frontend/scripts/install_dev_env/install_macos.sh @@ -41,9 +41,9 @@ printMessage "Setting up Flutter" # Get the current Flutter version FLUTTER_VERSION=$(flutter --version | grep -oE 'Flutter [^ ]+' | grep -oE '[^ ]+$') -# Check if the current version is 3.10.1 -if [ "$FLUTTER_VERSION" = "3.10.1" ]; then - echo "Flutter version is already 3.10.1" +# Check if the current version is 3.13.9 +if [ "$FLUTTER_VERSION" = "3.13.9" ]; then + echo "Flutter version is already 3.13.9" else # Get the path to the Flutter SDK FLUTTER_PATH=$(which flutter) @@ -52,12 +52,12 @@ else current_dir=$(pwd) cd $FLUTTER_PATH - # Use git to checkout version 3.10.1 of Flutter - git checkout 3.10.1 + # Use git to checkout version 3.13.9 of Flutter + git checkout 3.13.9 # Get back to current working directory cd "$current_dir" - echo "Switched to Flutter version 3.10.1" + echo "Switched to Flutter version 3.13.9" fi # Enable linux desktop diff --git a/frontend/scripts/install_dev_env/install_windows.sh b/frontend/scripts/install_dev_env/install_windows.sh index cda76f9af04d..b04eeb7a78aa 100644 --- a/frontend/scripts/install_dev_env/install_windows.sh +++ b/frontend/scripts/install_dev_env/install_windows.sh @@ -48,9 +48,9 @@ fi printMessage "Setting up Flutter" # Get the current Flutter version FLUTTER_VERSION=$(flutter --version | grep -oP 'Flutter \K\S+') -# Check if the current version is 3.10.1 -if [ "$FLUTTER_VERSION" = "3.10.1" ]; then - echo "Flutter version is already 3.10.1" +# Check if the current version is 3.13.9 +if [ "$FLUTTER_VERSION" = "3.13.9" ]; then + echo "Flutter version is already 3.13.9" else # Get the path to the Flutter SDK FLUTTER_PATH=$(which flutter) @@ -59,12 +59,12 @@ else current_dir=$(pwd) cd $FLUTTER_PATH - # Use git to checkout version 3.10.1 of Flutter - git checkout 3.10.1 + # Use git to checkout version 3.13.9 of Flutter + git checkout 3.13.9 # Get back to current working directory cd "$current_dir" - echo "Switched to Flutter version 3.10.1" + echo "Switched to Flutter version 3.13.9" fi # Add pub cache and cargo to PATH