feat: support publish view and unpublish views

This commit is contained in:
qinluhe 2024-06-21 00:00:47 +08:00 committed by Kilu
parent 4151c48180
commit 1c8913eb79
22 changed files with 884 additions and 5 deletions

View File

@ -1,3 +1,4 @@
use collab::entity::EncodedCollab;
use std::sync::Arc; use std::sync::Arc;
use collab_folder::{FolderData, View}; 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::event_map::FolderEvent::*; use flowy_folder::event_map::FolderEvent::*;
use flowy_folder::{entities::*, ViewLayout}; use flowy_folder::{entities::*, ViewLayout};
use flowy_folder_pub::entities::PublishViewPayload;
use flowy_search::services::manager::{SearchHandler, SearchType}; use flowy_search::services::manager::{SearchHandler, SearchType};
use flowy_user::entities::{ use flowy_user::entities::{
AcceptWorkspaceInvitationPB, QueryWorkspacePB, RemoveWorkspaceMemberPB, AcceptWorkspaceInvitationPB, QueryWorkspacePB, RemoveWorkspaceMemberPB,
@ -172,6 +174,24 @@ impl EventIntegrationTest {
folder.get_folder_data(&workspace_id).clone().unwrap() folder.get_folder_data(&workspace_id).clone().unwrap()
} }
pub async fn get_publish_payload(&self, view_id: &str) -> Vec<PublishViewPayload> {
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<ViewPB> { pub async fn get_all_workspace_views(&self) -> Vec<ViewPB> {
EventBuilder::new(self.clone()) EventBuilder::new(self.clone())
.event(FolderEvent::ReadCurrentWorkspaceViews) .event(FolderEvent::ReadCurrentWorkspaceViews)

View File

@ -3,3 +3,4 @@ mod import_test;
mod script; mod script;
mod subscription_test; mod subscription_test;
mod test; mod test;
mod view_publish_test;

View File

@ -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<PublishViewPayload> {
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<PublishViewPayload> {
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);
}

View File

@ -1,4 +1,5 @@
use bytes::Bytes; use bytes::Bytes;
use collab::entity::EncodedCollab;
use collab_integrate::collab_builder::AppFlowyCollabBuilder; use collab_integrate::collab_builder::AppFlowyCollabBuilder;
use collab_integrate::CollabKVDB; use collab_integrate::CollabKVDB;
use flowy_chat::chat_manager::ChatManager; 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<EncodedCollab, FlowyError> {
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. /// Create a view with built-in data.
fn create_built_in_view( fn create_built_in_view(
&self, &self,
@ -287,6 +303,15 @@ impl FolderOperationHandler for DatabaseFolderOperation {
}) })
} }
fn encoded_collab_v1(
&self,
_view_id: &str,
_layout: ViewLayout,
) -> FutureResult<EncodedCollab, FlowyError> {
// Database view doesn't support collab
FutureResult::new(async move { Err(FlowyError::not_support()) })
}
fn duplicate_view(&self, view_id: &str) -> FutureResult<Bytes, FlowyError> { fn duplicate_view(&self, view_id: &str) -> FutureResult<Bytes, FlowyError> {
let database_manager = self.0.clone(); let database_manager = self.0.clone();
let view_id = view_id.to_owned(); let view_id = view_id.to_owned();
@ -547,4 +572,13 @@ impl FolderOperationHandler for ChatFolderOperation {
) -> FutureResult<(), FlowyError> { ) -> FutureResult<(), FlowyError> {
FutureResult::new(async move { Err(FlowyError::not_support()) }) FutureResult::new(async move { Err(FlowyError::not_support()) })
} }
fn encoded_collab_v1(
&self,
_view_id: &str,
_layout: ViewLayout,
) -> FutureResult<EncodedCollab, FlowyError> {
// Chat view doesn't support collab
FutureResult::new(async move { Err(FlowyError::not_support()) })
}
} }

View File

@ -31,6 +31,7 @@ use flowy_error::{FlowyError, FlowyResult};
use flowy_folder_pub::cloud::{ use flowy_folder_pub::cloud::{
FolderCloudService, FolderCollabParams, FolderData, FolderSnapshot, Workspace, WorkspaceRecord, 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::af_cloud_config::AFCloudConfiguration;
use flowy_server_pub::supabase_config::SupabaseConfiguration; use flowy_server_pub::supabase_config::SupabaseConfiguration;
use flowy_storage_pub::cloud::{ObjectIdentity, ObjectValue, StorageCloudService}; use flowy_storage_pub::cloud::{ObjectIdentity, ObjectValue, StorageCloudService};
@ -301,6 +302,65 @@ impl FolderCloudService for ServerProvider {
.map(|provider| provider.folder_service().service_name()) .map(|provider| provider.folder_service().service_name())
.unwrap_or_default() .unwrap_or_default()
} }
fn publish_view(
&self,
workspace_id: &str,
payload: Vec<PublishViewPayload>,
) -> 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<String>) -> 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<PublishInfoResponse, Error> {
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<String, Error> {
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 { impl DatabaseCloudService for ServerProvider {

View File

@ -11,8 +11,8 @@ use collab_document::document_awareness::DocumentAwarenessState;
use collab_document::document_awareness::DocumentAwarenessUser; use collab_document::document_awareness::DocumentAwarenessUser;
use collab_document::document_data::default_document_data; use collab_document::document_data::default_document_data;
use collab_entity::CollabType; use collab_entity::CollabType;
use collab_plugins::CollabKVDB;
use collab_plugins::local_storage::kv::PersistenceError; use collab_plugins::local_storage::kv::PersistenceError;
use collab_plugins::CollabKVDB;
use dashmap::DashMap; use dashmap::DashMap;
use lib_infra::util::timestamp; use lib_infra::util::timestamp;
use tracing::trace; use tracing::trace;
@ -85,7 +85,9 @@ impl DocumentManager {
.await?; .await?;
let collab = collab.lock(); 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<()> { pub async fn initialize(&self, _uid: i64) -> FlowyResult<()> {

View File

@ -12,6 +12,7 @@ collab = { workspace = true }
collab-entity = { workspace = true } collab-entity = { workspace = true }
uuid.workspace = true uuid.workspace = true
anyhow.workspace = true anyhow.workspace = true
serde = { version = "1.0.202", features = ["derive"] }
[dev-dependencies] [dev-dependencies]
tokio.workspace = true tokio.workspace = true

View File

@ -3,6 +3,7 @@ use collab_entity::CollabType;
pub use collab_folder::{Folder, FolderData, Workspace}; pub use collab_folder::{Folder, FolderData, Workspace};
use uuid::Uuid; use uuid::Uuid;
use crate::entities::{PublishInfoResponse, PublishViewPayload};
use lib_infra::future::FutureResult; use lib_infra::future::FutureResult;
/// [FolderCloudService] represents the cloud service for folder. /// [FolderCloudService] represents the cloud service for folder.
@ -44,6 +45,24 @@ pub trait FolderCloudService: Send + Sync + 'static {
) -> FutureResult<(), Error>; ) -> FutureResult<(), Error>;
fn service_name(&self) -> String; fn service_name(&self) -> String;
fn publish_view(
&self,
workspace_id: &str,
payload: Vec<PublishViewPayload>,
) -> FutureResult<(), Error>;
fn unpublish_views(&self, workspace_id: &str, view_ids: Vec<String>) -> FutureResult<(), Error>;
fn get_publish_info(&self, view_id: &str) -> FutureResult<PublishInfoResponse, Error>;
fn set_publish_namespace(
&self,
workspace_id: &str,
new_namespace: &str,
) -> FutureResult<(), Error>;
fn get_publish_namespace(&self, workspace_id: &str) -> FutureResult<String, Error>;
} }
#[derive(Debug)] #[derive(Debug)]

View File

@ -1,4 +1,6 @@
use crate::folder_builder::ParentChildViews; use crate::folder_builder::ParentChildViews;
use collab_folder::{ViewIcon, ViewLayout};
use serde::Serialize;
use std::collections::HashMap; use std::collections::HashMap;
pub enum ImportData { pub enum ImportData {
@ -39,3 +41,44 @@ pub struct SearchData {
/// The data that is stored in the search index row. /// The data that is stored in the search index row.
pub data: String, pub data: String,
} }
#[derive(Serialize, Clone, Debug, Eq, PartialEq)]
pub struct PublishViewInfo {
pub view_id: String,
pub name: String,
pub icon: Option<ViewIcon>,
pub layout: ViewLayout,
pub extra: Option<String>,
pub created_by: Option<i64>,
pub last_edited_by: Option<i64>,
pub last_edited_time: i64,
pub created_at: i64,
pub child_views: Option<Vec<PublishViewInfo>>,
}
#[derive(Serialize, Clone, Debug, Eq, PartialEq)]
pub struct PublishViewMetaData {
pub view: PublishViewInfo,
pub child_views: Vec<PublishViewInfo>,
pub ancestor_views: Vec<PublishViewInfo>,
}
#[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<u8>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PublishInfoResponse {
pub view_id: String,
pub publish_name: String,
pub namespace: Option<String>,
}

View File

@ -39,6 +39,9 @@ serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true serde_json.workspace = true
validator.workspace = true validator.workspace = true
async-trait.workspace = true async-trait.workspace = true
regex = "1.9.5"
futures = "0.3.30"
[build-dependencies] [build-dependencies]
flowy-codegen.workspace = true flowy-codegen.workspace = true

View File

@ -39,7 +39,7 @@ pub struct ViewIconPB {
pub value: String, pub value: String,
} }
impl std::convert::From<ViewIconPB> for ViewIcon { impl From<ViewIconPB> for ViewIcon {
fn from(rev: ViewIconPB) -> Self { fn from(rev: ViewIconPB) -> Self {
ViewIcon { ViewIcon {
ty: rev.ty.into(), ty: rev.ty.into(),

View File

@ -1,12 +1,14 @@
pub mod icon; pub mod icon;
mod import; mod import;
mod parser; mod parser;
pub mod publish;
pub mod trash; pub mod trash;
pub mod view; pub mod view;
pub mod workspace; pub mod workspace;
pub use icon::*; pub use icon::*;
pub use import::*; pub use import::*;
pub use publish::*;
pub use trash::*; pub use trash::*;
pub use view::*; pub use view::*;
pub use workspace::*; pub use workspace::*;

View File

@ -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<String>,
}
#[derive(Default, ProtoBuf)]
pub struct UnpublishViewsPayloadPB {
#[pb(index = 1)]
pub view_ids: Vec<String>,
}
#[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<String>,
}
impl From<PublishInfoResponse> 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,
}

View File

@ -394,3 +394,58 @@ pub(crate) async fn update_view_visibility_status_handler(
folder.set_views_visibility(params.view_ids, params.is_public); folder.set_views_visibility(params.view_ids, params.is_public);
Ok(()) Ok(())
} }
#[tracing::instrument(level = "debug", skip(data, folder), err)]
pub(crate) async fn publish_view_handler(
data: AFPluginData<PublishViewParamsPB>,
folder: AFPluginState<Weak<FolderManager>>,
) -> 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<UnpublishViewsPayloadPB>,
folder: AFPluginState<Weak<FolderManager>>,
) -> 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<ViewIdPB>,
folder: AFPluginState<Weak<FolderManager>>,
) -> DataResult<PublishInfoResponsePB, FlowyError> {
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<SetPublishNamespacePayloadPB>,
folder: AFPluginState<Weak<FolderManager>>,
) -> 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<Weak<FolderManager>>,
) -> DataResult<PublishNamespacePB, FlowyError> {
let folder = upgrade_folder(folder)?;
let namespace = folder.get_publish_namespace().await?;
data_result_ok(PublishNamespacePB { namespace })
}

View File

@ -42,6 +42,11 @@ pub fn init(folder: Weak<FolderManager>) -> AFPlugin {
.event(FolderEvent::ReadCurrentWorkspaceViews, get_current_workspace_views_handler) .event(FolderEvent::ReadCurrentWorkspaceViews, get_current_workspace_views_handler)
.event(FolderEvent::UpdateViewVisibilityStatus, update_view_visibility_status_handler) .event(FolderEvent::UpdateViewVisibilityStatus, update_view_visibility_status_handler)
.event(FolderEvent::GetViewAncestors, get_view_ancestors_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)] #[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 /// Return the ancestors of the view
#[event(input = "ViewIdPB", output = "RepeatedViewPB")] #[event(input = "ViewIdPB", output = "RepeatedViewPB")]
GetViewAncestors = 42, 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,
} }

View File

@ -14,6 +14,7 @@ mod manager_observer;
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
pub mod manager_test_util; pub mod manager_test_util;
pub mod publish_util;
pub mod share; pub mod share;
#[cfg(feature = "test_helper")] #[cfg(feature = "test_helper")]
mod test_helper; mod test_helper;

View File

@ -12,6 +12,7 @@ use crate::manager_observer::{
use crate::notification::{ use crate::notification::{
send_notification, send_workspace_setting_notification, FolderNotification, 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::share::ImportParams;
use crate::util::{ use crate::util::{
folder_not_init_error, insert_parent_child_views, workspace_data_not_sync_error, 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 collab_integrate::CollabKVDB;
use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_error::{ErrorCode, FlowyError, FlowyResult};
use flowy_folder_pub::cloud::{gen_view_id, FolderCloudService}; 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_folder_pub::folder_builder::ParentChildViews;
use flowy_search_pub::entities::FolderIndexManager; use flowy_search_pub::entities::FolderIndexManager;
use flowy_sqlite::kv::StorePreferences; use flowy_sqlite::kv::StorePreferences;
use futures::future;
use parking_lot::RwLock; use parking_lot::RwLock;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::ops::Deref; use std::ops::Deref;
@ -936,6 +941,192 @@ impl FolderManager {
Ok(()) 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<String>) -> 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<String>) -> 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<PublishInfoResponse> {
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<String> {
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<String>,
) -> FlowyResult<Vec<PublishViewPayload>> {
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(&current_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(&current_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<PublishViewInfo> {
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::<Vec<PublishViewInfo>>();
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<String>,
layout: ViewLayout,
) -> FlowyResult<PublishViewPayload> {
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::<Vec<PublishViewInfo>>();
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. // 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) { async fn send_toggle_favorite_notification(&self, view_id: &str) {
if let Ok(view) = self.get_view_pb(view_id).await { if let Ok(view) = self.get_view_pb(view_id).await {

View File

@ -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,
}
}

View File

@ -2,6 +2,7 @@ use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use bytes::Bytes; use bytes::Bytes;
use collab::entity::EncodedCollab;
pub use collab_folder::View; pub use collab_folder::View;
use collab_folder::ViewLayout; use collab_folder::ViewLayout;
@ -45,6 +46,12 @@ pub trait FolderOperationHandler {
/// Returns the [ViewData] that can be used to create the same view. /// Returns the [ViewData] that can be used to create the same view.
fn duplicate_view(&self, view_id: &str) -> FutureResult<ViewData, FlowyError>; fn duplicate_view(&self, view_id: &str) -> FutureResult<ViewData, FlowyError>;
fn encoded_collab_v1(
&self,
view_id: &str,
layout: ViewLayout,
) -> FutureResult<EncodedCollab, FlowyError>;
/// Create a view with the data. /// Create a view with the data.
/// ///
/// # Arguments /// # Arguments

View File

@ -1,6 +1,7 @@
use anyhow::Error; use anyhow::Error;
use client_api::entity::{ 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::collab::DataSource;
use collab::core::origin::CollabOrigin; use collab::core::origin::CollabOrigin;
@ -8,12 +9,14 @@ use collab_entity::CollabType;
use collab_folder::RepeatedViewIdentifier; use collab_folder::RepeatedViewIdentifier;
use std::sync::Arc; use std::sync::Arc;
use tracing::instrument; use tracing::instrument;
use uuid::Uuid;
use flowy_error::FlowyError; use flowy_error::{ErrorCode, FlowyError};
use flowy_folder_pub::cloud::{ use flowy_folder_pub::cloud::{
Folder, FolderCloudService, FolderCollabParams, FolderData, FolderSnapshot, Workspace, Folder, FolderCloudService, FolderCollabParams, FolderData, FolderSnapshot, Workspace,
WorkspaceRecord, WorkspaceRecord,
}; };
use flowy_folder_pub::entities::{PublishInfoResponse, PublishViewPayload};
use lib_infra::future::FutureResult; use lib_infra::future::FutureResult;
use crate::af_cloud::define::ServerUser; use crate::af_cloud::define::ServerUser;
@ -180,4 +183,95 @@ where
fn service_name(&self) -> String { fn service_name(&self) -> String {
"AppFlowy Cloud".to_string() "AppFlowy Cloud".to_string()
} }
fn publish_view(
&self,
workspace_id: &str,
payload: Vec<PublishViewPayload>,
) -> 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::<Vec<_>>();
try_get_client?
.publish_collabs(&workspace_id, params)
.await
.map_err(FlowyError::from)?;
Ok(())
})
}
fn unpublish_views(&self, workspace_id: &str, view_ids: Vec<String>) -> 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::<Vec<_>>();
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<PublishInfoResponse, Error> {
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<String, Error> {
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)
})
}
} }

View File

@ -7,6 +7,7 @@ use flowy_folder_pub::cloud::{
gen_workspace_id, FolderCloudService, FolderCollabParams, FolderData, FolderSnapshot, Workspace, gen_workspace_id, FolderCloudService, FolderCollabParams, FolderData, FolderSnapshot, Workspace,
WorkspaceRecord, WorkspaceRecord,
}; };
use flowy_folder_pub::entities::{PublishInfoResponse, PublishViewPayload};
use lib_infra::future::FutureResult; use lib_infra::future::FutureResult;
use crate::local_server::LocalServerDB; use crate::local_server::LocalServerDB;
@ -77,4 +78,48 @@ impl FolderCloudService for LocalServerFolderCloudServiceImpl {
fn service_name(&self) -> String { fn service_name(&self) -> String {
"Local".to_string() "Local".to_string()
} }
fn publish_view(
&self,
_workspace_id: &str,
_payload: Vec<PublishViewPayload>,
) -> FutureResult<(), Error> {
FutureResult::new(async { Err(anyhow!("Local server doesn't support publish view")) })
}
fn unpublish_views(
&self,
_workspace_id: &str,
_view_ids: Vec<String>,
) -> FutureResult<(), Error> {
FutureResult::new(async { Err(anyhow!("Local server doesn't support unpublish views")) })
}
fn get_publish_info(&self, _view_id: &str) -> FutureResult<PublishInfoResponse, Error> {
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<String, Error> {
FutureResult::new(async {
Err(anyhow!(
"Local server doesn't support get publish namespace"
))
})
}
} }

View File

@ -13,6 +13,7 @@ use flowy_folder_pub::cloud::{
gen_workspace_id, Folder, FolderCloudService, FolderCollabParams, FolderData, FolderSnapshot, gen_workspace_id, Folder, FolderCloudService, FolderCollabParams, FolderData, FolderSnapshot,
Workspace, WorkspaceRecord, Workspace, WorkspaceRecord,
}; };
use flowy_folder_pub::entities::{PublishInfoResponse, PublishViewPayload};
use lib_dispatch::prelude::af_spawn; use lib_dispatch::prelude::af_spawn;
use lib_infra::future::FutureResult; use lib_infra::future::FutureResult;
use lib_infra::util::timestamp; use lib_infra::util::timestamp;
@ -174,6 +175,46 @@ where
fn service_name(&self) -> String { fn service_name(&self) -> String {
"Supabase".to_string() "Supabase".to_string()
} }
fn publish_view(
&self,
_workspace_id: &str,
_payload: Vec<PublishViewPayload>,
) -> FutureResult<(), Error> {
FutureResult::new(async { Err(anyhow!("supabase server doesn't support publish view")) })
}
fn unpublish_views(
&self,
_workspace_id: &str,
_view_ids: Vec<String>,
) -> FutureResult<(), Error> {
FutureResult::new(async { Err(anyhow!("supabase server doesn't support unpublish views")) })
}
fn get_publish_info(&self, _view_id: &str) -> FutureResult<PublishInfoResponse, Error> {
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<String, Error> {
FutureResult::new(async {
Err(anyhow!(
"supabase server doesn't support get publish namespace"
))
})
}
} }
fn workspace_from_json_value(value: Value) -> Result<Workspace, Error> { fn workspace_from_json_value(value: Value) -> Result<Workspace, Error> {