Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: api to add block to the end of document #1242

Merged
merged 1 commit into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down
5 changes: 5 additions & 0 deletions libs/app-error/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,9 @@ pub enum AppError {

#[error("Apply update error:{0}")]
ApplyUpdateError(String),

#[error("{0}")]
InvalidBlock(String),
}

impl AppError {
Expand Down Expand Up @@ -269,6 +272,7 @@ impl AppError {
AppError::DecodeUpdateError(_) => ErrorCode::DecodeUpdateError,
AppError::ApplyUpdateError(_) => ErrorCode::ApplyUpdateError,
AppError::ActionTimeout(_) => ErrorCode::ActionTimeout,
AppError::InvalidBlock(_) => ErrorCode::InvalidBlock,
}
}
}
Expand Down Expand Up @@ -438,6 +442,7 @@ pub enum ErrorCode {
AIMaxRequired = 1061,
InvalidPageData = 1062,
MemberNotFound = 1063,
InvalidBlock = 1064,
}

impl ErrorCode {
Expand Down
23 changes: 21 additions & 2 deletions libs/client-api/src/http_view.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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()
}
}
5 changes: 5 additions & 0 deletions libs/shared-entity/src/dto/workspace_dto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,11 @@ pub struct UpdatePageParams {
pub extra: Option<Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppendBlockToPageParams {
pub blocks: Vec<serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MovePageParams {
pub new_parent_view_id: String,
Expand Down
11 changes: 6 additions & 5 deletions libs/workspace-template/src/document/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,22 +44,23 @@ impl JsonToDocumentParser {
Self::serde_block_to_document(root)
}

fn generate_blocks(
pub fn generate_blocks(
block: &SerdeBlock,
id: Option<String>,
parent_id: String,
) -> (IndexMap<String, Block>, IndexMap<String, String>) {
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);
Expand All @@ -75,7 +76,7 @@ impl JsonToDocumentParser {
.collect()
}

fn generate_children_map(blocks: &IndexMap<String, Block>) -> HashMap<String, Vec<String>> {
pub fn generate_children_map(blocks: &IndexMap<String, Block>) -> HashMap<String, Vec<String>> {
let mut children_map = HashMap::new();
for (id, block) in blocks.iter() {
// add itself to it's parent's children
Expand Down
47 changes: 44 additions & 3 deletions src/api/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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};
Expand All @@ -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";
Expand Down Expand Up @@ -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)),
Expand Down Expand Up @@ -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<AppendBlockToPageParams>,
state: Data<AppState>,
server: Data<RealtimeServerAddr>,
req: HttpRequest,
) -> Result<Json<AppResponse<()>>> {
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<Result<SerdeBlock, AppError>> = 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::<Result<Vec<SerdeBlock>, 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)>,
Expand Down
88 changes: 86 additions & 2 deletions src/biz/workspace/page_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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<RealtimeServerAddr>,
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<Vec<u8>, 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,
Expand Down
Loading
Loading