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,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(¤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