feat: support publish view and unpublish views

This commit is contained in:
qinluhe 2024-06-21 00:00:47 +08:00
parent 63fe1a6ef3
commit 77a53ef67a
28 changed files with 927 additions and 35 deletions

View File

@ -52,7 +52,7 @@ collab-user = { version = "0.2" }
# Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "6262816043efeede8823d7a7ea252083adf407e9" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "fa7565d9de2a5e65e3886c8230588d58140637c5" }
[dependencies]
serde_json.workspace = true

View File

@ -54,7 +54,7 @@ yrs = "0.18.8"
# Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "6262816043efeede8823d7a7ea252083adf407e9" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "fa7565d9de2a5e65e3886c8230588d58140637c5" }
[profile.dev]
opt-level = 0

View File

@ -52,7 +52,7 @@ collab-user = { version = "0.2" }
# Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "6262816043efeede8823d7a7ea252083adf407e9" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "fa7565d9de2a5e65e3886c8230588d58140637c5" }
[dependencies]
serde_json.workspace = true

View File

@ -163,7 +163,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=fa7565d9de2a5e65e3886c8230588d58140637c5#fa7565d9de2a5e65e3886c8230588d58140637c5"
dependencies = [
"anyhow",
"bincode",
@ -183,7 +183,7 @@ dependencies = [
[[package]]
name = "appflowy-ai-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=fa7565d9de2a5e65e3886c8230588d58140637c5#fa7565d9de2a5e65e3886c8230588d58140637c5"
dependencies = [
"anyhow",
"bytes",
@ -664,7 +664,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=fa7565d9de2a5e65e3886c8230588d58140637c5#fa7565d9de2a5e65e3886c8230588d58140637c5"
dependencies = [
"again",
"anyhow",
@ -679,6 +679,7 @@ dependencies = [
"collab",
"collab-rt-entity",
"collab-rt-protocol",
"futures",
"futures-core",
"futures-util",
"getrandom 0.2.10",
@ -686,6 +687,8 @@ dependencies = [
"infra",
"mime",
"parking_lot 0.12.1",
"percent-encoding",
"pin-project",
"prost",
"reqwest",
"scraper 0.17.1",
@ -710,7 +713,7 @@ dependencies = [
[[package]]
name = "client-api-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=fa7565d9de2a5e65e3886c8230588d58140637c5#fa7565d9de2a5e65e3886c8230588d58140637c5"
dependencies = [
"collab-entity",
"collab-rt-entity",
@ -722,7 +725,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=fa7565d9de2a5e65e3886c8230588d58140637c5#fa7565d9de2a5e65e3886c8230588d58140637c5"
dependencies = [
"futures-channel",
"futures-util",
@ -931,7 +934,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=fa7565d9de2a5e65e3886c8230588d58140637c5#fa7565d9de2a5e65e3886c8230588d58140637c5"
dependencies = [
"anyhow",
"bincode",
@ -956,7 +959,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=fa7565d9de2a5e65e3886c8230588d58140637c5#fa7565d9de2a5e65e3886c8230588d58140637c5"
dependencies = [
"anyhow",
"async-trait",
@ -1276,7 +1279,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=fa7565d9de2a5e65e3886c8230588d58140637c5#fa7565d9de2a5e65e3886c8230588d58140637c5"
dependencies = [
"anyhow",
"app-error",
@ -1988,12 +1991,14 @@ dependencies = [
"flowy-notification",
"flowy-search-pub",
"flowy-sqlite",
"futures",
"lazy_static",
"lib-dispatch",
"lib-infra",
"nanoid",
"parking_lot 0.12.1",
"protobuf",
"regex",
"serde",
"serde_json",
"strum_macros 0.21.1",
@ -2014,6 +2019,7 @@ dependencies = [
"collab-entity",
"collab-folder",
"lib-infra",
"serde",
"tokio",
"uuid",
]
@ -2567,7 +2573,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=fa7565d9de2a5e65e3886c8230588d58140637c5#fa7565d9de2a5e65e3886c8230588d58140637c5"
dependencies = [
"anyhow",
"futures-util",
@ -2584,7 +2590,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=fa7565d9de2a5e65e3886c8230588d58140637c5#fa7565d9de2a5e65e3886c8230588d58140637c5"
dependencies = [
"anyhow",
"app-error",
@ -2949,7 +2955,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=fa7565d9de2a5e65e3886c8230588d58140637c5#fa7565d9de2a5e65e3886c8230588d58140637c5"
dependencies = [
"anyhow",
"bytes",
@ -5035,7 +5041,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6262816043efeede8823d7a7ea252083adf407e9#6262816043efeede8823d7a7ea252083adf407e9"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=fa7565d9de2a5e65e3886c8230588d58140637c5#fa7565d9de2a5e65e3886c8230588d58140637c5"
dependencies = [
"anyhow",
"app-error",

View File

@ -97,8 +97,8 @@ validator = { version = "0.16.1", features = ["derive"] }
# Run the script.add_workspace_members:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "6262816043efeede8823d7a7ea252083adf407e9" }
client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "6262816043efeede8823d7a7ea252083adf407e9" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "fa7565d9de2a5e65e3886c8230588d58140637c5" }
client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "fa7565d9de2a5e65e3886c8230588d58140637c5" }
[profile.dev]
opt-level = 1

View File

@ -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, AddWorkspaceMemberPB, QueryWorkspacePB, RemoveWorkspaceMemberPB,
@ -165,6 +167,24 @@ impl EventIntegrationTest {
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> {
EventBuilder::new(self.clone())
.event(FolderEvent::ReadCurrentWorkspaceViews)

View File

@ -3,3 +3,4 @@ mod import_test;
mod script;
mod subscription_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 collab::entity::EncodedCollab;
use collab_integrate::collab_builder::AppFlowyCollabBuilder;
use collab_integrate::CollabKVDB;
use flowy_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.
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<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> {
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<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::{
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};
@ -294,6 +295,65 @@ impl FolderCloudService for ServerProvider {
.map(|provider| provider.folder_service().service_name())
.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 {

View File

@ -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;
@ -75,8 +75,7 @@ impl DocumentManager {
}
}
/// In order to support the requirement of automatically publishing sub-documents in publishing requirements,
/// we need to read binary data from disk instead of reading from the open document.
/// Get the encoded collab of the document.
pub async fn encode_collab(&self, doc_id: &str) -> FlowyResult<EncodedCollab> {
let doc_state = DataSource::Disk;
let uid = self.user_service.user_id()?;
@ -85,7 +84,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<()> {

View File

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

View File

@ -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<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)]

View File

@ -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,46 @@ 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<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,
/// The doc_state of the encoded collab.
pub data: Vec<u8>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PublishInfoResponse {
pub view_id: String,
/// one part of publish url: /{namespace}/{publish_name}
pub publish_name: String,
pub namespace: Option<String>,
}

View File

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

View File

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

View File

@ -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::*;

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);
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::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,
}

View File

@ -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;

View File

@ -2,8 +2,8 @@ use crate::entities::icon::UpdateViewIconParams;
use crate::entities::{
view_pb_with_child_views, view_pb_without_child_views, view_pb_without_child_views_from_arc,
CreateViewParams, CreateWorkspaceParams, DeletedViewPB, FolderSnapshotPB, MoveNestedViewParams,
RepeatedTrashPB, RepeatedViewIdPB, RepeatedViewPB, UpdateViewParams, ViewPB, ViewSectionPB,
WorkspacePB, WorkspaceSettingPB,
RepeatedTrashPB, RepeatedViewIdPB, RepeatedViewPB, UpdateViewParams, ViewLayoutPB, ViewPB,
ViewSectionPB, WorkspacePB, WorkspaceSettingPB,
};
use crate::manager_observer::{
notify_child_views_changed, notify_did_update_workspace, notify_parent_view_did_change,
@ -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;
@ -863,6 +868,198 @@ impl FolderManager {
Ok(())
}
/// Publish the view with the given view id.
/// [publish_name] is one part of the URL of the published view. if it is None, the default publish name will be used. The default publish name is generated by the view id and view name.
#[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(())
}
/// Unpublish the view with the given view id.
#[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(())
}
/// Get the publish info of the view with the given view id.
/// The publish info contains the namespace and publish_name of the view.
#[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)
}
/// Get the namespace of the current workspace.
/// The namespace is used to generate the URL of the published view.
#[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(())
}
/// Get the namespace of the current workspace.
#[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.
async fn send_toggle_favorite_notification(&self, view_id: &str) {
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 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<ViewData, FlowyError>;
fn encoded_collab_v1(
&self,
view_id: &str,
layout: ViewLayout,
) -> FutureResult<EncodedCollab, FlowyError>;
/// Create a view with the data.
///
/// # Arguments

View File

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