feat: support publish view and unpublish views

This commit is contained in:
qinluhe
2024-06-21 00:00:47 +08:00
parent 63fe1a6ef3
commit dbcf8ba86f
28 changed files with 918 additions and 33 deletions

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,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<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.
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