From 2af93a9bcb141375b33f4dabe4331b143487d1ec Mon Sep 17 00:00:00 2001 From: Zack Date: Mon, 4 Mar 2024 16:05:16 +0800 Subject: [PATCH 1/5] feat: add support for renaming and updating the icon for workspaces (#4806) --- frontend/appflowy_tauri/src-tauri/Cargo.lock | 2 +- frontend/appflowy_web/wasm-libs/Cargo.lock | 18 ++++- frontend/rust-lib/Cargo.lock | 18 ++++- .../event-integration/src/user_event.rs | 73 ++++++++++++++----- .../user/af_cloud_test/workspace_test.rs | 66 ++++++++++++++--- .../af_cloud/impls/user/cloud_service_impl.rs | 29 +++++++- .../src/local_server/impls/user.rs | 18 ++++- .../flowy-server/src/supabase/api/user.rs | 14 ++++ frontend/rust-lib/flowy-user-pub/src/cloud.rs | 8 ++ .../flowy-user/src/entities/user_profile.rs | 4 + .../flowy-user/src/entities/workspace.rs | 21 ++++++ .../rust-lib/flowy-user/src/event_handler.rs | 26 +++++++ frontend/rust-lib/flowy-user/src/event_map.rs | 8 ++ .../user_manager/manager_user_workspace.rs | 15 ++++ 14 files changed, 283 insertions(+), 37 deletions(-) diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 4c9a8a0f7a..7e91aa05a4 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -1201,7 +1201,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa 1.0.6", - "phf 0.8.0", + "phf 0.11.2", "smallvec", ] diff --git a/frontend/appflowy_web/wasm-libs/Cargo.lock b/frontend/appflowy_web/wasm-libs/Cargo.lock index 16cec98380..2274c4fb42 100644 --- a/frontend/appflowy_web/wasm-libs/Cargo.lock +++ b/frontend/appflowy_web/wasm-libs/Cargo.lock @@ -900,7 +900,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.8.0", + "phf 0.11.2", "smallvec", ] @@ -2779,7 +2779,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", ] @@ -2799,6 +2799,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", ] @@ -2866,6 +2867,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.48", +] + [[package]] name = "phf_shared" version = "0.8.0" diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 42d997baba..595a605df0 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -1103,7 +1103,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.8.0", + "phf 0.11.2", "smallvec", ] @@ -3641,7 +3641,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", ] @@ -3661,6 +3661,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", ] @@ -3728,6 +3729,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.47", +] + [[package]] name = "phf_shared" version = "0.8.0" diff --git a/frontend/rust-lib/event-integration/src/user_event.rs b/frontend/rust-lib/event-integration/src/user_event.rs index 768a6e1a5c..b348b89d64 100644 --- a/frontend/rust-lib/event-integration/src/user_event.rs +++ b/frontend/rust-lib/event-integration/src/user_event.rs @@ -4,12 +4,14 @@ use std::sync::Arc; use bytes::Bytes; +use flowy_folder::entities::{RepeatedViewPB, WorkspacePB}; use nanoid::nanoid; use protobuf::ProtobufError; use tokio::sync::broadcast::{channel, Sender}; use tracing::error; use uuid::Uuid; +use flowy_folder::event_map::FolderEvent; use flowy_notification::entities::SubscribeObject; use flowy_notification::NotificationSender; use flowy_server::af_cloud::define::{USER_DEVICE_ID, USER_EMAIL, USER_SIGN_IN_URL, USER_UUID}; @@ -17,12 +19,12 @@ use flowy_server_pub::af_cloud_config::AFCloudConfiguration; use flowy_server_pub::AuthenticatorType; use flowy_user::entities::{ AuthenticatorPB, CloudSettingPB, CreateWorkspacePB, ImportAppFlowyDataPB, OauthSignInPB, - RepeatedUserWorkspacePB, SignInUrlPB, SignInUrlPayloadPB, SignUpPayloadPB, UpdateCloudConfigPB, - UpdateUserProfilePayloadPB, UserProfilePB, UserWorkspaceIdPB, UserWorkspacePB, + RenameWorkspacePB, RepeatedUserWorkspacePB, SignInUrlPB, SignInUrlPayloadPB, SignUpPayloadPB, + UpdateCloudConfigPB, UpdateUserProfilePayloadPB, UserProfilePB, UserWorkspaceIdPB, + UserWorkspacePB, }; use flowy_user::errors::{FlowyError, FlowyResult}; use flowy_user::event_map::UserEvent; -use flowy_user::event_map::UserEvent::*; use lib_dispatch::prelude::{af_spawn, AFPluginDispatcher, AFPluginRequest, ToBytes}; use crate::event_builder::EventBuilder; @@ -31,7 +33,7 @@ use crate::EventIntegrationTest; impl EventIntegrationTest { pub async fn enable_encryption(&self) -> String { let config = EventBuilder::new(self.clone()) - .event(GetCloudConfig) + .event(UserEvent::GetCloudConfig) .async_send() .await .parse::(); @@ -40,7 +42,7 @@ impl EventIntegrationTest { enable_encrypt: Some(true), }; let error = EventBuilder::new(self.clone()) - .event(SetCloudConfig) + .event(UserEvent::SetCloudConfig) .payload(update) .async_send() .await @@ -68,7 +70,7 @@ impl EventIntegrationTest { .into_bytes() .unwrap(); - let request = AFPluginRequest::new(SignUp).payload(payload); + let request = AFPluginRequest::new(UserEvent::SignUp).payload(payload); let user_profile = AFPluginDispatcher::async_send(&self.appflowy_core.dispatcher(), request) .await .parse::() @@ -95,7 +97,7 @@ impl EventIntegrationTest { }; EventBuilder::new(self.clone()) - .event(OauthSignIn) + .event(UserEvent::OauthSignIn) .payload(payload) .async_send() .await @@ -104,7 +106,7 @@ impl EventIntegrationTest { pub async fn sign_out(&self) { EventBuilder::new(self.clone()) - .event(SignOut) + .event(UserEvent::SignOut) .async_send() .await; } @@ -119,7 +121,7 @@ impl EventIntegrationTest { pub async fn get_user_profile(&self) -> Result { EventBuilder::new(self.clone()) - .event(GetUserProfile) + .event(UserEvent::GetUserProfile) .async_send() .await .try_parse::() @@ -127,7 +129,7 @@ impl EventIntegrationTest { pub async fn update_user_profile(&self, params: UpdateUserProfilePayloadPB) { EventBuilder::new(self.clone()) - .event(UpdateUserProfile) + .event(UserEvent::UpdateUserProfile) .payload(params) .async_send() .await; @@ -139,7 +141,7 @@ impl EventIntegrationTest { authenticator: AuthenticatorPB::AppFlowyCloud, }; let sign_in_url = EventBuilder::new(self.clone()) - .event(GenerateSignInURL) + .event(UserEvent::GenerateSignInURL) .payload(payload) .async_send() .await @@ -155,7 +157,7 @@ impl EventIntegrationTest { }; let user_profile = EventBuilder::new(self.clone()) - .event(OauthSignIn) + .event(UserEvent::OauthSignIn) .payload(payload) .async_send() .await @@ -182,7 +184,7 @@ impl EventIntegrationTest { }; let user_profile = EventBuilder::new(self.clone()) - .event(OauthSignIn) + .event(UserEvent::OauthSignIn) .payload(payload) .async_send() .await @@ -217,16 +219,53 @@ impl EventIntegrationTest { name: name.to_string(), }; EventBuilder::new(self.clone()) - .event(CreateWorkspace) + .event(UserEvent::CreateWorkspace) .payload(payload) .async_send() .await .parse::() } + pub async fn rename_workspace( + &self, + workspace_id: &str, + new_name: &str, + ) -> Result<(), FlowyError> { + let payload = RenameWorkspacePB { + workspace_id: workspace_id.to_owned(), + new_name: new_name.to_owned(), + }; + match EventBuilder::new(self.clone()) + .event(UserEvent::RenameWorkspace) + .payload(payload) + .async_send() + .await + .error() + { + Some(err) => Err(err), + None => Ok(()), + } + } + + pub async fn folder_read_current_workspace(&self) -> WorkspacePB { + EventBuilder::new(self.clone()) + .event(FolderEvent::ReadCurrentWorkspace) + .async_send() + .await + .parse() + } + + pub async fn folder_read_workspace_views(&self) -> RepeatedViewPB { + EventBuilder::new(self.clone()) + .event(FolderEvent::ReadWorkspaceViews) + .async_send() + .await + .parse() + } + pub async fn get_all_workspaces(&self) -> RepeatedUserWorkspacePB { EventBuilder::new(self.clone()) - .event(GetAllWorkspace) + .event(UserEvent::GetAllWorkspace) .async_send() .await .parse::() @@ -237,7 +276,7 @@ impl EventIntegrationTest { workspace_id: workspace_id.to_string(), }; EventBuilder::new(self.clone()) - .event(DeleteWorkspace) + .event(UserEvent::DeleteWorkspace) .payload(payload) .async_send() .await; @@ -248,7 +287,7 @@ impl EventIntegrationTest { workspace_id: workspace_id.to_string(), }; EventBuilder::new(self.clone()) - .event(OpenWorkspace) + .event(UserEvent::OpenWorkspace) .payload(payload) .async_send() .await; diff --git a/frontend/rust-lib/event-integration/tests/user/af_cloud_test/workspace_test.rs b/frontend/rust-lib/event-integration/tests/user/af_cloud_test/workspace_test.rs index 18b367e619..802349c512 100644 --- a/frontend/rust-lib/event-integration/tests/user/af_cloud_test/workspace_test.rs +++ b/frontend/rust-lib/event-integration/tests/user/af_cloud_test/workspace_test.rs @@ -2,11 +2,26 @@ use std::time::Duration; use event_integration::user_event::user_localhost_af_cloud; use event_integration::EventIntegrationTest; -use flowy_user::entities::RepeatedUserWorkspacePB; +use flowy_user::entities::{RepeatedUserWorkspacePB, UserWorkspacePB}; use flowy_user::protobuf::UserNotification; use crate::util::receive_with_timeout; +#[tokio::test] +async fn af_cloud_workspace_name_change() { + user_localhost_af_cloud().await; + let test = EventIntegrationTest::new().await; + let user_profile_pb = test.af_cloud_sign_up().await; + let workspaces = test.get_all_workspaces().await; + let workspace_id = workspaces.items[0].workspace_id.as_str(); + test + .rename_workspace(workspace_id, "new_workspace_name") + .await + .expect("failed to rename workspace"); + let workspaces = get_synced_workspaces(&test, user_profile_pb.id).await; + assert_eq!(workspaces[0].name, "new_workspace_name".to_string()); +} + #[tokio::test] async fn af_cloud_create_workspace_test() { user_localhost_af_cloud().await; @@ -14,22 +29,34 @@ async fn af_cloud_create_workspace_test() { let user_profile_pb = test.af_cloud_sign_up().await; let workspaces = test.get_all_workspaces().await.items; + let first_workspace_id = workspaces[0].workspace_id.as_str(); assert_eq!(workspaces.len(), 1); - test.create_workspace("my second workspace").await; - let _workspaces = test.get_all_workspaces().await.items; - - let a = user_profile_pb.id.to_string(); - let rx = test - .notification_sender - .subscribe::(&a, UserNotification::DidUpdateUserWorkspaces as i32); - let workspaces = receive_with_timeout(rx, Duration::from_secs(30)) - .await - .unwrap() - .items; + let created_workspace = test.create_workspace("my second workspace").await; + assert_eq!(created_workspace.name, "my second workspace"); + let workspaces = get_synced_workspaces(&test, user_profile_pb.id).await; assert_eq!(workspaces.len(), 2); assert_eq!(workspaces[1].name, "my second workspace".to_string()); + + { + // before opening new workspace + let folder_ws = test.folder_read_current_workspace().await; + assert_eq!(&folder_ws.id, first_workspace_id); + let views = test.folder_read_workspace_views().await; + assert_eq!(views.items[0].parent_view_id.as_str(), first_workspace_id); + } + { + // after opening new workspace + test.open_workspace(&created_workspace.workspace_id).await; + let folder_ws = test.folder_read_current_workspace().await; + assert_eq!(folder_ws.id, created_workspace.workspace_id); + let views = test.folder_read_workspace_views().await; + assert_eq!( + views.items[0].parent_view_id.as_str(), + created_workspace.workspace_id + ); + } } #[tokio::test] @@ -50,3 +77,18 @@ async fn af_cloud_open_workspace_test() { assert_eq!(views[1].name, "my first document".to_string()); assert_eq!(views[2].name, "my second document".to_string()); } + +async fn get_synced_workspaces(test: &EventIntegrationTest, user_id: i64) -> Vec { + let _workspaces = test.get_all_workspaces().await.items; + let sub_id = user_id.to_string(); + let rx = test + .notification_sender + .subscribe::( + &sub_id, + UserNotification::DidUpdateUserWorkspaces as i32, + ); + receive_with_timeout(rx, Duration::from_secs(30)) + .await + .unwrap() + .items +} 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 73717798f0..625790d578 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 @@ -3,7 +3,7 @@ use std::sync::Arc; use anyhow::{anyhow, Error}; use client_api::entity::workspace_dto::{ - CreateWorkspaceMember, CreateWorkspaceParam, WorkspaceMemberChangeset, + CreateWorkspaceMember, CreateWorkspaceParam, PatchWorkspaceParam, WorkspaceMemberChangeset, }; use client_api::entity::{AFRole, AFWorkspace, AuthProvider, CollabParams, CreateCollabParams}; use client_api::{Client, ClientConfiguration}; @@ -16,6 +16,7 @@ use flowy_user_pub::cloud::{UserCloudService, UserCollabParams, UserUpdate, User use flowy_user_pub::entities::*; use lib_infra::box_any::BoxAny; use lib_infra::future::FutureResult; +use uuid::Uuid; use crate::af_cloud::define::USER_SIGN_IN_URL; use crate::af_cloud::impls::user::dto::{ @@ -320,6 +321,32 @@ where Ok(()) }) } + + fn patch_workspace( + &self, + workspace_id: &str, + new_workspace_name: Option<&str>, + new_workspace_icon: Option<&str>, + ) -> FutureResult<(), FlowyError> { + let try_get_client = self.server.try_get_client(); + let owned_workspace_id = workspace_id.to_owned(); + let owned_workspace_name = new_workspace_name.map(|s| s.to_owned()); + let owned_workspace_icon = new_workspace_icon.map(|s| s.to_owned()); + FutureResult::new(async move { + let workspace_id: Uuid = owned_workspace_id + .parse() + .map_err(|_| ErrorCode::InvalidParams)?; + let client = try_get_client?; + client + .patch_workspace(PatchWorkspaceParam { + workspace_id, + workspace_name: owned_workspace_name, + workspace_icon: owned_workspace_icon, + }) + .await?; + Ok(()) + }) + } } async fn get_admin_client(client: &Arc) -> FlowyResult { 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 feb63ddc38..367d1bd732 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 @@ -179,7 +179,7 @@ impl UserCloudService for LocalServerUserAuthServiceImpl { FutureResult::new(async { Err( FlowyError::local_version_not_support() - .with_context("local server doesn't support mulitple workspaces"), + .with_context("local server doesn't support multiple workspaces"), ) }) } @@ -188,7 +188,21 @@ impl UserCloudService for LocalServerUserAuthServiceImpl { FutureResult::new(async { Err( FlowyError::local_version_not_support() - .with_context("local server doesn't support mulitple workspaces"), + .with_context("local server doesn't support multiple workspaces"), + ) + }) + } + + fn patch_workspace( + &self, + _workspace_id: &str, + _new_workspace_name: Option<&str>, + _new_workspace_icon: Option<&str>, + ) -> FutureResult<(), FlowyError> { + FutureResult::new(async { + Err( + FlowyError::local_version_not_support() + .with_context("local server doesn't support multiple workspaces"), ) }) } 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 1307787c73..382388558b 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs @@ -372,6 +372,20 @@ where ) }) } + + fn patch_workspace( + &self, + _workspace_id: &str, + _new_workspace_name: Option<&str>, + _new_workspace_icon: Option<&str>, + ) -> FutureResult<(), FlowyError> { + FutureResult::new(async { + Err( + FlowyError::local_version_not_support() + .with_context("supabase server doesn't support mulitple workspaces"), + ) + }) + } } pub struct CreateCollabAction { diff --git a/frontend/rust-lib/flowy-user-pub/src/cloud.rs b/frontend/rust-lib/flowy-user-pub/src/cloud.rs index 27cc2233f0..fe27ec5898 100644 --- a/frontend/rust-lib/flowy-user-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-pub/src/cloud.rs @@ -171,6 +171,14 @@ pub trait UserCloudService: Send + Sync + 'static { /// Returns the new workspace if successful fn create_workspace(&self, workspace_name: &str) -> FutureResult; + // Updates the workspace name and icon + fn patch_workspace( + &self, + workspace_id: &str, + new_workspace_name: Option<&str>, + new_workspace_icon: Option<&str>, + ) -> FutureResult<(), FlowyError>; + /// Deletes a workspace owned by the user. fn delete_workspace(&self, workspace_id: &str) -> FutureResult<(), FlowyError>; 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 030c5b1179..2ccbe6143b 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs @@ -225,6 +225,9 @@ pub struct UserWorkspacePB { #[pb(index = 2)] pub name: String, + + #[pb(index = 3)] + pub created_at_timestamp: i64, } impl From for UserWorkspacePB { @@ -232,6 +235,7 @@ impl From for UserWorkspacePB { Self { workspace_id: value.id, name: value.name, + created_at_timestamp: value.created_at.timestamp(), } } } diff --git a/frontend/rust-lib/flowy-user/src/entities/workspace.rs b/frontend/rust-lib/flowy-user/src/entities/workspace.rs index daef940819..c98e256547 100644 --- a/frontend/rust-lib/flowy-user/src/entities/workspace.rs +++ b/frontend/rust-lib/flowy-user/src/entities/workspace.rs @@ -116,3 +116,24 @@ pub struct CreateWorkspacePB { #[validate(custom = "required_not_empty_str")] pub name: String, } + +#[derive(ProtoBuf, Default, Clone, Validate)] +pub struct RenameWorkspacePB { + #[pb(index = 1)] + #[validate(custom = "required_not_empty_str")] + pub workspace_id: String, + + #[pb(index = 2)] + #[validate(custom = "required_not_empty_str")] + pub new_name: String, +} + +#[derive(ProtoBuf, Default, Clone, Validate)] +pub struct ChangeWorkspaceIconPB { + #[pb(index = 1)] + #[validate(custom = "required_not_empty_str")] + pub workspace_id: String, + + #[pb(index = 2)] + pub new_icon: String, +} diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index 40cba9c282..bff1ef891b 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -683,3 +683,29 @@ pub async fn delete_workspace_handler( manager.delete_workspace(&workspace_id).await?; Ok(()) } + +#[tracing::instrument(level = "debug", skip_all, err)] +pub async fn rename_workspace_handler( + rename_workspace_param: AFPluginData, + manager: AFPluginState>, +) -> Result<(), FlowyError> { + let params = rename_workspace_param.try_into_inner()?; + let manager = upgrade_manager(manager)?; + manager + .patch_workspace(¶ms.workspace_id, Some(¶ms.new_name), None) + .await?; + Ok(()) +} + +#[tracing::instrument(level = "debug", skip_all, err)] +pub async fn change_workspace_icon_handler( + change_workspace_icon_param: AFPluginData, + manager: AFPluginState>, +) -> Result<(), FlowyError> { + let params = change_workspace_icon_param.try_into_inner()?; + let manager = upgrade_manager(manager)?; + manager + .patch_workspace(¶ms.workspace_id, None, Some(¶ms.new_icon)) + .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 3ac2c8a7b8..611fd9bad1 100644 --- a/frontend/rust-lib/flowy-user/src/event_map.rs +++ b/frontend/rust-lib/flowy-user/src/event_map.rs @@ -62,6 +62,8 @@ pub fn init(user_manager: Weak) -> AFPlugin { .event(UserEvent::GetAllWorkspace, get_all_workspace_handler) .event(UserEvent::CreateWorkspace, create_workspace_handler) .event(UserEvent::DeleteWorkspace, delete_workspace_handler) + .event(UserEvent::RenameWorkspace, rename_workspace_handler) + .event(UserEvent::ChangeWorkspaceIcon, change_workspace_icon_handler) } #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] @@ -200,6 +202,12 @@ pub enum UserEvent { #[event(input = "UserWorkspaceIdPB")] DeleteWorkspace = 43, + + #[event(input = "RenameWorkspacePB")] + RenameWorkspace = 44, + + #[event(input = "ChangeWorkspaceIconPB")] + ChangeWorkspaceIcon = 45, } pub trait UserStatusCallback: Send + Sync + 'static { diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs index aa75a6f912..1b3ea81533 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs @@ -167,6 +167,21 @@ impl UserManager { Ok(new_workspace) } + pub async fn patch_workspace( + &self, + workspace_id: &str, + new_workspace_name: Option<&str>, + new_workspace_icon: Option<&str>, + ) -> FlowyResult<()> { + self + .cloud_services + .get_user_service()? + .patch_workspace(workspace_id, new_workspace_name, new_workspace_icon) + .await?; + + Ok(()) + } + pub async fn delete_workspace(&self, workspace_id: &str) -> FlowyResult<()> { self .cloud_services From ba965caa8fe9c5bfc8fb0f86fc2a72130ad034df Mon Sep 17 00:00:00 2001 From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Date: Mon, 4 Mar 2024 12:41:34 +0100 Subject: [PATCH 2/5] fix: stop hover when scrolling (#4801) --- .../menu/sidebar/folder/personal_folder.dart | 8 ++- .../home/menu/sidebar/sidebar.dart | 61 +++++++++++++++++-- .../home/menu/sidebar/sidebar_folder.dart | 7 ++- .../home/menu/view/view_item.dart | 21 +++++-- 4 files changed, 83 insertions(+), 14 deletions(-) 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 4aa2d41355..c8a44d2f75 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 @@ -1,3 +1,6 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/menu/menu_bloc.dart'; @@ -8,17 +11,17 @@ import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.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/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class PersonalFolder extends StatelessWidget { const PersonalFolder({ super.key, required this.views, + this.isHoverEnabled = true, }); final List views; + final bool isHoverEnabled; @override Widget build(BuildContext context) { @@ -60,6 +63,7 @@ class PersonalFolder extends StatelessWidget { }, onTertiarySelected: (view) => context.read().openTab(view), + isHoverEnabled: isHoverEnabled, ), ), ], 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 80cee5be5e..8a284464b8 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 @@ -1,3 +1,7 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; @@ -18,7 +22,6 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' show UserProfilePB; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; /// Home Sidebar is the left side bar of the home page. @@ -28,7 +31,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; /// - settings /// - scrollable document list /// - trash -class HomeSideBar extends StatelessWidget { +class HomeSideBar extends StatefulWidget { const HomeSideBar({ super.key, required this.userProfile, @@ -39,6 +42,43 @@ class HomeSideBar extends StatelessWidget { final WorkspaceSettingPB workspaceSetting; + @override + State createState() => _HomeSideBarState(); +} + +class _HomeSideBarState extends State { + final _scrollController = ScrollController(); + Timer? _srollDebounce; + bool isScrolling = false; + + @override + void initState() { + super.initState(); + _scrollController.addListener(_onScrollChanged); + } + + void _onScrollChanged() { + setState(() => isScrolling = true); + + _srollDebounce?.cancel(); + _srollDebounce = + Timer(const Duration(milliseconds: 300), _setScrollStopped); + } + + void _setScrollStopped() { + if (mounted) { + setState(() => isScrolling = false); + } + } + + @override + void dispose() { + _srollDebounce?.cancel(); + _scrollController.removeListener(_onScrollChanged); + _scrollController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return MultiBlocProvider( @@ -48,8 +88,8 @@ class HomeSideBar extends StatelessWidget { ), BlocProvider( create: (_) => MenuBloc( - user: userProfile, - workspaceId: workspaceSetting.workspaceId, + user: widget.userProfile, + workspaceId: widget.workspaceSetting.workspaceId, )..add(const MenuEvent.initial()), ), ], @@ -108,8 +148,14 @@ class HomeSideBar extends StatelessWidget { Padding( padding: menuHorizontalInset, child: FeatureFlag.collaborativeWorkspace.isOn - ? SidebarWorkspace(userProfile: userProfile, views: views) - : SidebarUser(userProfile: userProfile, views: views), + ? SidebarWorkspace( + userProfile: widget.userProfile, + views: views, + ) + : SidebarUser( + userProfile: widget.userProfile, + views: views, + ), ), const VSpace(20), @@ -118,9 +164,12 @@ class HomeSideBar extends StatelessWidget { child: Padding( padding: menuHorizontalInset, child: SingleChildScrollView( + controller: _scrollController, + physics: const ClampingScrollPhysics(), child: SidebarFolder( views: views, favoriteViews: favoriteViews, + isHoverEnabled: !isScrolling, ), ), ), 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 01ec648e74..397a3e3d90 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 @@ -1,20 +1,23 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/favorite_folder.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; class SidebarFolder extends StatelessWidget { const SidebarFolder({ super.key, required this.views, required this.favoriteViews, + this.isHoverEnabled = true, }); final List views; final List favoriteViews; + final bool isHoverEnabled; @override Widget build(BuildContext context) { @@ -38,7 +41,7 @@ class SidebarFolder extends StatelessWidget { const VSpace(10), ], // personal - PersonalFolder(views: views), + PersonalFolder(views: views, isHoverEnabled: isHoverEnabled), ], ); }, 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 3a2afacbfe..2cdc373181 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 @@ -42,6 +42,7 @@ class ViewItem extends StatelessWidget { this.isDraggable = true, required this.isFeedback, this.height = 28.0, + this.isHoverEnabled = true, }); final ViewPB view; @@ -75,6 +76,8 @@ class ViewItem extends StatelessWidget { final double height; + final bool isHoverEnabled; + @override Widget build(BuildContext context) { return BlocProvider( @@ -101,6 +104,7 @@ class ViewItem extends StatelessWidget { isDraggable: isDraggable, isFeedback: isFeedback, height: height, + isHoverEnabled: isHoverEnabled, ); }, ), @@ -127,6 +131,7 @@ class InnerViewItem extends StatelessWidget { this.isFirstChild = false, required this.isFeedback, required this.height, + this.isHoverEnabled = true, }); final ViewPB view; @@ -148,6 +153,8 @@ class InnerViewItem extends StatelessWidget { final ViewItemOnSelected? onTertiarySelected; final double height; + final bool isHoverEnabled; + @override Widget build(BuildContext context) { Widget child = SingleInnerViewItem( @@ -264,6 +271,7 @@ class SingleInnerViewItem extends StatefulWidget { this.onTertiarySelected, required this.isFeedback, required this.height, + this.isHoverEnabled = true, }); final ViewPB view; @@ -282,6 +290,8 @@ class SingleInnerViewItem extends StatefulWidget { final FolderCategoryType categoryType; final double height; + final bool isHoverEnabled; + @override State createState() => _SingleInnerViewItemState(); } @@ -292,13 +302,16 @@ class _SingleInnerViewItemState extends State { @override Widget build(BuildContext context) { - if (widget.isFeedback) { - return _buildViewItem(false); - } - final isSelected = getIt().latestOpenView?.id == widget.view.id; + if (widget.isFeedback || !widget.isHoverEnabled) { + return _buildViewItem( + false, + !widget.isHoverEnabled ? isSelected : false, + ); + } + return FlowyHover( style: HoverStyle( hoverColor: Theme.of(context).colorScheme.secondary, From 107e3cea4fc51e922ed40e6538a8cab7891aecda Mon Sep 17 00:00:00 2001 From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Date: Mon, 4 Mar 2024 12:42:00 +0100 Subject: [PATCH 3/5] feat: duplicate calendar event (#4816) * feat: duplicate calendar event * test: add simple test --- .../database/database_calendar_test.dart | 45 +++++++++++++++++++ .../calendar/application/calendar_bloc.dart | 20 +++++++++ .../presentation/calendar_event_card.dart | 17 ++++--- .../presentation/calendar_event_editor.dart | 45 +++++++++++-------- 4 files changed, 103 insertions(+), 24 deletions(-) diff --git a/frontend/appflowy_flutter/integration_test/database/database_calendar_test.dart b/frontend/appflowy_flutter/integration_test/database/database_calendar_test.dart index f882983712..33f06557be 100644 --- a/frontend/appflowy_flutter/integration_test/database/database_calendar_test.dart +++ b/frontend/appflowy_flutter/integration_test/database/database_calendar_test.dart @@ -1,5 +1,7 @@ +import 'package:appflowy/plugins/database/calendar/presentation/calendar_event_editor.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart'; +import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -147,6 +149,49 @@ void main() { tester.assertNumberOfEventsOnSpecificDay(1, DateTime.now()); }); + testWidgets('create and duplicate calendar event', (tester) async { + const customTitle = "EventTitleCustom"; + + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + // Create the calendar view + await tester.createNewPageWithNameUnderParent( + layout: ViewLayoutPB.Calendar, + ); + + // Scroll until today's date cell is visible + await tester.scrollToToday(); + + // Hover over today's calendar cell + await tester.hoverOnTodayCalendarCell( + // Tap on create new event button + onHover: () async => tester.tapAddCalendarEventButton(), + ); + + // Make sure that the event editor popup is shown + tester.assertEventEditorOpen(); + + tester.assertNumberOfEventsInCalendar(1); + + // Change the title of the event + await tester.editEventTitle(customTitle); + + // Duplicate event + final duplicateBtnFinder = find + .descendant( + of: find.byType(CalendarEventEditor), + matching: find.byType( + FlowyIconButton, + ), + ) + .first; + await tester.tap(duplicateBtnFinder); + await tester.pumpAndSettle(); + + tester.assertNumberOfEventsInCalendar(2, title: customTitle); + }); + testWidgets('rescheduling events', (tester) async { await tester.initializeAppFlowy(); await tester.tapGoButton(); diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_bloc.dart index b8edc4229f..4a2674f1c5 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_bloc.dart @@ -64,6 +64,20 @@ class CalendarBloc extends Bloc { createEvent: (DateTime date) async { await _createEvent(date); }, + duplicateEvent: (String viewId, String rowId) async { + final result = await RowBackendService.duplicateRow(viewId, rowId); + result.fold( + (_) => null, + (e) => Log.error('Failed to duplicate event: $e', e), + ); + }, + deleteEvent: (String viewId, String rowId) async { + final result = await RowBackendService.deleteRow(viewId, rowId); + result.fold( + (_) => null, + (e) => Log.error('Failed to delete event: $e', e), + ); + }, newEventPopupDisplayed: () { emit(state.copyWith(editingEvent: null)); }, @@ -407,6 +421,12 @@ class CalendarEvent with _$CalendarEvent { const factory CalendarEvent.didReceiveDatabaseUpdate(DatabasePB database) = _ReceiveDatabaseUpdate; + + const factory CalendarEvent.duplicateEvent(String viewId, String rowId) = + _DuplicateEvent; + + const factory CalendarEvent.deleteEvent(String viewId, String rowId) = + _DeleteEvent; } @freezed diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_card.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_card.dart index 40769d2303..9542cafe98 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_card.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_card.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/row/row_cache.dart'; @@ -9,11 +11,11 @@ import 'package:appflowy_popover/appflowy_popover.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:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import '../application/calendar_bloc.dart'; + import 'calendar_event_editor.dart'; class EventCard extends StatefulWidget { @@ -144,15 +146,18 @@ class _EventCardState extends State { asBarrier: true, margin: EdgeInsets.zero, offset: const Offset(10.0, 0), - popupBuilder: (BuildContext popoverContext) { + popupBuilder: (_) { final settings = context.watch().state.settings; if (settings == null) { return const SizedBox.shrink(); } - return CalendarEventEditor( - databaseController: widget.databaseController, - rowMeta: widget.event.event.rowMeta, - layoutSettings: settings, + return BlocProvider.value( + value: context.read(), + child: CalendarEventEditor( + databaseController: widget.databaseController, + rowMeta: widget.event.event.rowMeta, + layoutSettings: settings, + ), ); }, child: Container( diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_editor.dart index 5258e32d62..b701763a4a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_editor.dart @@ -1,25 +1,25 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/field/field_controller.dart'; import 'package:appflowy/plugins/database/application/row/row_controller.dart'; -import 'package:appflowy/plugins/database/application/row/row_service.dart'; +import 'package:appflowy/plugins/database/calendar/application/calendar_bloc.dart'; import 'package:appflowy/plugins/database/calendar/application/calendar_event_editor_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart'; import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart'; -import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/row/row_detail.dart'; import 'package:appflowy/util/field_type_extension.dart'; -import 'package:appflowy_backend/log.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:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class CalendarEventEditor extends StatelessWidget { @@ -86,17 +86,28 @@ class EventEditorControls extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ + FlowyIconButton( + width: 20, + icon: const FlowySvg(FlowySvgs.m_duplicate_s), + iconColorOnHover: Theme.of(context).colorScheme.onSecondary, + onPressed: () => context.read().add( + CalendarEvent.duplicateEvent( + rowController.viewId, + rowController.rowId, + ), + ), + ), + const HSpace(8.0), FlowyIconButton( width: 20, icon: const FlowySvg(FlowySvgs.delete_s), iconColorOnHover: Theme.of(context).colorScheme.onSecondary, - onPressed: () async { - final result = await RowBackendService.deleteRow( - rowController.viewId, - rowController.rowId, - ); - result.fold((l) => null, (err) => Log.error(err)); - }, + onPressed: () => context.read().add( + CalendarEvent.deleteEvent( + rowController.viewId, + rowController.rowId, + ), + ), ), const HSpace(8.0), FlowyIconButton( @@ -107,12 +118,10 @@ class EventEditorControls extends StatelessWidget { PopoverContainer.of(context).close(); FlowyOverlay.show( context: context, - builder: (BuildContext context) { - return RowDetailPage( - databaseController: databaseController, - rowController: rowController, - ); - }, + builder: (_) => RowDetailPage( + databaseController: databaseController, + rowController: rowController, + ), ); }, ), From b087a9aeb343f85c398803d6942629615a8428a6 Mon Sep 17 00:00:00 2001 From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Date: Mon, 4 Mar 2024 13:42:42 +0100 Subject: [PATCH 4/5] fix: filter keydownevents in actions handler (#4817) --- .../inline_actions/widgets/inline_actions_handler.dart | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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 6f8babf22f..da04567fcc 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,3 +1,6 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; @@ -7,8 +10,6 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; /// All heights are in physical pixels const double _groupTextHeight = 14; // 12 height + 2 bottom spacing @@ -208,6 +209,10 @@ class _InlineActionsHandlerState extends State { results[groupIndex].results[handlerIndex]; KeyEventResult onKeyEvent(focus, KeyEvent event) { + if (event is! KeyDownEvent) { + return KeyEventResult.ignored; + } + const moveKeys = [ LogicalKeyboardKey.arrowUp, LogicalKeyboardKey.arrowDown, From 5daf9d23f5b1a5fbff1769f8add61136f9b03080 Mon Sep 17 00:00:00 2001 From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Date: Mon, 4 Mar 2024 18:24:49 +0100 Subject: [PATCH 5/5] fix: update view names in page reference menu (#4802) --- .../document/presentation/editor_page.dart | 6 +- .../base/link_to_page_widget.dart | 4 +- .../base/page_reference_commands.dart | 7 +- .../handlers/date_reference.dart | 9 ++- .../handlers/inline_page_reference.dart | 77 +++++++++++++++---- .../handlers/reminder_reference.dart | 9 ++- .../inline_actions_command.dart | 2 +- .../inline_actions_service.dart | 10 +-- .../inline_actions/service_handler.dart | 7 ++ .../widgets/inline_actions_handler.dart | 2 +- frontend/rust-lib/flowy-folder/src/manager.rs | 11 ++- 11 files changed, 108 insertions(+), 36 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/plugins/inline_actions/service_handler.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 22dabd6517..e21e2fd313 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -81,9 +81,9 @@ class _AppFlowyEditorPageState extends State { InlinePageReferenceService( currentViewId: documentBloc.view.id, limitResults: 5, - ).inlinePageReferenceDelegate, - DateReferenceService(context).dateReferenceDelegate, - ReminderReferenceService(context).reminderReferenceDelegate, + ), + DateReferenceService(context), + ReminderReferenceService(context), ], ); 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 bef7ca88d9..0dcb8dd3e6 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 @@ -32,13 +32,13 @@ Future showLinkToPageMenu( customTitle: titleFromPageType(pageType), insertPage: pageType != ViewLayoutPB.Document, limitResults: 15, - ).inlinePageReferenceDelegate, + ), ], ); final List initialResults = []; for (final handler in service.handlers) { - final group = await handler(); + final group = await handler.search(null); if (group.results.isNotEmpty) { initialResults.add(group); 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 index f78b73bef7..42ee1a63d1 100644 --- 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 @@ -1,9 +1,10 @@ +import 'package:flutter/material.dart'; + 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 = '+'; @@ -89,7 +90,7 @@ Future inlinePageReferenceCommandHandler( InlinePageReferenceService( currentViewId: currentViewId, limitResults: 10, - ).inlinePageReferenceDelegate, + ), ], ); @@ -97,7 +98,7 @@ Future inlinePageReferenceCommandHandler( final List initialResults = []; for (final handler in service.handlers) { - final group = await handler(); + final group = await handler.search(null); if (group.results.isNotEmpty) { initialResults.add(group); 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 6d5012a5d6..6f3bf087a8 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 @@ -1,17 +1,19 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/date/date_service.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; +import 'package:appflowy/plugins/inline_actions/service_handler.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; final _keywords = [ LocaleKeys.inlineActions_date.tr().toLowerCase(), ]; -class DateReferenceService { +class DateReferenceService extends InlineActionsDelegate { DateReferenceService(this.context) { // Initialize locale _locale = context.locale.toLanguageTag(); @@ -27,7 +29,8 @@ class DateReferenceService { List options = []; - Future dateReferenceDelegate([ + @override + Future search([ String? search, ]) async { // Checks if Locale has changed since last 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 caba7d6888..b8d94cee3c 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 @@ -9,16 +9,23 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/me import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.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/service_handler.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; +import 'package:appflowy/workspace/application/workspace/workspace_listener.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; -class InlinePageReferenceService { +class InlinePageReferenceService extends InlineActionsDelegate { InlinePageReferenceService({ required this.currentViewId, this.viewLayout, @@ -51,12 +58,67 @@ class InlinePageReferenceService { List _items = []; List _filtered = []; + UserProfilePB? _user; + String? _workspaceId; + WorkspaceListener? _listener; + Future init() async { _items = await _generatePageItems(currentViewId, viewLayout); _filtered = limitResults > 0 ? _items.take(limitResults).toList() : _items; + + await _initWorkspaceListener(); + _initCompleter.complete(); } + Future _initWorkspaceListener() async { + final snapshot = await Future.wait([ + FolderEventGetCurrentWorkspaceSetting().send(), + getIt().getUser(), + ]); + + final (workspaceSettings, userProfile) = (snapshot.first, snapshot.last); + _workspaceId = workspaceSettings.fold( + (s) => (s as WorkspaceSettingPB).workspaceId, + (e) => null, + ); + + _user = userProfile.fold((s) => s as UserProfilePB, (e) => null); + + if (_user != null && _workspaceId != null) { + _listener = WorkspaceListener( + user: _user!, + workspaceId: _workspaceId!, + ); + _listener!.start( + appsChanged: (_) async { + _items = await _generatePageItems(currentViewId, viewLayout); + _filtered = + limitResults > 0 ? _items.take(limitResults).toList() : _items; + }, + ); + } + } + + @override + Future search([ + String? search, + ]) async { + _filtered = await _filterItems(search); + + return InlineActionsResult( + title: customTitle?.isNotEmpty == true + ? customTitle! + : LocaleKeys.inlineActions_pageReference.tr(), + results: _filtered, + ); + } + + @override + Future dispose() async { + await _listener?.stop(); + } + Future> _filterItems(String? search) async { await _initCompleter.future; @@ -76,19 +138,6 @@ class InlinePageReferenceService { : items.toList(); } - Future inlinePageReferenceDelegate([ - String? search, - ]) async { - _filtered = await _filterItems(search); - - return InlineActionsResult( - title: customTitle?.isNotEmpty == true - ? customTitle! - : LocaleKeys.inlineActions_pageReference.tr(), - results: _filtered, - ); - } - Future> _generatePageItems( String currentViewId, ViewLayoutPB? viewLayout, 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 cbb2c5e007..1fdda2a40e 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 @@ -1,9 +1,12 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/date/date_service.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/doc_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; +import 'package:appflowy/plugins/inline_actions/service_handler.dart'; import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; import 'package:appflowy/user/application/reminder/reminder_extension.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart'; @@ -11,7 +14,6 @@ import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:fixnum/fixnum.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:nanoid/nanoid.dart'; @@ -20,7 +22,7 @@ final _keywords = [ LocaleKeys.inlineActions_reminder_shortKeyword.tr().toLowerCase(), ]; -class ReminderReferenceService { +class ReminderReferenceService extends InlineActionsDelegate { ReminderReferenceService(this.context) { // Initialize locale _locale = context.locale.toLanguageTag(); @@ -36,7 +38,8 @@ class ReminderReferenceService { List options = []; - Future reminderReferenceDelegate([ + @override + Future search([ String? search, ]) async { // Checks if Locale has changed since last diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_command.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_command.dart index c1004d63f2..845eaf8c69 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_command.dart @@ -41,7 +41,7 @@ Future inlineActionsCommandHandler( final List initialResults = []; for (final handler in service.handlers) { - final group = await handler(); + final group = await handler.search(null); if (group.results.isNotEmpty) { initialResults.add(group); diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_service.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_service.dart index d3a5623d2f..3bdd5bf61a 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_service.dart @@ -1,9 +1,6 @@ -import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; import 'package:flutter/material.dart'; -typedef InlineActionsDelegate = Future Function([ - String? search, -]); +import 'package:appflowy/plugins/inline_actions/service_handler.dart'; abstract class _InlineActionsProvider { void dispose(); @@ -26,7 +23,10 @@ class InlineActionsService extends _InlineActionsProvider { /// we set the [BuildContext] to null. /// @override - void dispose() { + Future dispose() async { + for (final handler in handlers) { + await handler.dispose(); + } context = null; } } diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/service_handler.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/service_handler.dart new file mode 100644 index 0000000000..de537fb964 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/service_handler.dart @@ -0,0 +1,7 @@ +import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; + +abstract class InlineActionsDelegate { + Future search(String? search); + + Future dispose() async {} +} 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 da04567fcc..a19dffbd02 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 @@ -92,7 +92,7 @@ class _InlineActionsHandlerState extends State { Future _doSearch() async { final List newResults = []; for (final handler in widget.service.handlers) { - final group = await handler.call(_search); + final group = await handler.search(_search); if (group.results.isNotEmpty) { newResults.add(group); diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index 89c89b9ff3..04c1b2b4fc 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -27,7 +27,8 @@ use crate::entities::{ UpdateViewParams, ViewPB, WorkspacePB, WorkspaceSettingPB, }; use crate::manager_observer::{ - notify_child_views_changed, notify_parent_view_did_change, ChildViewChangeReason, + notify_child_views_changed, notify_did_update_workspace, notify_parent_view_did_change, + ChildViewChangeReason, }; use crate::notification::{ send_notification, send_workspace_setting_notification, FolderNotification, @@ -991,7 +992,15 @@ impl FolderManager { send_notification(&view_pb.id, FolderNotification::DidUpdateView) .payload(view_pb) .send(); + + if let Ok(workspace_id) = self.get_current_workspace_id().await { + let folder = &self.mutex_folder.lock(); + if let Some(folder) = folder.as_ref() { + notify_did_update_workspace(&workspace_id, folder); + } + } } + Ok(()) }