diff --git a/Cargo.lock b/Cargo.lock index 4a7b73992..8b62c58c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1896,7 +1896,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=df936abcf8b5747c5d59efac5a0f733357060a1d#df936abcf8b5747c5d59efac5a0f733357060a1d" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=7ea6f127429538e1c84a74c42ca087dc1a313bdd#7ea6f127429538e1c84a74c42ca087dc1a313bdd" dependencies = [ "anyhow", "arc-swap", @@ -1921,7 +1921,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=df936abcf8b5747c5d59efac5a0f733357060a1d#df936abcf8b5747c5d59efac5a0f733357060a1d" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=7ea6f127429538e1c84a74c42ca087dc1a313bdd#7ea6f127429538e1c84a74c42ca087dc1a313bdd" dependencies = [ "anyhow", "async-trait", @@ -1961,7 +1961,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=df936abcf8b5747c5d59efac5a0f733357060a1d#df936abcf8b5747c5d59efac5a0f733357060a1d" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=7ea6f127429538e1c84a74c42ca087dc1a313bdd#7ea6f127429538e1c84a74c42ca087dc1a313bdd" dependencies = [ "anyhow", "arc-swap", @@ -1982,7 +1982,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=df936abcf8b5747c5d59efac5a0f733357060a1d#df936abcf8b5747c5d59efac5a0f733357060a1d" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=7ea6f127429538e1c84a74c42ca087dc1a313bdd#7ea6f127429538e1c84a74c42ca087dc1a313bdd" dependencies = [ "anyhow", "bytes", @@ -2002,7 +2002,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=df936abcf8b5747c5d59efac5a0f733357060a1d#df936abcf8b5747c5d59efac5a0f733357060a1d" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=7ea6f127429538e1c84a74c42ca087dc1a313bdd#7ea6f127429538e1c84a74c42ca087dc1a313bdd" dependencies = [ "anyhow", "arc-swap", @@ -2024,7 +2024,7 @@ dependencies = [ [[package]] name = "collab-importer" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=df936abcf8b5747c5d59efac5a0f733357060a1d#df936abcf8b5747c5d59efac5a0f733357060a1d" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=7ea6f127429538e1c84a74c42ca087dc1a313bdd#7ea6f127429538e1c84a74c42ca087dc1a313bdd" dependencies = [ "anyhow", "async-recursion", @@ -2130,7 +2130,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=df936abcf8b5747c5d59efac5a0f733357060a1d#df936abcf8b5747c5d59efac5a0f733357060a1d" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=7ea6f127429538e1c84a74c42ca087dc1a313bdd#7ea6f127429538e1c84a74c42ca087dc1a313bdd" dependencies = [ "anyhow", "collab", diff --git a/Cargo.toml b/Cargo.toml index 9e4f59bc0..0f68c1c96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -303,13 +303,13 @@ lto = false [patch.crates-io] # It's diffcult to resovle different version with the same crate used in AppFlowy Frontend and the Client-API crate. # So using patch to workaround this issue. -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "df936abcf8b5747c5d59efac5a0f733357060a1d" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "df936abcf8b5747c5d59efac5a0f733357060a1d" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "df936abcf8b5747c5d59efac5a0f733357060a1d" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "df936abcf8b5747c5d59efac5a0f733357060a1d" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "df936abcf8b5747c5d59efac5a0f733357060a1d" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "df936abcf8b5747c5d59efac5a0f733357060a1d" } -collab-importer = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "df936abcf8b5747c5d59efac5a0f733357060a1d" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "7ea6f127429538e1c84a74c42ca087dc1a313bdd" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "7ea6f127429538e1c84a74c42ca087dc1a313bdd" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "7ea6f127429538e1c84a74c42ca087dc1a313bdd" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "7ea6f127429538e1c84a74c42ca087dc1a313bdd" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "7ea6f127429538e1c84a74c42ca087dc1a313bdd" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "7ea6f127429538e1c84a74c42ca087dc1a313bdd" } +collab-importer = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "7ea6f127429538e1c84a74c42ca087dc1a313bdd" } [features] history = [] diff --git a/libs/app-error/src/lib.rs b/libs/app-error/src/lib.rs index b95aeb7dc..8dddd16df 100644 --- a/libs/app-error/src/lib.rs +++ b/libs/app-error/src/lib.rs @@ -188,6 +188,9 @@ pub enum AppError { #[error("Apply update error:{0}")] ApplyUpdateError(String), + + #[error("{0}")] + InvalidBlock(String), } impl AppError { @@ -269,6 +272,7 @@ impl AppError { AppError::DecodeUpdateError(_) => ErrorCode::DecodeUpdateError, AppError::ApplyUpdateError(_) => ErrorCode::ApplyUpdateError, AppError::ActionTimeout(_) => ErrorCode::ActionTimeout, + AppError::InvalidBlock(_) => ErrorCode::InvalidBlock, } } } @@ -438,6 +442,7 @@ pub enum ErrorCode { AIMaxRequired = 1061, InvalidPageData = 1062, MemberNotFound = 1063, + InvalidBlock = 1064, } impl ErrorCode { diff --git a/libs/client-api/src/http_view.rs b/libs/client-api/src/http_view.rs index d6e08a228..90fefcba5 100644 --- a/libs/client-api/src/http_view.rs +++ b/libs/client-api/src/http_view.rs @@ -1,6 +1,6 @@ use client_api_entity::workspace_dto::{ - CreatePageParams, CreateSpaceParams, MovePageParams, Page, PageCollab, PublishPageParams, Space, - UpdatePageParams, UpdateSpaceParams, + AppendBlockToPageParams, CreatePageParams, CreateSpaceParams, MovePageParams, Page, PageCollab, + PublishPageParams, Space, UpdatePageParams, UpdateSpaceParams, }; use reqwest::Method; use serde_json::json; @@ -239,4 +239,23 @@ impl Client { .await?; AppResponse::<()>::from_response(resp).await?.into_error() } + + pub async fn append_block_to_page( + &self, + workspace_id: Uuid, + view_id: &str, + params: &AppendBlockToPageParams, + ) -> Result<(), AppResponseError> { + let url = format!( + "{}/api/workspace/{}/page-view/{}/append-block", + 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() + } } diff --git a/libs/shared-entity/src/dto/workspace_dto.rs b/libs/shared-entity/src/dto/workspace_dto.rs index 892461bf1..11adc432c 100644 --- a/libs/shared-entity/src/dto/workspace_dto.rs +++ b/libs/shared-entity/src/dto/workspace_dto.rs @@ -195,6 +195,11 @@ pub struct UpdatePageParams { pub extra: Option, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AppendBlockToPageParams { + pub blocks: Vec, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MovePageParams { pub new_parent_view_id: String, diff --git a/libs/workspace-template/src/document/parser.rs b/libs/workspace-template/src/document/parser.rs index a153c5e95..c8b2d2924 100644 --- a/libs/workspace-template/src/document/parser.rs +++ b/libs/workspace-template/src/document/parser.rs @@ -44,22 +44,23 @@ impl JsonToDocumentParser { Self::serde_block_to_document(root) } - fn generate_blocks( + pub fn generate_blocks( block: &SerdeBlock, id: Option, parent_id: String, ) -> (IndexMap, IndexMap) { let (block_pb, delta) = Self::block_to_block_pb(block, id, parent_id); + let block_id = block_pb.id.clone(); + let external_id = block_pb.external_id.clone(); let mut blocks = IndexMap::new(); + blocks.insert(block_pb.id.clone(), block_pb); let mut text_map = IndexMap::new(); for child in &block.children { let (child_blocks, child_blocks_text_map) = - Self::generate_blocks(child, None, block_pb.id.clone()); + Self::generate_blocks(child, None, block_id.clone()); blocks.extend(child_blocks); text_map.extend(child_blocks_text_map); } - let external_id = block_pb.external_id.clone(); - blocks.insert(block_pb.id.clone(), block_pb); if let Some(delta) = delta { if let Some(external_id) = external_id { text_map.insert(external_id, delta); @@ -75,7 +76,7 @@ impl JsonToDocumentParser { .collect() } - fn generate_children_map(blocks: &IndexMap) -> HashMap> { + pub fn generate_children_map(blocks: &IndexMap) -> HashMap> { let mut children_map = HashMap::new(); for (id, block) in blocks.iter() { // add itself to it's parent's children diff --git a/src/api/workspace.rs b/src/api/workspace.rs index ab7bb8024..16334d064 100644 --- a/src/api/workspace.rs +++ b/src/api/workspace.rs @@ -13,9 +13,10 @@ use crate::biz::workspace::ops::{ get_reactions_on_published_view, remove_comment_on_published_view, remove_reaction_on_comment, }; use crate::biz::workspace::page_view::{ - create_page, create_space, delete_all_pages_from_trash, delete_trash, get_page_view_collab, - move_page, move_page_to_trash, publish_page, restore_all_pages_from_trash, - restore_page_from_trash, unpublish_page, update_page, update_page_collab_data, update_space, + append_block_at_the_end_of_page, create_page, create_space, delete_all_pages_from_trash, + delete_trash, get_page_view_collab, move_page, move_page_to_trash, publish_page, + restore_all_pages_from_trash, restore_page_from_trash, unpublish_page, update_page, + update_page_collab_data, update_space, }; use crate::biz::workspace::publish::get_workspace_default_publish_view_info_meta; use crate::biz::workspace::quick_note::{ @@ -55,6 +56,7 @@ use database_entity::dto::PublishCollabItem; use database_entity::dto::PublishInfo; use database_entity::dto::*; use indexer::scheduler::{UnindexedCollabTask, UnindexedData}; +use itertools::Itertools; use prost::Message as ProstMessage; use rayon::prelude::*; use sha2::{Digest, Sha256}; @@ -70,6 +72,7 @@ use tokio_tungstenite::tungstenite::Message; use tracing::{error, event, instrument, trace}; use uuid::Uuid; use validator::Validate; +use workspace_template::document::parser::SerdeBlock; pub const WORKSPACE_ID_PATH: &str = "workspace_id"; pub const COLLAB_OBJECT_ID_PATH: &str = "object_id"; @@ -173,6 +176,10 @@ pub fn workspace_scope() -> Scope { .route(web::get().to(get_page_view_handler)) .route(web::patch().to(update_page_view_handler)), ) + .service( + web::resource("/{workspace_id}/page-view/{view_id}/append-block") + .route(web::post().to(append_block_to_page_handler)), + ) .service( web::resource("/{workspace_id}/page-view/{view_id}/move") .route(web::post().to(move_page_handler)), @@ -1214,6 +1221,40 @@ async fn post_page_view_handler( Ok(Json(AppResponse::Ok().with_data(page))) } +async fn append_block_to_page_handler( + user_uuid: UserUuid, + path: web::Path<(Uuid, String)>, + payload: Json, + state: Data, + server: Data, + req: HttpRequest, +) -> Result>> { + let uid = state.user_cache.get_user_uid(&user_uuid).await?; + let (workspace_uuid, view_id) = path.into_inner(); + let user = realtime_user_for_web_request(req.headers(), uid)?; + let serde_blocks: Vec> = payload + .blocks + .iter() + .map(|value| { + serde_json::from_value(value.clone()).map_err(|err| AppError::InvalidBlock(err.to_string())) + }) + .collect_vec(); + let serde_blocks = serde_blocks + .into_iter() + .collect::, AppError>>()?; + append_block_at_the_end_of_page( + &state.metrics.appflowy_web_metrics, + server, + user, + &state.collab_access_control_storage, + workspace_uuid, + &view_id, + &serde_blocks, + ) + .await?; + Ok(Json(AppResponse::Ok())) +} + async fn move_page_handler( user_uuid: UserUuid, path: web::Path<(Uuid, String)>, diff --git a/src/biz/workspace/page_view.rs b/src/biz/workspace/page_view.rs index 3856d687f..95b2c16d5 100644 --- a/src/biz/workspace/page_view.rs +++ b/src/biz/workspace/page_view.rs @@ -34,7 +34,7 @@ use collab_database::views::{ }; use collab_database::workspace_database::{NoPersistenceDatabaseCollabService, WorkspaceDatabase}; use collab_database::{database::DatabaseBody, rows::RowId}; -use collab_document::document::Document; +use collab_document::document::{Document, DocumentBody}; use collab_document::document_data::default_document_data; use collab_entity::{CollabType, EncodedCollab}; use collab_folder::hierarchy_builder::NestedChildViewBuilder; @@ -62,7 +62,7 @@ use std::time::{Duration, Instant}; use tokio::time::timeout_at; use tracing::instrument; use uuid::Uuid; -use workspace_template::document::parser::JsonToDocumentParser; +use workspace_template::document::parser::{JsonToDocumentParser, SerdeBlock}; use super::publish::PublishedCollabStore; @@ -448,6 +448,90 @@ async fn prepare_default_board_encoded_database( .await } +pub async fn append_block_at_the_end_of_page( + appflowy_web_metrics: &AppFlowyWebMetrics, + server: Data, + user: RealtimeUser, + collab_storage: &CollabAccessControlStorage, + workspace_id: Uuid, + view_id: &str, + serde_blocks: &[SerdeBlock], +) -> Result<(), AppError> { + let oid = Uuid::parse_str(view_id).unwrap(); + let update = + append_block_to_document_collab(user.uid, collab_storage, workspace_id, oid, serde_blocks) + .await?; + update_page_collab_data( + appflowy_web_metrics, + server, + user, + workspace_id, + oid, + CollabType::Document, + update, + ) + .await +} + +async fn append_block_to_document_collab( + uid: i64, + collab_storage: &CollabAccessControlStorage, + workspace_id: Uuid, + oid: Uuid, + serde_blocks: &[SerdeBlock], +) -> Result, AppError> { + let original_doc_state = get_latest_collab_encoded( + collab_storage, + GetCollabOrigin::User { uid }, + &workspace_id.to_string(), + &oid.to_string(), + CollabType::Document, + ) + .await? + .doc_state; + let mut collab = collab_from_doc_state(original_doc_state.to_vec(), &oid.to_string())?; + let document_body = DocumentBody::from_collab(&collab) + .ok_or_else(|| AppError::Internal(anyhow::anyhow!("invalid document collab")))?; + let document_data = { + let txn = collab.transact(); + document_body + .get_document_data(&txn) + .map_err(|err| AppError::Internal(anyhow::anyhow!(err.to_string()))) + }?; + let page_id = document_data.page_id.clone(); + let page_id_children_id = document_data + .blocks + .get(&page_id) + .map(|block| block.children.clone()); + let mut prev_id = page_id_children_id + .and_then(|children_id| document_data.meta.children_map.get(&children_id)) + .and_then(|child_ids| child_ids.last().cloned()); + + let update = { + let mut txn = collab.transact_mut(); + for serde_block in serde_blocks { + let (block_index_map, text_map) = + JsonToDocumentParser::generate_blocks(serde_block, None, page_id.clone()); + + for (block_id, block) in block_index_map.iter() { + document_body + .insert_block(&mut txn, block.clone(), prev_id.clone()) + .map_err(|err| AppError::InvalidBlock(err.to_string()))?; + prev_id = Some(block_id.clone()); + } + + for (text_id, text) in text_map.iter() { + let delta = serde_json::from_str(text).unwrap_or_else(|_| vec![]); + document_body + .text_operation + .apply_delta(&mut txn, text_id, delta); + } + } + txn.encode_update_v1() + }; + Ok(update) +} + #[allow(clippy::too_many_arguments)] async fn add_new_space_to_folder( uid: i64, diff --git a/tests/workspace/page_view.rs b/tests/workspace/page_view.rs index b8376e22a..957f3b745 100644 --- a/tests/workspace/page_view.rs +++ b/tests/workspace/page_view.rs @@ -9,8 +9,8 @@ use collab_entity::CollabType; use collab_folder::{CollabOrigin, Folder}; use serde_json::{json, Value}; use shared_entity::dto::workspace_dto::{ - CreatePageParams, CreateSpaceParams, IconType, MovePageParams, PublishPageParams, - SpacePermission, UpdatePageParams, UpdateSpaceParams, ViewIcon, ViewLayout, + AppendBlockToPageParams, CreatePageParams, CreateSpaceParams, IconType, MovePageParams, + PublishPageParams, SpacePermission, UpdatePageParams, UpdateSpaceParams, ViewIcon, ViewLayout, }; use tokio::time::sleep; use uuid::Uuid; @@ -251,6 +251,47 @@ async fn create_new_document_page() { .unwrap(); } +#[tokio::test] +async fn append_block_to_page() { + let (c, _user) = generate_unique_registered_user_client().await; + let workspaces = c.get_workspaces().await.unwrap(); + assert_eq!(workspaces.len(), 1); + let workspace_id = workspaces[0].workspace_id; + let folder_view = c + .get_workspace_folder(&workspace_id.to_string(), Some(2), None) + .await + .unwrap(); + let general_space = &folder_view + .children + .into_iter() + .find(|v| v.name == "General") + .unwrap(); + let getting_started = general_space + .children + .iter() + .find(|v| v.name == "Getting started") + .unwrap(); + let getting_started_view_id = &getting_started.view_id; + c.append_block_to_page( + workspace_id, + getting_started_view_id, + &AppendBlockToPageParams { + blocks: vec![json!({ + "type": "paragraph", + "data": { + "delta": [ + { + "insert": "The sky appears blue due to a phenomenon called Rayleigh scattering." + } + ] + } + })], + }, + ) + .await + .unwrap(); +} + #[tokio::test] async fn create_new_chat_page() { let (c, _user) = generate_unique_registered_user_client().await;