From 1c8913eb7996fd66edb981c1701f38a4c184bc9e Mon Sep 17 00:00:00 2001 From: qinluhe Date: Fri, 21 Jun 2024 00:00:47 +0800 Subject: [PATCH] feat: support publish view and unpublish views --- .../src/folder_event.rs | 20 ++ .../tests/folder/local_test/mod.rs | 1 + .../folder/local_test/view_publish_test.rs | 159 +++++++++++++++ .../src/deps_resolve/folder_deps.rs | 34 ++++ .../flowy-core/src/integrate/trait_impls.rs | 60 ++++++ .../rust-lib/flowy-document/src/manager.rs | 6 +- frontend/rust-lib/flowy-folder-pub/Cargo.toml | 1 + .../rust-lib/flowy-folder-pub/src/cloud.rs | 19 ++ .../rust-lib/flowy-folder-pub/src/entities.rs | 43 ++++ frontend/rust-lib/flowy-folder/Cargo.toml | 3 + .../flowy-folder/src/entities/icon.rs | 2 +- .../rust-lib/flowy-folder/src/entities/mod.rs | 2 + .../flowy-folder/src/entities/publish.rs | 48 +++++ .../flowy-folder/src/event_handler.rs | 55 +++++ .../rust-lib/flowy-folder/src/event_map.rs | 20 ++ frontend/rust-lib/flowy-folder/src/lib.rs | 1 + frontend/rust-lib/flowy-folder/src/manager.rs | 191 ++++++++++++++++++ .../rust-lib/flowy-folder/src/publish_util.rs | 33 +++ .../flowy-folder/src/view_operation.rs | 7 + .../flowy-server/src/af_cloud/impls/folder.rs | 98 ++++++++- .../src/local_server/impls/folder.rs | 45 +++++ .../flowy-server/src/supabase/api/folder.rs | 41 ++++ 22 files changed, 884 insertions(+), 5 deletions(-) create mode 100644 frontend/rust-lib/event-integration-test/tests/folder/local_test/view_publish_test.rs create mode 100644 frontend/rust-lib/flowy-folder/src/entities/publish.rs create mode 100644 frontend/rust-lib/flowy-folder/src/publish_util.rs 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 0a13eb7f5b..9d0b756f29 100644 --- a/frontend/rust-lib/event-integration-test/src/folder_event.rs +++ b/frontend/rust-lib/event-integration-test/src/folder_event.rs @@ -1,3 +1,4 @@ +use collab::entity::EncodedCollab; use std::sync::Arc; use collab_folder::{FolderData, View}; @@ -5,6 +6,7 @@ use flowy_folder::entities::icon::UpdateViewIconPayloadPB; use flowy_folder::event_map::FolderEvent; use flowy_folder::event_map::FolderEvent::*; use flowy_folder::{entities::*, ViewLayout}; +use flowy_folder_pub::entities::PublishViewPayload; use flowy_search::services::manager::{SearchHandler, SearchType}; use flowy_user::entities::{ AcceptWorkspaceInvitationPB, QueryWorkspacePB, RemoveWorkspaceMemberPB, @@ -172,6 +174,24 @@ impl EventIntegrationTest { folder.get_folder_data(&workspace_id).clone().unwrap() } + pub async fn get_publish_payload(&self, view_id: &str) -> Vec { + let manager = self.folder_manager.clone(); + let payload = manager.get_batch_publish_payload(view_id, None).await; + + if payload.is_err() { + panic!("Get publish payload failed") + } + + payload.unwrap() + } + + pub async fn encoded_collab_v1(&self, view_id: &str, layout: ViewLayout) -> EncodedCollab { + let manager = self.folder_manager.clone(); + let handlers = manager.get_operation_handlers(); + let handler = handlers.get(&layout).unwrap(); + handler.encoded_collab_v1(view_id, layout).await.unwrap() + } + pub async fn get_all_workspace_views(&self) -> Vec { EventBuilder::new(self.clone()) .event(FolderEvent::ReadCurrentWorkspaceViews) diff --git a/frontend/rust-lib/event-integration-test/tests/folder/local_test/mod.rs b/frontend/rust-lib/event-integration-test/tests/folder/local_test/mod.rs index aa58a02baf..2c48d266f7 100644 --- a/frontend/rust-lib/event-integration-test/tests/folder/local_test/mod.rs +++ b/frontend/rust-lib/event-integration-test/tests/folder/local_test/mod.rs @@ -3,3 +3,4 @@ mod import_test; mod script; mod subscription_test; mod test; +mod view_publish_test; diff --git a/frontend/rust-lib/event-integration-test/tests/folder/local_test/view_publish_test.rs b/frontend/rust-lib/event-integration-test/tests/folder/local_test/view_publish_test.rs new file mode 100644 index 0000000000..718d090736 --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/folder/local_test/view_publish_test.rs @@ -0,0 +1,159 @@ +use collab_folder::ViewLayout; +use event_integration_test::EventIntegrationTest; +use flowy_folder::entities::{ViewLayoutPB, ViewPB}; +use flowy_folder::publish_util::generate_publish_name; +use flowy_folder_pub::entities::{ + PublishViewInfo, PublishViewMeta, PublishViewMetaData, PublishViewPayload, +}; + +async fn mock_single_document_view_publish_payload( + test: &EventIntegrationTest, + view: &ViewPB, + publish_name: String, +) -> Vec { + let view_id = &view.id; + let layout: ViewLayout = view.layout.clone().into(); + let view_encoded_collab = test.encoded_collab_v1(view_id, layout).await; + let publish_view_info = PublishViewInfo { + view_id: view_id.to_string(), + name: view.name.to_string(), + icon: None, + layout: ViewLayout::Document, + extra: None, + created_by: view.created_by, + last_edited_by: view.last_edited_by, + last_edited_time: view.last_edited, + created_at: view.create_time, + child_views: None, + }; + + vec![PublishViewPayload { + meta: PublishViewMeta { + metadata: PublishViewMetaData { + view: publish_view_info.clone(), + child_views: vec![], + ancestor_views: vec![publish_view_info], + }, + view_id: view_id.to_string(), + publish_name, + }, + data: Vec::from(view_encoded_collab.doc_state), + }] +} + +async fn mock_nested_document_view_publish_payload( + test: &EventIntegrationTest, + view: &ViewPB, + publish_name: String, +) -> Vec { + let view_id = &view.id; + let layout: ViewLayout = view.layout.clone().into(); + let view_encoded_collab = test.encoded_collab_v1(view_id, layout).await; + let publish_view_info = PublishViewInfo { + view_id: view_id.to_string(), + name: view.name.to_string(), + icon: None, + layout: ViewLayout::Document, + extra: None, + created_by: view.created_by, + last_edited_by: view.last_edited_by, + last_edited_time: view.last_edited, + created_at: view.create_time, + child_views: None, + }; + + let child_view_id = &view.child_views[0].id; + let child_view = test.get_view(child_view_id).await; + let child_layout: ViewLayout = child_view.layout.clone().into(); + let child_view_encoded_collab = test.encoded_collab_v1(child_view_id, child_layout).await; + let child_publish_view_info = PublishViewInfo { + view_id: child_view_id.to_string(), + name: child_view.name.to_string(), + icon: None, + layout: ViewLayout::Document, + extra: None, + created_by: child_view.created_by, + last_edited_by: child_view.last_edited_by, + last_edited_time: child_view.last_edited, + created_at: child_view.create_time, + child_views: None, + }; + let child_publish_name = generate_publish_name(&child_view.id, &child_view.name); + + vec![ + PublishViewPayload { + meta: PublishViewMeta { + metadata: PublishViewMetaData { + view: publish_view_info.clone(), + child_views: vec![child_publish_view_info.clone()], + ancestor_views: vec![publish_view_info.clone()], + }, + view_id: view_id.to_string(), + publish_name, + }, + data: Vec::from(view_encoded_collab.doc_state), + }, + PublishViewPayload { + meta: PublishViewMeta { + metadata: PublishViewMetaData { + view: child_publish_view_info.clone(), + child_views: vec![], + ancestor_views: vec![publish_view_info.clone(), child_publish_view_info.clone()], + }, + view_id: child_view_id.to_string(), + publish_name: child_publish_name, + }, + data: Vec::from(child_view_encoded_collab.doc_state), + }, + ] +} + +async fn create_single_document(test: &EventIntegrationTest, view_id: &str, name: &str) { + test + .create_orphan_view(name, view_id, ViewLayoutPB::Document) + .await; +} + +async fn create_nested_document(test: &EventIntegrationTest, view_id: &str, name: &str) { + create_single_document(test, view_id, name).await; + let child_name = "Child View"; + test.create_view(view_id, child_name.to_string()).await; +} +#[tokio::test] +async fn single_document_get_publish_view_payload_test() { + let test = EventIntegrationTest::new_anon().await; + let view_id = "20240521"; + let name = "Orphan View"; + create_single_document(&test, view_id, name).await; + let view = test.get_view(view_id).await; + let payload = test.get_publish_payload(view_id).await; + + let expect_payload = mock_single_document_view_publish_payload( + &test, + &view, + format!("{}-{}", "Orphan_View", view_id), + ) + .await; + + assert_eq!(payload, expect_payload); +} + +#[tokio::test] +async fn nested_document_get_publish_view_payload_test() { + let test = EventIntegrationTest::new_anon().await; + let name = "Orphan View"; + let view_id = "20240521"; + create_nested_document(&test, view_id, name).await; + let view = test.get_view(view_id).await; + let payload = test.get_publish_payload(view_id).await; + + let expect_payload = mock_nested_document_view_publish_payload( + &test, + &view, + format!("{}-{}", "Orphan_View", view_id), + ) + .await; + + assert_eq!(payload.len(), 2); + assert_eq!(payload, expect_payload); +} 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 9eee9d7875..8387d38155 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 @@ -1,4 +1,5 @@ use bytes::Bytes; +use collab::entity::EncodedCollab; use collab_integrate::collab_builder::AppFlowyCollabBuilder; use collab_integrate::CollabKVDB; use flowy_chat::chat_manager::ChatManager; @@ -200,6 +201,21 @@ impl FolderOperationHandler for DocumentFolderOperation { }) } + fn encoded_collab_v1( + &self, + view_id: &str, + layout: ViewLayout, + ) -> FutureResult { + debug_assert_eq!(layout, ViewLayout::Document); + let view_id = view_id.to_string(); + let manager = self.0.clone(); + FutureResult::new(async move { + let encoded_collab = manager.encode_collab(&view_id).await?; + + Ok(encoded_collab) + }) + } + /// Create a view with built-in data. fn create_built_in_view( &self, @@ -287,6 +303,15 @@ impl FolderOperationHandler for DatabaseFolderOperation { }) } + fn encoded_collab_v1( + &self, + _view_id: &str, + _layout: ViewLayout, + ) -> FutureResult { + // Database view doesn't support collab + FutureResult::new(async move { Err(FlowyError::not_support()) }) + } + fn duplicate_view(&self, view_id: &str) -> FutureResult { let database_manager = self.0.clone(); let view_id = view_id.to_owned(); @@ -547,4 +572,13 @@ impl FolderOperationHandler for ChatFolderOperation { ) -> FutureResult<(), FlowyError> { FutureResult::new(async move { Err(FlowyError::not_support()) }) } + + fn encoded_collab_v1( + &self, + _view_id: &str, + _layout: ViewLayout, + ) -> FutureResult { + // Chat view doesn't support collab + FutureResult::new(async move { Err(FlowyError::not_support()) }) + } } diff --git a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs index 71c5c0891d..3a379d47d8 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs @@ -31,6 +31,7 @@ use flowy_error::{FlowyError, FlowyResult}; use flowy_folder_pub::cloud::{ FolderCloudService, FolderCollabParams, FolderData, FolderSnapshot, Workspace, WorkspaceRecord, }; +use flowy_folder_pub::entities::{PublishInfoResponse, PublishViewPayload}; use flowy_server_pub::af_cloud_config::AFCloudConfiguration; use flowy_server_pub::supabase_config::SupabaseConfiguration; use flowy_storage_pub::cloud::{ObjectIdentity, ObjectValue, StorageCloudService}; @@ -301,6 +302,65 @@ impl FolderCloudService for ServerProvider { .map(|provider| provider.folder_service().service_name()) .unwrap_or_default() } + + fn publish_view( + &self, + workspace_id: &str, + payload: Vec, + ) -> FutureResult<(), Error> { + let workspace_id = workspace_id.to_string(); + let server = self.get_server(); + FutureResult::new(async move { + server? + .folder_service() + .publish_view(&workspace_id, payload) + .await + }) + } + + fn unpublish_views(&self, workspace_id: &str, view_ids: Vec) -> FutureResult<(), Error> { + let workspace_id = workspace_id.to_string(); + let server = self.get_server(); + FutureResult::new(async move { + server? + .folder_service() + .unpublish_views(&workspace_id, view_ids) + .await + }) + } + + fn get_publish_info(&self, view_id: &str) -> FutureResult { + let view_id = view_id.to_string(); + let server = self.get_server(); + FutureResult::new(async move { server?.folder_service().get_publish_info(&view_id).await }) + } + + fn set_publish_namespace( + &self, + workspace_id: &str, + new_namespace: &str, + ) -> FutureResult<(), Error> { + let workspace_id = workspace_id.to_string(); + let new_namespace = new_namespace.to_string(); + let server = self.get_server(); + FutureResult::new(async move { + server? + .folder_service() + .set_publish_namespace(&workspace_id, &new_namespace) + .await + }) + } + + fn get_publish_namespace(&self, workspace_id: &str) -> FutureResult { + let workspace_id = workspace_id.to_string(); + let server = self.get_server(); + FutureResult::new(async move { + server? + .folder_service() + .get_publish_namespace(&workspace_id) + .await + }) + } } impl DatabaseCloudService for ServerProvider { diff --git a/frontend/rust-lib/flowy-document/src/manager.rs b/frontend/rust-lib/flowy-document/src/manager.rs index 37f59755f2..41a797aa3b 100644 --- a/frontend/rust-lib/flowy-document/src/manager.rs +++ b/frontend/rust-lib/flowy-document/src/manager.rs @@ -11,8 +11,8 @@ use collab_document::document_awareness::DocumentAwarenessState; use collab_document::document_awareness::DocumentAwarenessUser; use collab_document::document_data::default_document_data; use collab_entity::CollabType; -use collab_plugins::CollabKVDB; use collab_plugins::local_storage::kv::PersistenceError; +use collab_plugins::CollabKVDB; use dashmap::DashMap; use lib_infra::util::timestamp; use tracing::trace; @@ -85,7 +85,9 @@ impl DocumentManager { .await?; let collab = collab.lock(); - collab.encode_collab_v1(|_| Ok::<(), PersistenceError>(())).map_err(internal_error) + collab + .encode_collab_v1(|_| Ok::<(), PersistenceError>(())) + .map_err(internal_error) } pub async fn initialize(&self, _uid: i64) -> FlowyResult<()> { diff --git a/frontend/rust-lib/flowy-folder-pub/Cargo.toml b/frontend/rust-lib/flowy-folder-pub/Cargo.toml index 13f13935f7..fa1997aa9b 100644 --- a/frontend/rust-lib/flowy-folder-pub/Cargo.toml +++ b/frontend/rust-lib/flowy-folder-pub/Cargo.toml @@ -12,6 +12,7 @@ collab = { workspace = true } collab-entity = { workspace = true } uuid.workspace = true anyhow.workspace = true +serde = { version = "1.0.202", features = ["derive"] } [dev-dependencies] tokio.workspace = true diff --git a/frontend/rust-lib/flowy-folder-pub/src/cloud.rs b/frontend/rust-lib/flowy-folder-pub/src/cloud.rs index d88b4df203..a017215f88 100644 --- a/frontend/rust-lib/flowy-folder-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-folder-pub/src/cloud.rs @@ -3,6 +3,7 @@ use collab_entity::CollabType; pub use collab_folder::{Folder, FolderData, Workspace}; use uuid::Uuid; +use crate::entities::{PublishInfoResponse, PublishViewPayload}; use lib_infra::future::FutureResult; /// [FolderCloudService] represents the cloud service for folder. @@ -44,6 +45,24 @@ pub trait FolderCloudService: Send + Sync + 'static { ) -> FutureResult<(), Error>; fn service_name(&self) -> String; + + fn publish_view( + &self, + workspace_id: &str, + payload: Vec, + ) -> FutureResult<(), Error>; + + fn unpublish_views(&self, workspace_id: &str, view_ids: Vec) -> FutureResult<(), Error>; + + fn get_publish_info(&self, view_id: &str) -> FutureResult; + + fn set_publish_namespace( + &self, + workspace_id: &str, + new_namespace: &str, + ) -> FutureResult<(), Error>; + + fn get_publish_namespace(&self, workspace_id: &str) -> FutureResult; } #[derive(Debug)] diff --git a/frontend/rust-lib/flowy-folder-pub/src/entities.rs b/frontend/rust-lib/flowy-folder-pub/src/entities.rs index 41163fae73..393f1d7afb 100644 --- a/frontend/rust-lib/flowy-folder-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-folder-pub/src/entities.rs @@ -1,4 +1,6 @@ use crate::folder_builder::ParentChildViews; +use collab_folder::{ViewIcon, ViewLayout}; +use serde::Serialize; use std::collections::HashMap; pub enum ImportData { @@ -39,3 +41,44 @@ pub struct SearchData { /// The data that is stored in the search index row. pub data: String, } + +#[derive(Serialize, Clone, Debug, Eq, PartialEq)] +pub struct PublishViewInfo { + pub view_id: String, + pub name: String, + pub icon: Option, + pub layout: ViewLayout, + pub extra: Option, + pub created_by: Option, + pub last_edited_by: Option, + pub last_edited_time: i64, + pub created_at: i64, + pub child_views: Option>, +} + +#[derive(Serialize, Clone, Debug, Eq, PartialEq)] +pub struct PublishViewMetaData { + pub view: PublishViewInfo, + pub child_views: Vec, + pub ancestor_views: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PublishViewMeta { + pub metadata: PublishViewMetaData, + pub view_id: String, + pub publish_name: String, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PublishViewPayload { + pub meta: PublishViewMeta, + pub data: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PublishInfoResponse { + pub view_id: String, + pub publish_name: String, + pub namespace: Option, +} diff --git a/frontend/rust-lib/flowy-folder/Cargo.toml b/frontend/rust-lib/flowy-folder/Cargo.toml index b8ed79720f..e0327a5044 100644 --- a/frontend/rust-lib/flowy-folder/Cargo.toml +++ b/frontend/rust-lib/flowy-folder/Cargo.toml @@ -39,6 +39,9 @@ serde = { workspace = true, features = ["derive"] } serde_json.workspace = true validator.workspace = true async-trait.workspace = true +regex = "1.9.5" +futures = "0.3.30" + [build-dependencies] flowy-codegen.workspace = true diff --git a/frontend/rust-lib/flowy-folder/src/entities/icon.rs b/frontend/rust-lib/flowy-folder/src/entities/icon.rs index 2342b02246..1169c320ea 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/icon.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/icon.rs @@ -39,7 +39,7 @@ pub struct ViewIconPB { pub value: String, } -impl std::convert::From for ViewIcon { +impl From for ViewIcon { fn from(rev: ViewIconPB) -> Self { ViewIcon { ty: rev.ty.into(), diff --git a/frontend/rust-lib/flowy-folder/src/entities/mod.rs b/frontend/rust-lib/flowy-folder/src/entities/mod.rs index b496f334b5..24e5475caa 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/mod.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/mod.rs @@ -1,12 +1,14 @@ pub mod icon; mod import; mod parser; +pub mod publish; pub mod trash; pub mod view; pub mod workspace; pub use icon::*; pub use import::*; +pub use publish::*; pub use trash::*; pub use view::*; pub use workspace::*; diff --git a/frontend/rust-lib/flowy-folder/src/entities/publish.rs b/frontend/rust-lib/flowy-folder/src/entities/publish.rs new file mode 100644 index 0000000000..740d58c366 --- /dev/null +++ b/frontend/rust-lib/flowy-folder/src/entities/publish.rs @@ -0,0 +1,48 @@ +use flowy_derive::ProtoBuf; +use flowy_folder_pub::entities::PublishInfoResponse; + +#[derive(Default, ProtoBuf)] +pub struct PublishViewParamsPB { + #[pb(index = 1)] + pub view_id: String, + #[pb(index = 2, one_of)] + pub publish_name: Option, +} + +#[derive(Default, ProtoBuf)] +pub struct UnpublishViewsPayloadPB { + #[pb(index = 1)] + pub view_ids: Vec, +} + +#[derive(Default, ProtoBuf)] +pub struct PublishInfoResponsePB { + #[pb(index = 1)] + pub view_id: String, + #[pb(index = 2)] + pub publish_name: String, + #[pb(index = 3, one_of)] + pub namespace: Option, +} + +impl From for PublishInfoResponsePB { + fn from(info: PublishInfoResponse) -> Self { + Self { + view_id: info.view_id, + publish_name: info.publish_name, + namespace: info.namespace, + } + } +} + +#[derive(Default, ProtoBuf)] +pub struct SetPublishNamespacePayloadPB { + #[pb(index = 1)] + pub new_namespace: String, +} + +#[derive(Default, ProtoBuf)] +pub struct PublishNamespacePB { + #[pb(index = 1)] + pub namespace: String, +} diff --git a/frontend/rust-lib/flowy-folder/src/event_handler.rs b/frontend/rust-lib/flowy-folder/src/event_handler.rs index 03b71fd2a9..690bf6940e 100644 --- a/frontend/rust-lib/flowy-folder/src/event_handler.rs +++ b/frontend/rust-lib/flowy-folder/src/event_handler.rs @@ -394,3 +394,58 @@ pub(crate) async fn update_view_visibility_status_handler( folder.set_views_visibility(params.view_ids, params.is_public); Ok(()) } + +#[tracing::instrument(level = "debug", skip(data, folder), err)] +pub(crate) async fn publish_view_handler( + data: AFPluginData, + folder: AFPluginState>, +) -> Result<(), FlowyError> { + let folder = upgrade_folder(folder)?; + let params = data.into_inner(); + folder + .publish_view(params.view_id.as_str(), params.publish_name) + .await?; + Ok(()) +} + +#[tracing::instrument(level = "debug", skip(data, folder), err)] +pub(crate) async fn unpublish_views_handler( + data: AFPluginData, + folder: AFPluginState>, +) -> Result<(), FlowyError> { + let folder = upgrade_folder(folder)?; + let params = data.into_inner(); + folder.unpublish_views(params.view_ids).await?; + Ok(()) +} + +#[tracing::instrument(level = "debug", skip(data, folder), err)] +pub(crate) async fn get_publish_info_handler( + data: AFPluginData, + folder: AFPluginState>, +) -> DataResult { + let folder = upgrade_folder(folder)?; + let view_id = data.into_inner().value; + let info = folder.get_publish_info(&view_id).await?; + data_result_ok(PublishInfoResponsePB::from(info)) +} + +#[tracing::instrument(level = "debug", skip(data, folder), err)] +pub(crate) async fn set_publish_namespace_handler( + data: AFPluginData, + folder: AFPluginState>, +) -> Result<(), FlowyError> { + let folder = upgrade_folder(folder)?; + let namespace = data.into_inner().new_namespace; + folder.set_publish_namespace(namespace).await?; + Ok(()) +} + +#[tracing::instrument(level = "debug", skip(folder), err)] +pub(crate) async fn get_publish_namespace_handler( + folder: AFPluginState>, +) -> DataResult { + let folder = upgrade_folder(folder)?; + let namespace = folder.get_publish_namespace().await?; + data_result_ok(PublishNamespacePB { namespace }) +} diff --git a/frontend/rust-lib/flowy-folder/src/event_map.rs b/frontend/rust-lib/flowy-folder/src/event_map.rs index aa8bf551a4..d13c16b435 100644 --- a/frontend/rust-lib/flowy-folder/src/event_map.rs +++ b/frontend/rust-lib/flowy-folder/src/event_map.rs @@ -42,6 +42,11 @@ pub fn init(folder: Weak) -> AFPlugin { .event(FolderEvent::ReadCurrentWorkspaceViews, get_current_workspace_views_handler) .event(FolderEvent::UpdateViewVisibilityStatus, update_view_visibility_status_handler) .event(FolderEvent::GetViewAncestors, get_view_ancestors_handler) + .event(FolderEvent::PublishView, publish_view_handler) + .event(FolderEvent::GetPublishInfo, get_publish_info_handler) + .event(FolderEvent::UnpublishViews, unpublish_views_handler) + .event(FolderEvent::SetPublishNamespace, set_publish_namespace_handler) + .event(FolderEvent::GetPublishNamespace, get_publish_namespace_handler) } #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] @@ -176,4 +181,19 @@ pub enum FolderEvent { /// Return the ancestors of the view #[event(input = "ViewIdPB", output = "RepeatedViewPB")] GetViewAncestors = 42, + + #[event(input = "PublishViewParamsPB")] + PublishView = 43, + + #[event(input = "ViewIdPB", output = "PublishInfoResponsePB")] + GetPublishInfo = 44, + + #[event(output = "PublishNamespacePB")] + GetPublishNamespace = 45, + + #[event(input = "SetPublishNamespacePayloadPB")] + SetPublishNamespace = 46, + + #[event(input = "UnpublishViewsPayloadPB")] + UnpublishViews = 47, } diff --git a/frontend/rust-lib/flowy-folder/src/lib.rs b/frontend/rust-lib/flowy-folder/src/lib.rs index bc927d20c7..55d447bdc7 100644 --- a/frontend/rust-lib/flowy-folder/src/lib.rs +++ b/frontend/rust-lib/flowy-folder/src/lib.rs @@ -14,6 +14,7 @@ mod manager_observer; #[cfg(debug_assertions)] pub mod manager_test_util; +pub mod publish_util; pub mod share; #[cfg(feature = "test_helper")] mod test_helper; diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index 28d802b6db..1e158dbad5 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -12,6 +12,7 @@ use crate::manager_observer::{ use crate::notification::{ send_notification, send_workspace_setting_notification, FolderNotification, }; +use crate::publish_util::{generate_publish_name, view_pb_to_publish_view}; use crate::share::ImportParams; use crate::util::{ folder_not_init_error, insert_parent_child_views, workspace_data_not_sync_error, @@ -28,9 +29,13 @@ use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabBuilderConfi use collab_integrate::CollabKVDB; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_folder_pub::cloud::{gen_view_id, FolderCloudService}; +use flowy_folder_pub::entities::{ + PublishInfoResponse, PublishViewInfo, PublishViewMeta, PublishViewMetaData, PublishViewPayload, +}; use flowy_folder_pub::folder_builder::ParentChildViews; use flowy_search_pub::entities::FolderIndexManager; use flowy_sqlite::kv::StorePreferences; +use futures::future; use parking_lot::RwLock; use std::fmt::{Display, Formatter}; use std::ops::Deref; @@ -936,6 +941,192 @@ impl FolderManager { Ok(()) } + /// The view will be published to the web with the specified view id. + /// The [publish_name] is the [view name] + [view id] when currently published + #[tracing::instrument(level = "debug", skip(self), err)] + pub async fn publish_view(&self, view_id: &str, publish_name: Option) -> FlowyResult<()> { + let view = self + .with_folder(|| None, |folder| folder.views.get_view(view_id)) + .ok_or_else(|| FlowyError::record_not_found().with_context("Can't find the view"))?; + + let layout = view.layout.clone(); + + if layout != ViewLayout::Document { + return Err(FlowyError::new( + ErrorCode::NotSupportYet, + "Only document view can be published".to_string(), + )); + } + + // Get the view payload and its child views recursively + let payload = self + .get_batch_publish_payload(view_id, publish_name) + .await?; + + let workspace_id = self.user.workspace_id()?; + self + .cloud_service + .publish_view(workspace_id.as_str(), payload) + .await?; + Ok(()) + } + + #[tracing::instrument(level = "debug", skip(self), err)] + pub async fn unpublish_views(&self, view_ids: Vec) -> FlowyResult<()> { + let workspace_id = self.user.workspace_id()?; + self + .cloud_service + .unpublish_views(workspace_id.as_str(), view_ids) + .await?; + Ok(()) + } + + #[tracing::instrument(level = "debug", skip(self), err)] + pub async fn get_publish_info(&self, view_id: &str) -> FlowyResult { + let publish_info = self.cloud_service.get_publish_info(view_id).await?; + Ok(publish_info) + } + + #[tracing::instrument(level = "debug", skip(self), err)] + pub async fn set_publish_namespace(&self, namespace: String) -> FlowyResult<()> { + let workspace_id = self.user.workspace_id()?; + self + .cloud_service + .set_publish_namespace(workspace_id.as_str(), namespace.as_str()) + .await?; + Ok(()) + } + + #[tracing::instrument(level = "debug", skip(self), err)] + pub async fn get_publish_namespace(&self) -> FlowyResult { + let workspace_id = self.user.workspace_id()?; + let namespace = self + .cloud_service + .get_publish_namespace(workspace_id.as_str()) + .await?; + Ok(namespace) + } + + /// Get the publishing payload of the view with the given view id. + /// The publishing payload contains the view data and its child views(not recursively). + pub async fn get_batch_publish_payload( + &self, + view_id: &str, + publish_name: Option, + ) -> FlowyResult> { + let mut stack = vec![view_id.to_string()]; + let mut payloads = Vec::new(); + + while let Some(current_view_id) = stack.pop() { + let view = match self.get_view_pb(¤t_view_id).await { + Ok(view) => view, + Err(_) => continue, + }; + + // Only document view can be published + let layout = if view.layout == ViewLayoutPB::Document { + ViewLayout::Document + } else { + continue; + }; + + // Only support set the publish_name for the current view, not for the child views + let publish_name = if current_view_id == view_id { + publish_name.clone() + } else { + None + }; + + let payload = self + .get_publish_payload(¤t_view_id, publish_name, layout) + .await; + + if let Ok(payload) = payload { + payloads.push(payload); + } + + // Add the child views to the stack + for child in &view.child_views { + stack.push(child.id.clone()); + } + } + + Ok(payloads) + } + + async fn build_publish_views(&self, view_id: &str) -> Option { + let view_pb = self.get_view_pb(view_id).await.ok()?; + + let mut child_views_futures = vec![]; + + for child in &view_pb.child_views { + let future = self.build_publish_views(&child.id); + child_views_futures.push(future); + } + + let child_views = future::join_all(child_views_futures) + .await + .into_iter() + .flatten() + .collect::>(); + + let view_child_views = if child_views.is_empty() { + None + } else { + Some(child_views) + }; + + let view = view_pb_to_publish_view(&view_pb); + + let view = PublishViewInfo { + child_views: view_child_views, + ..view + }; + + Some(view) + } + async fn get_publish_payload( + &self, + view_id: &str, + publish_name: Option, + layout: ViewLayout, + ) -> FlowyResult { + let handler = self.get_handler(&layout)?; + let encoded_collab = handler.encoded_collab_v1(view_id, layout).await?; + let view = self + .with_folder(|| None, |folder| folder.views.get_view(view_id)) + .ok_or_else(|| FlowyError::record_not_found().with_context("Can't find the view"))?; + let publish_name = publish_name.unwrap_or_else(|| generate_publish_name(&view.id, &view.name)); + + let child_views = self + .build_publish_views(view_id) + .await + .map(|v| v.child_views.map_or(vec![], |c| c)) + .map_or(vec![], |c| c); + + let ancestor_views = self + .get_view_ancestors_pb(view_id) + .await? + .iter() + .map(view_pb_to_publish_view) + .collect::>(); + + let view_pb = self.get_view_pb(view_id).await?; + let metadata = PublishViewMetaData { + view: view_pb_to_publish_view(&view_pb), + child_views, + ancestor_views, + }; + let meta = PublishViewMeta { + view_id: view.id.clone(), + publish_name, + metadata, + }; + + let data = Vec::from(encoded_collab.doc_state); + Ok(PublishViewPayload { meta, data }) + } + // Used by toggle_favorites to send notification to frontend, after the favorite status of view has been changed.It sends two distinct notifications: one to correctly update the concerned view's is_favorite status, and another to update the list of favorites that is to be displayed. async fn send_toggle_favorite_notification(&self, view_id: &str) { if let Ok(view) = self.get_view_pb(view_id).await { diff --git a/frontend/rust-lib/flowy-folder/src/publish_util.rs b/frontend/rust-lib/flowy-folder/src/publish_util.rs new file mode 100644 index 0000000000..09a14b7287 --- /dev/null +++ b/frontend/rust-lib/flowy-folder/src/publish_util.rs @@ -0,0 +1,33 @@ +use crate::entities::ViewPB; +use flowy_folder_pub::entities::PublishViewInfo; +use regex::Regex; + +fn replace_invalid_url_chars(input: &str) -> String { + let re = Regex::new(r"[^\w-]").unwrap(); + + let replaced = re.replace_all(input, "_").to_string(); + if replaced.len() > 20 { + replaced[..20].to_string() + } else { + replaced + } +} +pub fn generate_publish_name(id: &str, name: &str) -> String { + let name = replace_invalid_url_chars(name); + format!("{}-{}", name, id) +} + +pub fn view_pb_to_publish_view(view: &ViewPB) -> PublishViewInfo { + PublishViewInfo { + view_id: view.id.clone(), + name: view.name.clone(), + layout: view.layout.clone().into(), + icon: view.icon.clone().map(|icon| icon.into()), + child_views: None, + extra: view.extra.clone(), + created_by: view.created_by, + last_edited_by: view.last_edited_by, + last_edited_time: view.last_edited, + created_at: view.create_time, + } +} diff --git a/frontend/rust-lib/flowy-folder/src/view_operation.rs b/frontend/rust-lib/flowy-folder/src/view_operation.rs index 3132384e77..dc3f31ef48 100644 --- a/frontend/rust-lib/flowy-folder/src/view_operation.rs +++ b/frontend/rust-lib/flowy-folder/src/view_operation.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::sync::Arc; use bytes::Bytes; +use collab::entity::EncodedCollab; pub use collab_folder::View; use collab_folder::ViewLayout; @@ -45,6 +46,12 @@ pub trait FolderOperationHandler { /// Returns the [ViewData] that can be used to create the same view. fn duplicate_view(&self, view_id: &str) -> FutureResult; + fn encoded_collab_v1( + &self, + view_id: &str, + layout: ViewLayout, + ) -> FutureResult; + /// Create a view with the data. /// /// # Arguments diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs index fe58f3fc16..7468c163fd 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs @@ -1,6 +1,7 @@ use anyhow::Error; use client_api::entity::{ - workspace_dto::CreateWorkspaceParam, CollabParams, QueryCollab, QueryCollabParams, + workspace_dto::CreateWorkspaceParam, CollabParams, PublishCollabItem, PublishCollabMetadata, + QueryCollab, QueryCollabParams, }; use collab::core::collab::DataSource; use collab::core::origin::CollabOrigin; @@ -8,12 +9,14 @@ use collab_entity::CollabType; use collab_folder::RepeatedViewIdentifier; use std::sync::Arc; use tracing::instrument; +use uuid::Uuid; -use flowy_error::FlowyError; +use flowy_error::{ErrorCode, FlowyError}; use flowy_folder_pub::cloud::{ Folder, FolderCloudService, FolderCollabParams, FolderData, FolderSnapshot, Workspace, WorkspaceRecord, }; +use flowy_folder_pub::entities::{PublishInfoResponse, PublishViewPayload}; use lib_infra::future::FutureResult; use crate::af_cloud::define::ServerUser; @@ -180,4 +183,95 @@ where fn service_name(&self) -> String { "AppFlowy Cloud".to_string() } + + fn publish_view( + &self, + workspace_id: &str, + payload: Vec, + ) -> FutureResult<(), Error> { + let workspace_id = workspace_id.to_string(); + let try_get_client = self.inner.try_get_client(); + FutureResult::new(async move { + let params = payload + .into_iter() + .map(|object| PublishCollabItem { + meta: PublishCollabMetadata { + view_id: Uuid::parse_str(object.meta.view_id.as_str()).unwrap_or(Uuid::nil()), + publish_name: object.meta.publish_name, + metadata: object.meta.metadata, + }, + data: object.data, + }) + .collect::>(); + try_get_client? + .publish_collabs(&workspace_id, params) + .await + .map_err(FlowyError::from)?; + Ok(()) + }) + } + + fn unpublish_views(&self, workspace_id: &str, view_ids: Vec) -> FutureResult<(), Error> { + let workspace_id = workspace_id.to_string(); + let try_get_client = self.inner.try_get_client(); + let view_uuids = view_ids + .iter() + .map(|id| Uuid::parse_str(id).unwrap_or(Uuid::nil())) + .collect::>(); + FutureResult::new(async move { + try_get_client? + .unpublish_collabs(&workspace_id, &view_uuids) + .await + .map_err(FlowyError::from)?; + Ok(()) + }) + } + + fn get_publish_info(&self, view_id: &str) -> FutureResult { + let try_get_client = self.inner.try_get_client(); + let view_id = Uuid::parse_str(view_id) + .map_err(|_| FlowyError::new(ErrorCode::InvalidParams, "Invalid view id")); + + FutureResult::new(async move { + let view_id = view_id?; + let info = try_get_client? + .get_published_collab_info(&view_id) + .await + .map_err(FlowyError::from)?; + Ok(PublishInfoResponse { + view_id: info.view_id.to_string(), + publish_name: info.publish_name, + namespace: info.namespace, + }) + }) + } + + fn set_publish_namespace( + &self, + workspace_id: &str, + new_namespace: &str, + ) -> FutureResult<(), Error> { + let workspace_id = workspace_id.to_string(); + let namespace = new_namespace.to_string(); + let try_get_client = self.inner.try_get_client(); + FutureResult::new(async move { + try_get_client? + .set_workspace_publish_namespace(&workspace_id, &namespace) + .await + .map_err(FlowyError::from)?; + Ok(()) + }) + } + + fn get_publish_namespace(&self, workspace_id: &str) -> FutureResult { + let workspace_id = workspace_id.to_string(); + let try_get_client = self.inner.try_get_client(); + FutureResult::new(async move { + let namespace = try_get_client? + .get_workspace_publish_namespace(&workspace_id) + .await + .map_err(FlowyError::from)?; + Ok(namespace) + }) + } } diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs index ea0ee027b9..5c3a5464ed 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs @@ -7,6 +7,7 @@ use flowy_folder_pub::cloud::{ gen_workspace_id, FolderCloudService, FolderCollabParams, FolderData, FolderSnapshot, Workspace, WorkspaceRecord, }; +use flowy_folder_pub::entities::{PublishInfoResponse, PublishViewPayload}; use lib_infra::future::FutureResult; use crate::local_server::LocalServerDB; @@ -77,4 +78,48 @@ impl FolderCloudService for LocalServerFolderCloudServiceImpl { fn service_name(&self) -> String { "Local".to_string() } + + fn publish_view( + &self, + _workspace_id: &str, + _payload: Vec, + ) -> FutureResult<(), Error> { + FutureResult::new(async { Err(anyhow!("Local server doesn't support publish view")) }) + } + + fn unpublish_views( + &self, + _workspace_id: &str, + _view_ids: Vec, + ) -> FutureResult<(), Error> { + FutureResult::new(async { Err(anyhow!("Local server doesn't support unpublish views")) }) + } + + fn get_publish_info(&self, _view_id: &str) -> FutureResult { + FutureResult::new(async move { + Err(anyhow!( + "Local server doesn't support get publish info from remote" + )) + }) + } + + fn set_publish_namespace( + &self, + _workspace_id: &str, + _new_namespace: &str, + ) -> FutureResult<(), Error> { + FutureResult::new(async { + Err(anyhow!( + "Local server doesn't support set publish namespace" + )) + }) + } + + fn get_publish_namespace(&self, _workspace_id: &str) -> FutureResult { + FutureResult::new(async { + Err(anyhow!( + "Local server doesn't support get publish namespace" + )) + }) + } } diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs b/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs index ca0957c375..828c0fedbb 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs @@ -13,6 +13,7 @@ use flowy_folder_pub::cloud::{ gen_workspace_id, Folder, FolderCloudService, FolderCollabParams, FolderData, FolderSnapshot, Workspace, WorkspaceRecord, }; +use flowy_folder_pub::entities::{PublishInfoResponse, PublishViewPayload}; use lib_dispatch::prelude::af_spawn; use lib_infra::future::FutureResult; use lib_infra::util::timestamp; @@ -174,6 +175,46 @@ where fn service_name(&self) -> String { "Supabase".to_string() } + + fn publish_view( + &self, + _workspace_id: &str, + _payload: Vec, + ) -> FutureResult<(), Error> { + FutureResult::new(async { Err(anyhow!("supabase server doesn't support publish view")) }) + } + + fn unpublish_views( + &self, + _workspace_id: &str, + _view_ids: Vec, + ) -> FutureResult<(), Error> { + FutureResult::new(async { Err(anyhow!("supabase server doesn't support unpublish views")) }) + } + + fn get_publish_info(&self, _view_id: &str) -> FutureResult { + FutureResult::new(async { Err(anyhow!("supabase server doesn't support publish info")) }) + } + + fn set_publish_namespace( + &self, + _workspace_id: &str, + _new_namespace: &str, + ) -> FutureResult<(), Error> { + FutureResult::new(async { + Err(anyhow!( + "supabase server doesn't support set publish namespace" + )) + }) + } + + fn get_publish_namespace(&self, _workspace_id: &str) -> FutureResult { + FutureResult::new(async { + Err(anyhow!( + "supabase server doesn't support get publish namespace" + )) + }) + } } fn workspace_from_json_value(value: Value) -> Result {