mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support publish view and unpublish views
This commit is contained in:
@ -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
|
||||
|
@ -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(),
|
||||
|
@ -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::*;
|
||||
|
48
frontend/rust-lib/flowy-folder/src/entities/publish.rs
Normal file
48
frontend/rust-lib/flowy-folder/src/entities/publish.rs
Normal 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,
|
||||
}
|
@ -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 })
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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(¤t_view_id).await {
|
||||
Ok(view) => view,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
// Only document view can be published
|
||||
let layout = if view.layout == ViewLayoutPB::Document {
|
||||
ViewLayout::Document
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Only support set the publish_name for the current view, not for the child views
|
||||
let publish_name = if current_view_id == view_id {
|
||||
publish_name.clone()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let payload = self
|
||||
.get_publish_payload(¤t_view_id, publish_name, layout)
|
||||
.await;
|
||||
|
||||
if let Ok(payload) = payload {
|
||||
payloads.push(payload);
|
||||
}
|
||||
|
||||
// Add the child views to the stack
|
||||
for child in &view.child_views {
|
||||
stack.push(child.id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(payloads)
|
||||
}
|
||||
|
||||
async fn build_publish_views(&self, view_id: &str) -> Option<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 {
|
||||
|
33
frontend/rust-lib/flowy-folder/src/publish_util.rs
Normal file
33
frontend/rust-lib/flowy-folder/src/publish_util.rs
Normal 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,
|
||||
}
|
||||
}
|
@ -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
|
||||
|
Reference in New Issue
Block a user