feat: api for publish page to web (#1108)
This commit is contained in:
parent
a61a5c58cc
commit
ab0fa6e7fc
|
|
@ -1,5 +1,5 @@
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use client_api_entity::workspace_dto::PublishInfoView;
|
use client_api_entity::workspace_dto::{PublishInfoView, PublishedView};
|
||||||
use client_api_entity::{workspace_dto::PublishedDuplicate, PublishInfo, UpdatePublishNamespace};
|
use client_api_entity::{workspace_dto::PublishedDuplicate, PublishInfo, UpdatePublishNamespace};
|
||||||
use client_api_entity::{
|
use client_api_entity::{
|
||||||
CreateGlobalCommentParams, CreateReactionParams, DeleteGlobalCommentParams, DeleteReactionParams,
|
CreateGlobalCommentParams, CreateReactionParams, DeleteGlobalCommentParams, DeleteReactionParams,
|
||||||
|
|
@ -301,6 +301,29 @@ impl Client {
|
||||||
.into_data()
|
.into_data()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip_all)]
|
||||||
|
pub async fn get_published_outline(
|
||||||
|
&self,
|
||||||
|
publish_namespace: &str,
|
||||||
|
) -> Result<PublishedView, AppResponseError> {
|
||||||
|
let url = format!(
|
||||||
|
"{}/api/workspace/published-outline/{}",
|
||||||
|
self.base_url, publish_namespace,
|
||||||
|
);
|
||||||
|
|
||||||
|
let resp = self
|
||||||
|
.cloud_client
|
||||||
|
.get(&url)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?;
|
||||||
|
|
||||||
|
log_request_id(&resp);
|
||||||
|
AppResponse::<PublishedView>::from_response(resp)
|
||||||
|
.await?
|
||||||
|
.into_data()
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(level = "debug", skip_all)]
|
#[instrument(level = "debug", skip_all)]
|
||||||
pub async fn get_default_published_collab<T>(
|
pub async fn get_default_published_collab<T>(
|
||||||
&self,
|
&self,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use client_api_entity::workspace_dto::{
|
use client_api_entity::workspace_dto::{
|
||||||
CreatePageParams, CreateSpaceParams, MovePageParams, Page, PageCollab, Space, UpdatePageParams,
|
CreatePageParams, CreateSpaceParams, MovePageParams, Page, PageCollab, PublishPageParams, Space,
|
||||||
UpdateSpaceParams,
|
UpdatePageParams, UpdateSpaceParams,
|
||||||
};
|
};
|
||||||
use reqwest::Method;
|
use reqwest::Method;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
@ -169,6 +169,25 @@ impl Client {
|
||||||
.into_data()
|
.into_data()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn publish_page(
|
||||||
|
&self,
|
||||||
|
workspace_id: Uuid,
|
||||||
|
view_id: &str,
|
||||||
|
params: &PublishPageParams,
|
||||||
|
) -> Result<(), AppResponseError> {
|
||||||
|
let url = format!(
|
||||||
|
"{}/api/workspace/{}/page-view/{}/publish",
|
||||||
|
self.base_url, workspace_id, view_id
|
||||||
|
);
|
||||||
|
let resp = self
|
||||||
|
.http_client_with_auth(Method::POST, &url)
|
||||||
|
.await?
|
||||||
|
.json(params)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
AppResponse::<()>::from_response(resp).await?.into_error()
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn create_space(
|
pub async fn create_space(
|
||||||
&self,
|
&self,
|
||||||
workspace_id: Uuid,
|
workspace_id: Uuid,
|
||||||
|
|
|
||||||
|
|
@ -281,6 +281,12 @@ pub struct PublishInfoView {
|
||||||
pub info: PublishInfo,
|
pub info: PublishInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct PublishPageParams {
|
||||||
|
pub publish_name: Option<String>,
|
||||||
|
pub visible_database_view_ids: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Eq, PartialEq, Debug, Hash, Clone, Serialize_repr, Deserialize_repr)]
|
#[derive(Eq, PartialEq, Debug, Hash, Clone, Serialize_repr, Deserialize_repr)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
pub enum IconType {
|
pub enum IconType {
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ use crate::biz::workspace::ops::{
|
||||||
};
|
};
|
||||||
use crate::biz::workspace::page_view::{
|
use crate::biz::workspace::page_view::{
|
||||||
create_page, create_space, delete_all_pages_from_trash, delete_trash, get_page_view_collab,
|
create_page, create_space, delete_all_pages_from_trash, delete_trash, get_page_view_collab,
|
||||||
move_page, move_page_to_trash, restore_all_pages_from_trash, restore_page_from_trash,
|
move_page, move_page_to_trash, publish_page, restore_all_pages_from_trash,
|
||||||
update_page, update_page_collab_data, update_space,
|
restore_page_from_trash, update_page, update_page_collab_data, update_space,
|
||||||
};
|
};
|
||||||
use crate::biz::workspace::publish::get_workspace_default_publish_view_info_meta;
|
use crate::biz::workspace::publish::get_workspace_default_publish_view_info_meta;
|
||||||
use crate::biz::workspace::quick_note::{
|
use crate::biz::workspace::quick_note::{
|
||||||
|
|
@ -189,6 +189,10 @@ pub fn workspace_scope() -> Scope {
|
||||||
web::resource("/{workspace_id}/delete-all-pages-from-trash")
|
web::resource("/{workspace_id}/delete-all-pages-from-trash")
|
||||||
.route(web::post().to(delete_all_pages_from_trash_handler)),
|
.route(web::post().to(delete_all_pages_from_trash_handler)),
|
||||||
)
|
)
|
||||||
|
.service(
|
||||||
|
web::resource("/{workspace_id}/page-view/{view_id}/publish")
|
||||||
|
.route(web::post().to(publish_page_handler)),
|
||||||
|
)
|
||||||
.service(
|
.service(
|
||||||
web::resource("/{workspace_id}/batch/collab")
|
web::resource("/{workspace_id}/batch/collab")
|
||||||
.route(web::post().to(batch_create_collab_handler)),
|
.route(web::post().to(batch_create_collab_handler)),
|
||||||
|
|
@ -1272,6 +1276,37 @@ async fn delete_all_pages_from_trash_handler(
|
||||||
Ok(Json(AppResponse::Ok()))
|
Ok(Json(AppResponse::Ok()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn publish_page_handler(
|
||||||
|
user_uuid: UserUuid,
|
||||||
|
path: web::Path<(Uuid, String)>,
|
||||||
|
payload: Json<PublishPageParams>,
|
||||||
|
state: Data<AppState>,
|
||||||
|
) -> Result<Json<AppResponse<()>>> {
|
||||||
|
let (workspace_id, view_id) = path.into_inner();
|
||||||
|
let uid = state
|
||||||
|
.user_cache
|
||||||
|
.get_user_uid(&user_uuid)
|
||||||
|
.await
|
||||||
|
.map_err(AppResponseError::from)?;
|
||||||
|
let PublishPageParams {
|
||||||
|
publish_name,
|
||||||
|
visible_database_view_ids,
|
||||||
|
} = payload.into_inner();
|
||||||
|
publish_page(
|
||||||
|
&state.pg_pool,
|
||||||
|
&state.collab_access_control_storage,
|
||||||
|
state.published_collab_store.as_ref(),
|
||||||
|
uid,
|
||||||
|
*user_uuid,
|
||||||
|
workspace_id,
|
||||||
|
&view_id,
|
||||||
|
visible_database_view_ids,
|
||||||
|
publish_name,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(Json(AppResponse::Ok()))
|
||||||
|
}
|
||||||
|
|
||||||
async fn update_page_view_handler(
|
async fn update_page_view_handler(
|
||||||
user_uuid: UserUuid,
|
user_uuid: UserUuid,
|
||||||
path: web::Path<(Uuid, String)>,
|
path: web::Path<(Uuid, String)>,
|
||||||
|
|
|
||||||
|
|
@ -76,14 +76,12 @@ use super::folder_view::section_items_to_recent_folder_view;
|
||||||
use super::folder_view::section_items_to_trash_folder_view;
|
use super::folder_view::section_items_to_trash_folder_view;
|
||||||
use super::folder_view::to_dto_folder_view_miminal;
|
use super::folder_view::to_dto_folder_view_miminal;
|
||||||
use super::publish_outline::collab_folder_to_published_outline;
|
use super::publish_outline::collab_folder_to_published_outline;
|
||||||
use super::utils::collab_from_doc_state;
|
|
||||||
use super::utils::collab_to_bin;
|
use super::utils::collab_to_bin;
|
||||||
use super::utils::create_row_document;
|
use super::utils::create_row_document;
|
||||||
use super::utils::field_by_id_name_uniq;
|
use super::utils::field_by_id_name_uniq;
|
||||||
use super::utils::get_latest_collab;
|
use super::utils::get_latest_collab;
|
||||||
use super::utils::get_latest_collab_database_body;
|
use super::utils::get_latest_collab_database_body;
|
||||||
use super::utils::get_latest_collab_database_row_body;
|
use super::utils::get_latest_collab_database_row_body;
|
||||||
use super::utils::get_latest_collab_encoded;
|
|
||||||
use super::utils::get_latest_collab_folder;
|
use super::utils::get_latest_collab_folder;
|
||||||
use super::utils::get_row_details_serde;
|
use super::utils::get_row_details_serde;
|
||||||
use super::utils::type_option_reader_by_id;
|
use super::utils::type_option_reader_by_id;
|
||||||
|
|
@ -434,8 +432,7 @@ pub async fn get_latest_workspace_database(
|
||||||
workspace_id: Uuid,
|
workspace_id: Uuid,
|
||||||
) -> Result<(String, WorkspaceDatabase), AppError> {
|
) -> Result<(String, WorkspaceDatabase), AppError> {
|
||||||
let workspace_database_oid = select_workspace_database_oid(pg_pool, &workspace_id).await?;
|
let workspace_database_oid = select_workspace_database_oid(pg_pool, &workspace_id).await?;
|
||||||
let workspace_database_collab = {
|
let workspace_database_collab = get_latest_collab(
|
||||||
let encoded_collab = get_latest_collab_encoded(
|
|
||||||
collab_storage,
|
collab_storage,
|
||||||
collab_origin,
|
collab_origin,
|
||||||
&workspace_id.to_string(),
|
&workspace_id.to_string(),
|
||||||
|
|
@ -443,8 +440,6 @@ pub async fn get_latest_workspace_database(
|
||||||
CollabType::WorkspaceDatabase,
|
CollabType::WorkspaceDatabase,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
collab_from_doc_state(encoded_collab.doc_state.to_vec(), &workspace_database_oid)?
|
|
||||||
};
|
|
||||||
|
|
||||||
let workspace_database = WorkspaceDatabase::open(workspace_database_collab)
|
let workspace_database = WorkspaceDatabase::open(workspace_database_collab)
|
||||||
.map_err(|err| AppError::Unhandled(format!("failed to open workspace database: {}", err)))?;
|
.map_err(|err| AppError::Unhandled(format!("failed to open workspace database: {}", err)))?;
|
||||||
|
|
|
||||||
|
|
@ -19,19 +19,26 @@ use collab_database::rows::RowId;
|
||||||
use collab_database::rows::RowMetaKey;
|
use collab_database::rows::RowMetaKey;
|
||||||
use collab_database::template::timestamp_parse::TimestampCellData;
|
use collab_database::template::timestamp_parse::TimestampCellData;
|
||||||
use collab_database::workspace_database::NoPersistenceDatabaseCollabService;
|
use collab_database::workspace_database::NoPersistenceDatabaseCollabService;
|
||||||
|
use collab_database::workspace_database::WorkspaceDatabaseBody;
|
||||||
use collab_document::document::Document;
|
use collab_document::document::Document;
|
||||||
use collab_document::importer::md_importer::MDImporter;
|
use collab_document::importer::md_importer::MDImporter;
|
||||||
use collab_entity::CollabType;
|
use collab_entity::CollabType;
|
||||||
use collab_entity::EncodedCollab;
|
use collab_entity::EncodedCollab;
|
||||||
use collab_folder::CollabOrigin;
|
use collab_folder::CollabOrigin;
|
||||||
use collab_folder::Folder;
|
use collab_folder::Folder;
|
||||||
|
use database::collab::select_workspace_database_oid;
|
||||||
use database::collab::CollabStorage;
|
use database::collab::CollabStorage;
|
||||||
use database::collab::GetCollabOrigin;
|
use database::collab::GetCollabOrigin;
|
||||||
use database_entity::dto::QueryCollab;
|
use database_entity::dto::QueryCollab;
|
||||||
use database_entity::dto::QueryCollabParams;
|
use database_entity::dto::QueryCollabParams;
|
||||||
|
use database_entity::dto::QueryCollabResult;
|
||||||
|
use rayon::iter::IntoParallelIterator;
|
||||||
|
use rayon::iter::ParallelIterator;
|
||||||
|
use sqlx::PgPool;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
use yrs::Map;
|
use yrs::Map;
|
||||||
|
|
||||||
pub const DEFAULT_SPACE_ICON: &str = "interface_essential/home-3";
|
pub const DEFAULT_SPACE_ICON: &str = "interface_essential/home-3";
|
||||||
|
|
@ -262,6 +269,53 @@ pub async fn get_latest_collab_encoded(
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn batch_get_latest_collab_encoded(
|
||||||
|
collab_storage: &CollabAccessControlStorage,
|
||||||
|
collab_origin: GetCollabOrigin,
|
||||||
|
workspace_id: &str,
|
||||||
|
oid_list: &[String],
|
||||||
|
collab_type: CollabType,
|
||||||
|
) -> Result<HashMap<String, EncodedCollab>, AppError> {
|
||||||
|
let uid = match collab_origin {
|
||||||
|
GetCollabOrigin::User { uid } => uid,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
let queries: Vec<QueryCollab> = oid_list
|
||||||
|
.iter()
|
||||||
|
.map(|row_id| QueryCollab {
|
||||||
|
object_id: row_id.to_string(),
|
||||||
|
collab_type: collab_type.clone(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let query_collab_results = collab_storage
|
||||||
|
.batch_get_collab(&uid, workspace_id, queries, true)
|
||||||
|
.await;
|
||||||
|
let encoded_collabs = tokio::task::spawn_blocking(move || {
|
||||||
|
let collabs: HashMap<String, EncodedCollab> = query_collab_results
|
||||||
|
.into_par_iter()
|
||||||
|
.filter_map(|(oid, query_collab_result)| match query_collab_result {
|
||||||
|
QueryCollabResult::Success { encode_collab_v1 } => {
|
||||||
|
let decoded_result = EncodedCollab::decode_from_bytes(&encode_collab_v1);
|
||||||
|
match decoded_result {
|
||||||
|
Ok(decoded) => Some((oid, decoded)),
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!("Failed to decode collab for row {}: {}", oid, err);
|
||||||
|
None
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
QueryCollabResult::Failed { error } => {
|
||||||
|
tracing::error!("Failed to get collab: {:?}", error);
|
||||||
|
None
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
collabs
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
Ok(encoded_collabs)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_latest_collab(
|
pub async fn get_latest_collab(
|
||||||
storage: &CollabAccessControlStorage,
|
storage: &CollabAccessControlStorage,
|
||||||
origin: GetCollabOrigin,
|
origin: GetCollabOrigin,
|
||||||
|
|
@ -280,6 +334,31 @@ pub async fn get_latest_collab(
|
||||||
Ok(collab)
|
Ok(collab)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_latest_collab_workspace_database_body(
|
||||||
|
pg_pool: &PgPool,
|
||||||
|
storage: &CollabAccessControlStorage,
|
||||||
|
origin: GetCollabOrigin,
|
||||||
|
workspace_id: &str,
|
||||||
|
) -> Result<WorkspaceDatabaseBody, AppError> {
|
||||||
|
let workspace_uuid = Uuid::parse_str(workspace_id)?;
|
||||||
|
let ws_db_oid = select_workspace_database_oid(pg_pool, &workspace_uuid).await?;
|
||||||
|
let mut collab = get_latest_collab(
|
||||||
|
storage,
|
||||||
|
origin,
|
||||||
|
workspace_id,
|
||||||
|
&ws_db_oid,
|
||||||
|
CollabType::WorkspaceDatabase,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let ws_db = WorkspaceDatabaseBody::open(&mut collab).map_err(|err| {
|
||||||
|
AppError::Internal(anyhow::anyhow!(
|
||||||
|
"Failed to open workspace database body: {}",
|
||||||
|
err
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
Ok(ws_db)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_latest_collab_folder(
|
pub async fn get_latest_collab_folder(
|
||||||
collab_storage: &CollabAccessControlStorage,
|
collab_storage: &CollabAccessControlStorage,
|
||||||
collab_origin: GetCollabOrigin,
|
collab_origin: GetCollabOrigin,
|
||||||
|
|
@ -344,6 +423,21 @@ pub async fn collab_to_bin(collab: Collab, collab_type: CollabType) -> Result<Ve
|
||||||
.await?
|
.await?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn collab_to_doc_state(
|
||||||
|
collab: Collab,
|
||||||
|
collab_type: CollabType,
|
||||||
|
) -> Result<Vec<u8>, AppError> {
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
let bin = collab
|
||||||
|
.encode_collab_v1(|collab| collab_type.validate_require_data(collab))
|
||||||
|
.map_err(|e| AppError::Unhandled(e.to_string()))?
|
||||||
|
.doc_state
|
||||||
|
.to_vec();
|
||||||
|
Ok(bin)
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
|
||||||
pub fn collab_from_doc_state(doc_state: Vec<u8>, object_id: &str) -> Result<Collab, AppError> {
|
pub fn collab_from_doc_state(doc_state: Vec<u8>, object_id: &str) -> Result<Collab, AppError> {
|
||||||
let collab = Collab::new_with_source(
|
let collab = Collab::new_with_source(
|
||||||
CollabOrigin::Server,
|
CollabOrigin::Server,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@ use crate::biz::collab::folder_view::{
|
||||||
};
|
};
|
||||||
use crate::biz::collab::ops::get_latest_workspace_database;
|
use crate::biz::collab::ops::get_latest_workspace_database;
|
||||||
use crate::biz::collab::utils::{
|
use crate::biz::collab::utils::{
|
||||||
collab_from_doc_state, get_latest_collab_encoded, get_latest_collab_folder,
|
batch_get_latest_collab_encoded, collab_from_doc_state, collab_to_doc_state,
|
||||||
|
get_latest_collab_database_body, get_latest_collab_encoded, get_latest_collab_folder,
|
||||||
};
|
};
|
||||||
use actix_web::web::Data;
|
use actix_web::web::Data;
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
|
|
@ -41,21 +42,27 @@ use collab_rt_entity::user::RealtimeUser;
|
||||||
use database::collab::{select_workspace_database_oid, CollabStorage, GetCollabOrigin};
|
use database::collab::{select_workspace_database_oid, CollabStorage, GetCollabOrigin};
|
||||||
use database::publish::select_published_view_ids_for_workspace;
|
use database::publish::select_published_view_ids_for_workspace;
|
||||||
use database::user::select_web_user_from_uid;
|
use database::user::select_web_user_from_uid;
|
||||||
use database_entity::dto::{CollabParams, QueryCollab, QueryCollabResult};
|
use database_entity::dto::{
|
||||||
|
CollabParams, PublishCollabItem, PublishCollabMetadata, QueryCollab, QueryCollabResult,
|
||||||
|
};
|
||||||
|
use fancy_regex::Regex;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
use shared_entity::dto::publish_dto::{PublishDatabaseData, PublishViewInfo, PublishViewMetaData};
|
||||||
use shared_entity::dto::workspace_dto::{
|
use shared_entity::dto::workspace_dto::{
|
||||||
FolderView, Page, PageCollab, PageCollabData, Space, SpacePermission, ViewIcon, ViewLayout,
|
FolderView, Page, PageCollab, PageCollabData, Space, SpacePermission, ViewIcon, ViewLayout,
|
||||||
};
|
};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, LazyLock};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use tokio::time::timeout_at;
|
use tokio::time::timeout_at;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use super::publish::PublishedCollabStore;
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn update_space(
|
pub async fn update_space(
|
||||||
appflowy_web_metrics: &AppFlowyWebMetrics,
|
appflowy_web_metrics: &AppFlowyWebMetrics,
|
||||||
|
|
@ -1036,6 +1043,217 @@ pub async fn update_page(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static INVALID_URL_CHARS: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[^\w-]").unwrap());
|
||||||
|
|
||||||
|
fn replace_invalid_url_chars(input: &str) -> String {
|
||||||
|
INVALID_URL_CHARS.replace_all(input, "-").to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_publish_name(view_id: &str, name: &str) -> String {
|
||||||
|
let id_len = view_id.len();
|
||||||
|
let name = replace_invalid_url_chars(name);
|
||||||
|
let name_len = name.len();
|
||||||
|
// The backend limits the publish name to a maximum of 50 characters.
|
||||||
|
// If the combined length of the ID and the name exceeds 50 characters,
|
||||||
|
// we will truncate the name to ensure the final result is within the limit.
|
||||||
|
// The name should only contain alphanumeric characters and hyphens.
|
||||||
|
let result = format!(
|
||||||
|
"{}-{}",
|
||||||
|
&name[..std::cmp::min(49 - id_len, name_len)],
|
||||||
|
view_id
|
||||||
|
);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub async fn publish_page(
|
||||||
|
pg_pool: &PgPool,
|
||||||
|
collab_access_control_storage: &CollabAccessControlStorage,
|
||||||
|
publish_collab_store: &dyn PublishedCollabStore,
|
||||||
|
uid: i64,
|
||||||
|
user_uuid: Uuid,
|
||||||
|
workspace_id: Uuid,
|
||||||
|
view_id: &str,
|
||||||
|
visible_database_view_ids: Option<Vec<String>>,
|
||||||
|
publish_name: Option<impl ToString>,
|
||||||
|
) -> Result<(), AppError> {
|
||||||
|
let folder = get_latest_collab_folder(
|
||||||
|
collab_access_control_storage,
|
||||||
|
GetCollabOrigin::User { uid },
|
||||||
|
&workspace_id.to_string(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let view = folder
|
||||||
|
.get_view(view_id)
|
||||||
|
.ok_or(AppError::InvalidFolderView(format!(
|
||||||
|
"View {} not found",
|
||||||
|
view_id
|
||||||
|
)))?;
|
||||||
|
let icon = view
|
||||||
|
.icon
|
||||||
|
.as_ref()
|
||||||
|
.map(|icon| to_dto_view_icon(icon.clone()));
|
||||||
|
let metadata = PublishViewMetaData {
|
||||||
|
view: PublishViewInfo {
|
||||||
|
view_id: view_id.to_string(),
|
||||||
|
name: view.name.clone(),
|
||||||
|
icon,
|
||||||
|
layout: to_dto_view_layout(&view.layout),
|
||||||
|
extra: view.extra.clone(),
|
||||||
|
created_by: view.created_by,
|
||||||
|
last_edited_by: view.last_edited_by,
|
||||||
|
last_edited_time: view.last_edited_time,
|
||||||
|
created_at: view.created_at,
|
||||||
|
child_views: None,
|
||||||
|
},
|
||||||
|
// Note: The use of child views and ancestor views are going to be deprecated in
|
||||||
|
// appflowy web as there is now endpoint to obtain published outline.
|
||||||
|
child_views: vec![],
|
||||||
|
ancestor_views: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let publish_data = match view.layout {
|
||||||
|
collab_folder::ViewLayout::Document => {
|
||||||
|
generate_publish_data_for_document(collab_access_control_storage, uid, workspace_id, view_id)
|
||||||
|
.await
|
||||||
|
},
|
||||||
|
collab_folder::ViewLayout::Grid
|
||||||
|
| collab_folder::ViewLayout::Board
|
||||||
|
| collab_folder::ViewLayout::Calendar => {
|
||||||
|
generate_publish_data_for_database(
|
||||||
|
pg_pool,
|
||||||
|
collab_access_control_storage,
|
||||||
|
uid,
|
||||||
|
workspace_id,
|
||||||
|
view_id,
|
||||||
|
visible_database_view_ids,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
},
|
||||||
|
collab_folder::ViewLayout::Chat => Err(AppError::InvalidRequest(
|
||||||
|
"AI Chat cannot be published".to_string(),
|
||||||
|
)),
|
||||||
|
}?;
|
||||||
|
publish_collab_store
|
||||||
|
.publish_collabs(
|
||||||
|
vec![PublishCollabItem {
|
||||||
|
meta: PublishCollabMetadata {
|
||||||
|
view_id: Uuid::parse_str(view_id).unwrap(),
|
||||||
|
publish_name: publish_name
|
||||||
|
.map(|name| name.to_string())
|
||||||
|
.unwrap_or_else(|| generate_publish_name(view_id, &view.name)),
|
||||||
|
metadata: serde_json::value::to_value(metadata).unwrap(),
|
||||||
|
},
|
||||||
|
data: publish_data,
|
||||||
|
}],
|
||||||
|
&workspace_id,
|
||||||
|
&user_uuid,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn generate_publish_data_for_document(
|
||||||
|
collab_access_control_storage: &CollabAccessControlStorage,
|
||||||
|
uid: i64,
|
||||||
|
workspace_id: Uuid,
|
||||||
|
view_id: &str,
|
||||||
|
) -> Result<Vec<u8>, AppError> {
|
||||||
|
let collab = get_latest_collab_encoded(
|
||||||
|
collab_access_control_storage,
|
||||||
|
GetCollabOrigin::User { uid },
|
||||||
|
&workspace_id.to_string(),
|
||||||
|
view_id,
|
||||||
|
CollabType::Document,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(collab.doc_state.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn generate_publish_data_for_database(
|
||||||
|
pg_pool: &PgPool,
|
||||||
|
collab_storage: &CollabAccessControlStorage,
|
||||||
|
uid: i64,
|
||||||
|
workspace_id: Uuid,
|
||||||
|
view_id: &str,
|
||||||
|
visible_database_view_ids: Option<Vec<String>>,
|
||||||
|
) -> Result<Vec<u8>, AppError> {
|
||||||
|
let (_, ws_db) = get_latest_workspace_database(
|
||||||
|
collab_storage,
|
||||||
|
pg_pool,
|
||||||
|
GetCollabOrigin::User { uid },
|
||||||
|
workspace_id,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let db_oid = {
|
||||||
|
ws_db
|
||||||
|
.get_database_meta_with_view_id(view_id)
|
||||||
|
.ok_or(AppError::NoRequiredData(format!(
|
||||||
|
"Database view {} not found",
|
||||||
|
view_id
|
||||||
|
)))?
|
||||||
|
.database_id
|
||||||
|
};
|
||||||
|
let (db_collab, db_body) =
|
||||||
|
get_latest_collab_database_body(collab_storage, &workspace_id.to_string(), &db_oid).await?;
|
||||||
|
let inline_view_id = {
|
||||||
|
let txn = db_collab.transact();
|
||||||
|
db_body.get_inline_view_id(&txn)
|
||||||
|
};
|
||||||
|
let row_ids: Vec<String> = {
|
||||||
|
let txn = db_collab.transact();
|
||||||
|
db_body
|
||||||
|
.views
|
||||||
|
.get_row_orders(&txn, &inline_view_id)
|
||||||
|
.iter()
|
||||||
|
.map(|ro| ro.id.to_string())
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
let encoded_rows = batch_get_latest_collab_encoded(
|
||||||
|
collab_storage,
|
||||||
|
GetCollabOrigin::User { uid },
|
||||||
|
&workspace_id.to_string(),
|
||||||
|
&row_ids,
|
||||||
|
CollabType::DatabaseRow,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let row_data: HashMap<String, Vec<u8>> = encoded_rows
|
||||||
|
.into_iter()
|
||||||
|
.map(|(oid, encoded_collab)| (oid, encoded_collab.doc_state.to_vec()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let row_document_ids = row_ids
|
||||||
|
.iter()
|
||||||
|
.filter_map(|row_id| {
|
||||||
|
db_body
|
||||||
|
.block
|
||||||
|
.get_row_document_id(&RowId::from(row_id.to_owned()))
|
||||||
|
.map(|doc_id| doc_id.to_string())
|
||||||
|
})
|
||||||
|
.collect_vec();
|
||||||
|
let encoded_row_documents = batch_get_latest_collab_encoded(
|
||||||
|
collab_storage,
|
||||||
|
GetCollabOrigin::User { uid },
|
||||||
|
&workspace_id.to_string(),
|
||||||
|
&row_document_ids,
|
||||||
|
CollabType::Document,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let row_document_data: HashMap<String, Vec<u8>> = encoded_row_documents
|
||||||
|
.into_iter()
|
||||||
|
.map(|(oid, encoded_collab)| (oid, encoded_collab.doc_state.to_vec()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let data = PublishDatabaseData {
|
||||||
|
database_collab: collab_to_doc_state(db_collab, CollabType::Database).await?,
|
||||||
|
database_row_collabs: row_data,
|
||||||
|
database_row_document_collabs: row_document_data,
|
||||||
|
visible_database_view_ids: visible_database_view_ids.unwrap_or(vec![view_id.to_string()]),
|
||||||
|
database_relations: HashMap::from([(db_oid, view_id.to_string())]),
|
||||||
|
};
|
||||||
|
Ok(serde_json::ser::to_vec(&data)?)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_page_view_collab(
|
pub async fn get_page_view_collab(
|
||||||
pg_pool: &PgPool,
|
pg_pool: &PgPool,
|
||||||
collab_access_control_storage: &CollabAccessControlStorage,
|
collab_access_control_storage: &CollabAccessControlStorage,
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ use collab_entity::CollabType;
|
||||||
use collab_folder::{CollabOrigin, Folder};
|
use collab_folder::{CollabOrigin, Folder};
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use shared_entity::dto::workspace_dto::{
|
use shared_entity::dto::workspace_dto::{
|
||||||
CreatePageParams, CreateSpaceParams, IconType, MovePageParams, SpacePermission, UpdatePageParams,
|
CreatePageParams, CreateSpaceParams, IconType, MovePageParams, PublishPageParams,
|
||||||
UpdateSpaceParams, ViewIcon, ViewLayout,
|
SpacePermission, UpdatePageParams, UpdateSpaceParams, ViewIcon, ViewLayout,
|
||||||
};
|
};
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
@ -727,3 +727,71 @@ async fn create_space() {
|
||||||
assert_eq!(space_info["space_icon"].as_str().unwrap(), "space_icon_3");
|
assert_eq!(space_info["space_icon"].as_str().unwrap(), "space_icon_3");
|
||||||
assert_eq!(space_info["space_icon_color"].as_str().unwrap(), "#000000");
|
assert_eq!(space_info["space_icon_color"].as_str().unwrap(), "#000000");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn publish_page() {
|
||||||
|
let registered_user = generate_unique_registered_user().await;
|
||||||
|
let web_client = TestClient::user_with_new_device(registered_user.clone()).await;
|
||||||
|
let workspace_id = web_client.workspace_id().await;
|
||||||
|
let folder_view = web_client
|
||||||
|
.api_client
|
||||||
|
.get_workspace_folder(&workspace_id, Some(2), None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let general_space = &folder_view
|
||||||
|
.children
|
||||||
|
.into_iter()
|
||||||
|
.find(|v| v.name == "General")
|
||||||
|
.unwrap();
|
||||||
|
let database_page_id = general_space
|
||||||
|
.children
|
||||||
|
.iter()
|
||||||
|
.find(|v| v.name == "To-dos")
|
||||||
|
.unwrap()
|
||||||
|
.view_id
|
||||||
|
.clone();
|
||||||
|
let document_page_id = general_space
|
||||||
|
.children
|
||||||
|
.iter()
|
||||||
|
.find(|v| v.name == "Getting started")
|
||||||
|
.unwrap()
|
||||||
|
.view_id
|
||||||
|
.clone();
|
||||||
|
let page_to_be_published = vec![database_page_id, document_page_id];
|
||||||
|
for view_id in &page_to_be_published {
|
||||||
|
web_client
|
||||||
|
.api_client
|
||||||
|
.publish_page(
|
||||||
|
Uuid::parse_str(&workspace_id).unwrap(),
|
||||||
|
view_id,
|
||||||
|
&PublishPageParams {
|
||||||
|
publish_name: None,
|
||||||
|
visible_database_view_ids: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
let publish_namespace = web_client
|
||||||
|
.api_client
|
||||||
|
.get_workspace_publish_namespace(&workspace_id)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let published_view = web_client
|
||||||
|
.api_client
|
||||||
|
.get_published_outline(&publish_namespace)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let published_view_ids: HashSet<String> = published_view
|
||||||
|
.children
|
||||||
|
.iter()
|
||||||
|
.find(|v| v.name == "General")
|
||||||
|
.unwrap()
|
||||||
|
.children
|
||||||
|
.iter()
|
||||||
|
.map(|v| v.view_id.clone())
|
||||||
|
.collect();
|
||||||
|
for view_id in &page_to_be_published {
|
||||||
|
assert!(published_view_ids.contains(view_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue