From 8c1520b273b3e2a9ebf5789acc2a5b77816dbe8e Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 2 Jul 2024 13:02:15 +0800 Subject: [PATCH] feat: sync the created view after duplicating (#5674) * feat: sync the created view after duplicating * chore: revert launch.json * chore: refacotor code --- .../application/sidebar/space/space_bloc.dart | 1 + .../workspace/application/view/view_bloc.dart | 1 + .../application/view/view_service.dart | 4 +- .../menu/view/view_more_action_button.dart | 13 +-- .../src/folder_event.rs | 2 +- .../src/deps_resolve/folder_deps.rs | 16 +-- .../rust-lib/flowy-database2/src/manager.rs | 13 ++- .../flowy-folder/src/entities/view.rs | 6 ++ .../flowy-folder/src/event_handler.rs | 2 +- frontend/rust-lib/flowy-folder/src/manager.rs | 102 ++++++++++++++---- .../rust-lib/flowy-folder/src/test_helper.rs | 2 +- .../flowy-folder/src/view_operation.rs | 5 +- 12 files changed, 124 insertions(+), 43 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_bloc.dart index 611908cb1a..eeb5043920 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_bloc.dart @@ -625,6 +625,7 @@ class SpaceBloc extends Bloc { await ViewBackendService.duplicate( view: view, openAfterDuplicate: true, + syncAfterDuplicate: true, includeChildren: true, parentViewId: newSpace.id, suffix: '', diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart index 2600422511..ff486fbace 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart @@ -157,6 +157,7 @@ class ViewBloc extends Bloc { final result = await ViewBackendService.duplicate( view: view, openAfterDuplicate: true, + syncAfterDuplicate: true, includeChildren: true, ); emit( 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 9a0874b711..4d59958333 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart @@ -143,11 +143,13 @@ class ViewBackendService { required bool includeChildren, String? parentViewId, String? suffix, + required bool syncAfterDuplicate, }) { final payload = DuplicateViewPayloadPB.create() ..viewId = view.id ..openAfterDuplicate = openAfterDuplicate - ..includeChildren = includeChildren; + ..includeChildren = includeChildren + ..syncAfterCreate = syncAfterDuplicate; if (parentViewId != null) { payload.parentViewId = parentViewId; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart index 5e0107ce73..0cecd4849c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart @@ -242,14 +242,11 @@ class ViewMoreActionTypeWrapper extends CustomActionCell { leftIcon: inner.leftIcon, rightIcon: inner.rightIcon, iconPadding: 10.0, - text: SizedBox( - height: 18.0, - child: FlowyText.regular( - inner.name, - color: inner == ViewMoreActionType.delete - ? Theme.of(context).colorScheme.error - : null, - ), + text: FlowyText.regular( + inner.name, + color: inner == ViewMoreActionType.delete + ? Theme.of(context).colorScheme.error + : null, ), onTap: onTap, ), diff --git a/frontend/rust-lib/event-integration-test/src/folder_event.rs b/frontend/rust-lib/event-integration-test/src/folder_event.rs index 4c07a19862..6168d4bd41 100644 --- a/frontend/rust-lib/event-integration-test/src/folder_event.rs +++ b/frontend/rust-lib/event-integration-test/src/folder_event.rs @@ -140,7 +140,7 @@ impl EventIntegrationTest { self .appflowy_core .folder_manager - .create_view_with_params(params) + .create_view_with_params(params, true) .await .unwrap(); } 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 e1f53a55e9..81739a51d8 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 @@ -185,16 +185,16 @@ impl FolderOperationHandler for DocumentFolderOperation { &self, user_id: i64, params: CreateViewParams, - ) -> FutureResult<(), FlowyError> { + ) -> FutureResult, FlowyError> { debug_assert_eq!(params.layout, ViewLayoutPB::Document); let view_id = params.view_id.to_string(); let manager = self.0.clone(); FutureResult::new(async move { let data = DocumentDataPB::try_from(Bytes::from(params.initial_data))?; - manager + let encoded_collab = manager .create_document(user_id, &view_id, Some(data.into())) .await?; - Ok(()) + Ok(Some(encoded_collab)) }) } @@ -301,16 +301,16 @@ impl FolderOperationHandler for DatabaseFolderOperation { &self, _user_id: i64, params: CreateViewParams, - ) -> FutureResult<(), FlowyError> { + ) -> FutureResult, FlowyError> { match CreateDatabaseExtParams::from_map(params.meta.clone()) { None => { let database_manager = self.0.clone(); let view_id = params.view_id.to_string(); FutureResult::new(async move { - database_manager + let encoded_collab = database_manager .create_database_with_database_data(&view_id, params.initial_data) .await?; - Ok(()) + Ok(Some(encoded_collab)) }) }, Some(database_params) => { @@ -338,7 +338,7 @@ impl FolderOperationHandler for DatabaseFolderOperation { database_parent_view_id, ) .await?; - Ok(()) + Ok(None) }) }, } @@ -505,7 +505,7 @@ impl FolderOperationHandler for ChatFolderOperation { &self, _user_id: i64, _params: CreateViewParams, - ) -> FutureResult<(), FlowyError> { + ) -> FutureResult, FlowyError> { FutureResult::new(async move { Err(FlowyError::not_support()) }) } diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index 58ccec904d..c7d941f4cb 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -10,7 +10,7 @@ use collab_database::views::{CreateDatabaseParams, CreateViewParams, DatabaseLay use collab_database::workspace_database::{ CollabDocStateByOid, CollabFuture, DatabaseCollabService, DatabaseMeta, WorkspaceDatabase, }; -use collab_entity::CollabType; +use collab_entity::{CollabType, EncodedCollab}; use collab_plugins::local_storage::kv::KVTransactionDB; use tokio::sync::{Mutex, RwLock}; use tracing::{event, instrument, trace}; @@ -289,7 +289,7 @@ impl DatabaseManager { &self, view_id: &str, data: Vec, - ) -> FlowyResult<()> { + ) -> FlowyResult { let database_data = DatabaseData::from_json_bytes(data)?; let mut create_database_params = CreateDatabaseParams::from_database_data(database_data); @@ -305,8 +305,13 @@ impl DatabaseManager { } let wdb = self.get_database_indexer().await?; - let _ = wdb.create_database(create_database_params)?; - Ok(()) + let database = wdb.create_database(create_database_params)?; + let encoded_collab = database + .lock() + .get_collab() + .lock() + .encode_collab_v1(|collab| CollabType::Database.validate_require_data(collab))?; + Ok(encoded_collab) } pub async fn create_database_with_params( diff --git a/frontend/rust-lib/flowy-folder/src/entities/view.rs b/frontend/rust-lib/flowy-folder/src/entities/view.rs index 3a919e8ec4..c774b199a4 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/view.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/view.rs @@ -599,6 +599,9 @@ pub struct DuplicateViewPayloadPB { // If the suffix is None, the duplicated view will have the same name with (copy) suffix. #[pb(index = 5, one_of)] pub suffix: Option, + + #[pb(index = 6)] + pub sync_after_create: bool, } #[derive(Debug)] @@ -612,6 +615,8 @@ pub struct DuplicateViewParams { pub parent_view_id: Option, pub suffix: Option, + + pub sync_after_create: bool, } impl TryInto for DuplicateViewPayloadPB { @@ -625,6 +630,7 @@ impl TryInto for DuplicateViewPayloadPB { include_children: self.include_children, parent_view_id: self.parent_view_id, suffix: self.suffix, + sync_after_create: self.sync_after_create, }) } } diff --git a/frontend/rust-lib/flowy-folder/src/event_handler.rs b/frontend/rust-lib/flowy-folder/src/event_handler.rs index c22fdeacca..7af6d23f78 100644 --- a/frontend/rust-lib/flowy-folder/src/event_handler.rs +++ b/frontend/rust-lib/flowy-folder/src/event_handler.rs @@ -105,7 +105,7 @@ pub(crate) async fn create_view_handler( let folder = upgrade_folder(folder)?; let params: CreateViewParams = data.into_inner().try_into()?; let set_as_current = params.set_as_current; - let view = folder.create_view_with_params(params).await?; + let (view, _) = folder.create_view_with_params(params, true).await?; if set_as_current { let _ = folder.set_current_view(&view.id).await; } diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index 6ebf619689..adcbe416ac 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -371,11 +371,21 @@ impl FolderManager { } } - pub async fn create_view_with_params(&self, params: CreateViewParams) -> FlowyResult { + /// Asynchronously creates a view with provided parameters and notifies the workspace if update is needed. + /// + /// Commonly, the notify_workspace_update parameter is set to true when the view is created in the workspace. + /// If you're handling multiple views in the same hierarchy and want to notify the workspace only after the last view is created, + /// you can set notify_workspace_update to false to avoid multiple notifications. + pub async fn create_view_with_params( + &self, + params: CreateViewParams, + notify_workspace_update: bool, + ) -> FlowyResult<(View, Option)> { let workspace_id = self.user.workspace_id()?; let view_layout: ViewLayout = params.layout.clone().into(); let handler = self.get_handler(&view_layout)?; let user_id = self.user.user_id()?; + let mut encoded_collab: Option = None; if params.meta.is_empty() && params.initial_data.is_empty() { tracing::trace!("Create view with build-in data"); @@ -384,7 +394,7 @@ impl FolderManager { .await?; } else { tracing::trace!("Create view with view data"); - handler + encoded_collab = handler .create_view_with_view_data(user_id, params.clone()) .await?; } @@ -403,12 +413,14 @@ impl FolderManager { }, ); - let folder = &self.mutex_folder.read(); - if let Some(folder) = folder.as_ref() { - notify_did_update_workspace(&workspace_id, folder); + if notify_workspace_update { + let folder = &self.mutex_folder.read(); + if let Some(folder) = folder.as_ref() { + notify_did_update_workspace(&workspace_id, folder); + } } - Ok(view) + Ok((view, encoded_collab)) } /// The orphan view is meant to be a view that is not attached to any parent view. By default, this @@ -752,6 +764,7 @@ impl FolderManager { params.open_after_duplicate, params.include_children, params.suffix, + params.sync_after_create, ) .await } @@ -767,6 +780,7 @@ impl FolderManager { open_after_duplicated: bool, include_children: bool, suffix: Option, + sync_after_create: bool, ) -> Result<(), FlowyError> { if view_id == parent_view_id { return Err(FlowyError::new( @@ -775,6 +789,7 @@ impl FolderManager { )); } + // filter the view ids that in the trash or private section let filtered_view_ids = self.with_folder(Vec::new, |folder| { self.get_view_ids_should_be_filtered(folder) }); @@ -783,6 +798,7 @@ impl FolderManager { let mut is_source_view = true; // use a stack to duplicate the view and its children let mut stack = vec![(view_id.to_string(), parent_view_id.to_string())]; + let mut objects = vec![]; let suffix = suffix.unwrap_or(" (copy)".to_string()); while let Some((current_view_id, current_parent_id)) = stack.pop() { @@ -823,6 +839,7 @@ impl FolderManager { } else { view.name.clone() }; + let duplicate_params = CreateViewParams { parent_view_id: current_parent_id.clone(), name, @@ -838,7 +855,30 @@ impl FolderManager { icon: view.icon.clone(), }; - let duplicated_view = self.create_view_with_params(duplicate_params).await?; + // set the notify_workspace_update to false to avoid multiple notifications + let (duplicated_view, encoded_collab) = self + .create_view_with_params(duplicate_params, false) + .await?; + + if sync_after_create { + if let Some(encoded_collab) = encoded_collab { + let object_id = duplicated_view.id.clone(); + let collab_type = match duplicated_view.layout { + ViewLayout::Document => CollabType::Document, + ViewLayout::Board | ViewLayout::Grid | ViewLayout::Calendar => CollabType::Database, + ViewLayout::Chat => CollabType::Unknown, + }; + // don't block the whole import process if the view can't be encoded + if collab_type != CollabType::Unknown { + match self.get_folder_collab_params(object_id, collab_type, encoded_collab) { + Ok(params) => objects.push(params), + Err(e) => { + error!("duplicate error {}", e); + }, + } + } + } + } if include_children { let child_views = self.get_views_belong_to(¤t_view_id).await?; @@ -854,6 +894,23 @@ impl FolderManager { is_source_view = false } + let workspace_id = &self.user.workspace_id()?; + + // Sync the view to the cloud + if sync_after_create { + self + .cloud_service + .batch_create_folder_collab_objects(workspace_id, objects) + .await?; + } + + // notify the update here + notify_parent_view_did_change( + workspace_id, + self.mutex_folder.clone(), + vec![parent_view_id.to_string()], + ); + Ok(()) } @@ -1128,18 +1185,11 @@ impl FolderManager { if sync_after_create { if let Some(encoded_collab) = encoded_collab { - // Try to encode the collaboration data to bytes - let encode_collab_v1 = encoded_collab.encode_to_bytes().map_err(internal_error); - - // If the view can't be encoded, skip it and don't block the whole import process - match encode_collab_v1 { - Ok(encode_collab_v1) => objects.push(FolderCollabParams { - object_id, - encoded_collab_v1: encode_collab_v1, - collab_type, - }), + // don't block the whole import process if the view can't be encoded + match self.get_folder_collab_params(object_id, collab_type, encoded_collab) { + Ok(params) => objects.push(params), Err(e) => { - error!("import error {}", e) + error!("import error {}", e); }, } } @@ -1214,6 +1264,22 @@ impl FolderManager { } } + fn get_folder_collab_params( + &self, + object_id: String, + collab_type: CollabType, + encoded_collab: EncodedCollab, + ) -> FlowyResult { + // Try to encode the collaboration data to bytes + let encoded_collab_v1: Result, FlowyError> = + encoded_collab.encode_to_bytes().map_err(internal_error); + encoded_collab_v1.map(|encoded_collab_v1| FolderCollabParams { + object_id, + encoded_collab_v1, + collab_type, + }) + } + /// Returns the relation of the view. The relation is a tuple of (is_workspace, parent_view_id, /// child_view_ids). If the view is a workspace, then the parent_view_id is the workspace id. /// Otherwise, the parent_view_id is the parent view id of the view. The child_view_ids is the diff --git a/frontend/rust-lib/flowy-folder/src/test_helper.rs b/frontend/rust-lib/flowy-folder/src/test_helper.rs index f702956828..3779a91b48 100644 --- a/frontend/rust-lib/flowy-folder/src/test_helper.rs +++ b/frontend/rust-lib/flowy-folder/src/test_helper.rs @@ -51,7 +51,7 @@ impl FolderManager { icon: None, extra: None, }; - self.create_view_with_params(params).await.unwrap(); + self.create_view_with_params(params, true).await.unwrap(); view_id } } diff --git a/frontend/rust-lib/flowy-folder/src/view_operation.rs b/frontend/rust-lib/flowy-folder/src/view_operation.rs index 969092c58b..86b3f4894f 100644 --- a/frontend/rust-lib/flowy-folder/src/view_operation.rs +++ b/frontend/rust-lib/flowy-folder/src/view_operation.rs @@ -61,11 +61,14 @@ pub trait FolderOperationHandler { /// * `layout`: the layout of the view /// * `meta`: use to carry extra information. For example, the database view will use this /// to carry the reference database id. + /// + /// The return value is the [Option] that can be used to create the view. + /// It can be used in syncing the view data to cloud. fn create_view_with_view_data( &self, user_id: i64, params: CreateViewParams, - ) -> FutureResult<(), FlowyError>; + ) -> FutureResult, FlowyError>; /// Create a view with the pre-defined data. /// For example, the initial data of the grid/calendar/kanban board when