feat: sync the created view after duplicating (#5674)

* feat: sync the created view after duplicating

* chore: revert launch.json

* chore: refacotor code
This commit is contained in:
Lucas.Xu 2024-07-02 13:02:15 +08:00 committed by GitHub
parent a7b850e752
commit 8c1520b273
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 124 additions and 43 deletions

View File

@ -625,6 +625,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
await ViewBackendService.duplicate( await ViewBackendService.duplicate(
view: view, view: view,
openAfterDuplicate: true, openAfterDuplicate: true,
syncAfterDuplicate: true,
includeChildren: true, includeChildren: true,
parentViewId: newSpace.id, parentViewId: newSpace.id,
suffix: '', suffix: '',

View File

@ -157,6 +157,7 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
final result = await ViewBackendService.duplicate( final result = await ViewBackendService.duplicate(
view: view, view: view,
openAfterDuplicate: true, openAfterDuplicate: true,
syncAfterDuplicate: true,
includeChildren: true, includeChildren: true,
); );
emit( emit(

View File

@ -143,11 +143,13 @@ class ViewBackendService {
required bool includeChildren, required bool includeChildren,
String? parentViewId, String? parentViewId,
String? suffix, String? suffix,
required bool syncAfterDuplicate,
}) { }) {
final payload = DuplicateViewPayloadPB.create() final payload = DuplicateViewPayloadPB.create()
..viewId = view.id ..viewId = view.id
..openAfterDuplicate = openAfterDuplicate ..openAfterDuplicate = openAfterDuplicate
..includeChildren = includeChildren; ..includeChildren = includeChildren
..syncAfterCreate = syncAfterDuplicate;
if (parentViewId != null) { if (parentViewId != null) {
payload.parentViewId = parentViewId; payload.parentViewId = parentViewId;

View File

@ -242,14 +242,11 @@ class ViewMoreActionTypeWrapper extends CustomActionCell {
leftIcon: inner.leftIcon, leftIcon: inner.leftIcon,
rightIcon: inner.rightIcon, rightIcon: inner.rightIcon,
iconPadding: 10.0, iconPadding: 10.0,
text: SizedBox( text: FlowyText.regular(
height: 18.0, inner.name,
child: FlowyText.regular( color: inner == ViewMoreActionType.delete
inner.name, ? Theme.of(context).colorScheme.error
color: inner == ViewMoreActionType.delete : null,
? Theme.of(context).colorScheme.error
: null,
),
), ),
onTap: onTap, onTap: onTap,
), ),

View File

@ -140,7 +140,7 @@ impl EventIntegrationTest {
self self
.appflowy_core .appflowy_core
.folder_manager .folder_manager
.create_view_with_params(params) .create_view_with_params(params, true)
.await .await
.unwrap(); .unwrap();
} }

View File

@ -185,16 +185,16 @@ impl FolderOperationHandler for DocumentFolderOperation {
&self, &self,
user_id: i64, user_id: i64,
params: CreateViewParams, params: CreateViewParams,
) -> FutureResult<(), FlowyError> { ) -> FutureResult<Option<EncodedCollab>, FlowyError> {
debug_assert_eq!(params.layout, ViewLayoutPB::Document); debug_assert_eq!(params.layout, ViewLayoutPB::Document);
let view_id = params.view_id.to_string(); let view_id = params.view_id.to_string();
let manager = self.0.clone(); let manager = self.0.clone();
FutureResult::new(async move { FutureResult::new(async move {
let data = DocumentDataPB::try_from(Bytes::from(params.initial_data))?; let data = DocumentDataPB::try_from(Bytes::from(params.initial_data))?;
manager let encoded_collab = manager
.create_document(user_id, &view_id, Some(data.into())) .create_document(user_id, &view_id, Some(data.into()))
.await?; .await?;
Ok(()) Ok(Some(encoded_collab))
}) })
} }
@ -301,16 +301,16 @@ impl FolderOperationHandler for DatabaseFolderOperation {
&self, &self,
_user_id: i64, _user_id: i64,
params: CreateViewParams, params: CreateViewParams,
) -> FutureResult<(), FlowyError> { ) -> FutureResult<Option<EncodedCollab>, FlowyError> {
match CreateDatabaseExtParams::from_map(params.meta.clone()) { match CreateDatabaseExtParams::from_map(params.meta.clone()) {
None => { None => {
let database_manager = self.0.clone(); let database_manager = self.0.clone();
let view_id = params.view_id.to_string(); let view_id = params.view_id.to_string();
FutureResult::new(async move { FutureResult::new(async move {
database_manager let encoded_collab = database_manager
.create_database_with_database_data(&view_id, params.initial_data) .create_database_with_database_data(&view_id, params.initial_data)
.await?; .await?;
Ok(()) Ok(Some(encoded_collab))
}) })
}, },
Some(database_params) => { Some(database_params) => {
@ -338,7 +338,7 @@ impl FolderOperationHandler for DatabaseFolderOperation {
database_parent_view_id, database_parent_view_id,
) )
.await?; .await?;
Ok(()) Ok(None)
}) })
}, },
} }
@ -505,7 +505,7 @@ impl FolderOperationHandler for ChatFolderOperation {
&self, &self,
_user_id: i64, _user_id: i64,
_params: CreateViewParams, _params: CreateViewParams,
) -> FutureResult<(), FlowyError> { ) -> FutureResult<Option<EncodedCollab>, FlowyError> {
FutureResult::new(async move { Err(FlowyError::not_support()) }) FutureResult::new(async move { Err(FlowyError::not_support()) })
} }

View File

@ -10,7 +10,7 @@ use collab_database::views::{CreateDatabaseParams, CreateViewParams, DatabaseLay
use collab_database::workspace_database::{ use collab_database::workspace_database::{
CollabDocStateByOid, CollabFuture, DatabaseCollabService, DatabaseMeta, WorkspaceDatabase, CollabDocStateByOid, CollabFuture, DatabaseCollabService, DatabaseMeta, WorkspaceDatabase,
}; };
use collab_entity::CollabType; use collab_entity::{CollabType, EncodedCollab};
use collab_plugins::local_storage::kv::KVTransactionDB; use collab_plugins::local_storage::kv::KVTransactionDB;
use tokio::sync::{Mutex, RwLock}; use tokio::sync::{Mutex, RwLock};
use tracing::{event, instrument, trace}; use tracing::{event, instrument, trace};
@ -289,7 +289,7 @@ impl DatabaseManager {
&self, &self,
view_id: &str, view_id: &str,
data: Vec<u8>, data: Vec<u8>,
) -> FlowyResult<()> { ) -> FlowyResult<EncodedCollab> {
let database_data = DatabaseData::from_json_bytes(data)?; let database_data = DatabaseData::from_json_bytes(data)?;
let mut create_database_params = CreateDatabaseParams::from_database_data(database_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 = self.get_database_indexer().await?;
let _ = wdb.create_database(create_database_params)?; let database = wdb.create_database(create_database_params)?;
Ok(()) 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( pub async fn create_database_with_params(

View File

@ -599,6 +599,9 @@ pub struct DuplicateViewPayloadPB {
// If the suffix is None, the duplicated view will have the same name with (copy) suffix. // If the suffix is None, the duplicated view will have the same name with (copy) suffix.
#[pb(index = 5, one_of)] #[pb(index = 5, one_of)]
pub suffix: Option<String>, pub suffix: Option<String>,
#[pb(index = 6)]
pub sync_after_create: bool,
} }
#[derive(Debug)] #[derive(Debug)]
@ -612,6 +615,8 @@ pub struct DuplicateViewParams {
pub parent_view_id: Option<String>, pub parent_view_id: Option<String>,
pub suffix: Option<String>, pub suffix: Option<String>,
pub sync_after_create: bool,
} }
impl TryInto<DuplicateViewParams> for DuplicateViewPayloadPB { impl TryInto<DuplicateViewParams> for DuplicateViewPayloadPB {
@ -625,6 +630,7 @@ impl TryInto<DuplicateViewParams> for DuplicateViewPayloadPB {
include_children: self.include_children, include_children: self.include_children,
parent_view_id: self.parent_view_id, parent_view_id: self.parent_view_id,
suffix: self.suffix, suffix: self.suffix,
sync_after_create: self.sync_after_create,
}) })
} }
} }

View File

@ -105,7 +105,7 @@ pub(crate) async fn create_view_handler(
let folder = upgrade_folder(folder)?; let folder = upgrade_folder(folder)?;
let params: CreateViewParams = data.into_inner().try_into()?; let params: CreateViewParams = data.into_inner().try_into()?;
let set_as_current = params.set_as_current; 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 { if set_as_current {
let _ = folder.set_current_view(&view.id).await; let _ = folder.set_current_view(&view.id).await;
} }

View File

@ -371,11 +371,21 @@ impl FolderManager {
} }
} }
pub async fn create_view_with_params(&self, params: CreateViewParams) -> FlowyResult<View> { /// 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<EncodedCollab>)> {
let workspace_id = self.user.workspace_id()?; let workspace_id = self.user.workspace_id()?;
let view_layout: ViewLayout = params.layout.clone().into(); let view_layout: ViewLayout = params.layout.clone().into();
let handler = self.get_handler(&view_layout)?; let handler = self.get_handler(&view_layout)?;
let user_id = self.user.user_id()?; let user_id = self.user.user_id()?;
let mut encoded_collab: Option<EncodedCollab> = None;
if params.meta.is_empty() && params.initial_data.is_empty() { if params.meta.is_empty() && params.initial_data.is_empty() {
tracing::trace!("Create view with build-in data"); tracing::trace!("Create view with build-in data");
@ -384,7 +394,7 @@ impl FolderManager {
.await?; .await?;
} else { } else {
tracing::trace!("Create view with view data"); tracing::trace!("Create view with view data");
handler encoded_collab = handler
.create_view_with_view_data(user_id, params.clone()) .create_view_with_view_data(user_id, params.clone())
.await?; .await?;
} }
@ -403,12 +413,14 @@ impl FolderManager {
}, },
); );
let folder = &self.mutex_folder.read(); if notify_workspace_update {
if let Some(folder) = folder.as_ref() { let folder = &self.mutex_folder.read();
notify_did_update_workspace(&workspace_id, folder); 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 /// 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.open_after_duplicate,
params.include_children, params.include_children,
params.suffix, params.suffix,
params.sync_after_create,
) )
.await .await
} }
@ -767,6 +780,7 @@ impl FolderManager {
open_after_duplicated: bool, open_after_duplicated: bool,
include_children: bool, include_children: bool,
suffix: Option<String>, suffix: Option<String>,
sync_after_create: bool,
) -> Result<(), FlowyError> { ) -> Result<(), FlowyError> {
if view_id == parent_view_id { if view_id == parent_view_id {
return Err(FlowyError::new( 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| { let filtered_view_ids = self.with_folder(Vec::new, |folder| {
self.get_view_ids_should_be_filtered(folder) self.get_view_ids_should_be_filtered(folder)
}); });
@ -783,6 +798,7 @@ impl FolderManager {
let mut is_source_view = true; let mut is_source_view = true;
// use a stack to duplicate the view and its children // 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 stack = vec![(view_id.to_string(), parent_view_id.to_string())];
let mut objects = vec![];
let suffix = suffix.unwrap_or(" (copy)".to_string()); let suffix = suffix.unwrap_or(" (copy)".to_string());
while let Some((current_view_id, current_parent_id)) = stack.pop() { while let Some((current_view_id, current_parent_id)) = stack.pop() {
@ -823,6 +839,7 @@ impl FolderManager {
} else { } else {
view.name.clone() view.name.clone()
}; };
let duplicate_params = CreateViewParams { let duplicate_params = CreateViewParams {
parent_view_id: current_parent_id.clone(), parent_view_id: current_parent_id.clone(),
name, name,
@ -838,7 +855,30 @@ impl FolderManager {
icon: view.icon.clone(), 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 { if include_children {
let child_views = self.get_views_belong_to(&current_view_id).await?; let child_views = self.get_views_belong_to(&current_view_id).await?;
@ -854,6 +894,23 @@ impl FolderManager {
is_source_view = false 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(()) Ok(())
} }
@ -1128,18 +1185,11 @@ impl FolderManager {
if sync_after_create { if sync_after_create {
if let Some(encoded_collab) = encoded_collab { if let Some(encoded_collab) = encoded_collab {
// Try to encode the collaboration data to bytes // don't block the whole import process if the view can't be encoded
let encode_collab_v1 = encoded_collab.encode_to_bytes().map_err(internal_error); match self.get_folder_collab_params(object_id, collab_type, encoded_collab) {
Ok(params) => objects.push(params),
// 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,
}),
Err(e) => { 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<FolderCollabParams> {
// Try to encode the collaboration data to bytes
let encoded_collab_v1: Result<Vec<u8>, 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, /// 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. /// 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 /// Otherwise, the parent_view_id is the parent view id of the view. The child_view_ids is the

View File

@ -51,7 +51,7 @@ impl FolderManager {
icon: None, icon: None,
extra: None, extra: None,
}; };
self.create_view_with_params(params).await.unwrap(); self.create_view_with_params(params, true).await.unwrap();
view_id view_id
} }
} }

View File

@ -61,11 +61,14 @@ pub trait FolderOperationHandler {
/// * `layout`: the layout of the view /// * `layout`: the layout of the view
/// * `meta`: use to carry extra information. For example, the database view will use this /// * `meta`: use to carry extra information. For example, the database view will use this
/// to carry the reference database id. /// to carry the reference database id.
///
/// The return value is the [Option<EncodedCollab>] 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( fn create_view_with_view_data(
&self, &self,
user_id: i64, user_id: i64,
params: CreateViewParams, params: CreateViewParams,
) -> FutureResult<(), FlowyError>; ) -> FutureResult<Option<EncodedCollab>, FlowyError>;
/// Create a view with the pre-defined data. /// Create a view with the pre-defined data.
/// For example, the initial data of the grid/calendar/kanban board when /// For example, the initial data of the grid/calendar/kanban board when