From 0a10aff86fea59fe03a70fabea1639266aad7c07 Mon Sep 17 00:00:00 2001 From: Khor Shu Heng <32997938+khorshuheng@users.noreply.github.com> Date: Wed, 11 Dec 2024 17:16:59 +0800 Subject: [PATCH] feat: api for moving page (#1060) --- libs/client-api/src/http_view.rs | 22 ++++++- libs/shared-entity/src/dto/workspace_dto.rs | 6 ++ src/api/workspace.rs | 27 ++++++++- src/biz/workspace/page_view.rs | 63 ++++++++++++++++++--- tests/workspace/page_view.rs | 56 +++++++++++++++++- 5 files changed, 162 insertions(+), 12 deletions(-) diff --git a/libs/client-api/src/http_view.rs b/libs/client-api/src/http_view.rs index 1bf56e7b..fefcfbaf 100644 --- a/libs/client-api/src/http_view.rs +++ b/libs/client-api/src/http_view.rs @@ -1,5 +1,6 @@ use client_api_entity::workspace_dto::{ - CreatePageParams, CreateSpaceParams, Page, PageCollab, Space, UpdatePageParams, UpdateSpaceParams, + CreatePageParams, CreateSpaceParams, MovePageParams, Page, PageCollab, Space, UpdatePageParams, + UpdateSpaceParams, }; use reqwest::Method; use serde_json::json; @@ -24,6 +25,25 @@ impl Client { AppResponse::::from_response(resp).await?.into_data() } + pub async fn move_workspace_page_view( + &self, + workspace_id: Uuid, + view_id: &str, + params: &MovePageParams, + ) -> Result<(), AppResponseError> { + let url = format!( + "{}/api/workspace/{}/page-view/{}/move", + 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 move_workspace_page_view_to_trash( &self, workspace_id: Uuid, diff --git a/libs/shared-entity/src/dto/workspace_dto.rs b/libs/shared-entity/src/dto/workspace_dto.rs index 89ca77d7..1531eb76 100644 --- a/libs/shared-entity/src/dto/workspace_dto.rs +++ b/libs/shared-entity/src/dto/workspace_dto.rs @@ -179,6 +179,12 @@ pub struct UpdatePageParams { pub extra: Option, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MovePageParams { + pub new_parent_view_id: String, + pub prev_view_id: Option, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PageCollabData { pub encoded_collab: Vec, diff --git a/src/api/workspace.rs b/src/api/workspace.rs index 5ca11d12..53f52c9b 100644 --- a/src/api/workspace.rs +++ b/src/api/workspace.rs @@ -52,7 +52,7 @@ 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, get_page_view_collab, move_page_to_trash, + create_page, create_space, get_page_view_collab, move_page, move_page_to_trash, restore_all_pages_from_trash, restore_page_from_trash, update_page, update_page_collab_data, update_space, }; @@ -143,6 +143,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}/move") + .route(web::post().to(move_page_handler)), + ) .service( web::resource("/{workspace_id}/page-view/{view_id}/move-to-trash") .route(web::post().to(move_page_to_trash_handler)), @@ -996,6 +1000,27 @@ async fn post_page_view_handler( Ok(Json(AppResponse::Ok().with_data(page))) } +async fn move_page_handler( + user_uuid: UserUuid, + path: web::Path<(Uuid, String)>, + payload: Json, + state: Data, +) -> Result>> { + let uid = state.user_cache.get_user_uid(&user_uuid).await?; + let (workspace_uuid, view_id) = path.into_inner(); + move_page( + &state.pg_pool, + &state.collab_access_control_storage, + uid, + workspace_uuid, + &view_id, + &payload.new_parent_view_id, + payload.prev_view_id.clone(), + ) + .await?; + Ok(Json(AppResponse::Ok())) +} + async fn move_page_to_trash_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 739a88d4..1ae69744 100644 --- a/src/biz/workspace/page_view.rs +++ b/src/biz/workspace/page_view.rs @@ -60,7 +60,7 @@ struct WorkspaceDatabaseUpdate { struct FolderUpdate { pub updated_encoded_collab: Vec, - pub encoded_updates: Vec, + pub encoded_update: Vec, } #[allow(clippy::too_many_arguments)] @@ -436,7 +436,7 @@ async fn add_new_space_to_folder( }; Ok(FolderUpdate { updated_encoded_collab: folder_to_encoded_collab(folder)?, - encoded_updates: encoded_update, + encoded_update, }) } @@ -470,7 +470,7 @@ async fn update_space_properties( }; Ok(FolderUpdate { updated_encoded_collab: folder_to_encoded_collab(folder)?, - encoded_updates: encoded_update, + encoded_update, }) } @@ -513,7 +513,7 @@ async fn add_new_view_to_folder( }; Ok(FolderUpdate { updated_encoded_collab: folder_to_encoded_collab(folder)?, - encoded_updates: encoded_update, + encoded_update, }) } @@ -538,7 +538,26 @@ async fn update_view_properties( }; Ok(FolderUpdate { updated_encoded_collab: folder_to_encoded_collab(folder)?, - encoded_updates: encoded_update, + encoded_update, + }) +} + +async fn move_view( + view_id: &str, + new_parent_view_id: &str, + prev_view_id: Option, + folder: &mut Folder, +) -> Result { + let encoded_update = { + let mut txn = folder.collab.transact_mut(); + folder + .body + .move_nested_view(&mut txn, view_id, new_parent_view_id, prev_view_id); + txn.encode_update_v1() + }; + Ok(FolderUpdate { + updated_encoded_collab: folder_to_encoded_collab(folder)?, + encoded_update, }) } @@ -566,7 +585,7 @@ async fn move_view_to_trash(view_id: &str, folder: &mut Folder) -> Result Result, +) -> Result<(), AppError> { + let collab_origin = GetCollabOrigin::User { uid }; + let mut folder = + get_latest_collab_folder(collab_storage, collab_origin, &workspace_id.to_string()).await?; + let folder_update = move_view(view_id, new_parent_view_id, prev_view_id, &mut folder).await?; + let mut transaction = pg_pool.begin().await?; + insert_and_broadcast_workspace_folder_update( + uid, + workspace_id, + folder_update, + collab_storage, + &mut transaction, + ) + .await?; + transaction.commit().await?; + Ok(()) +} + pub async fn move_page_to_trash( pg_pool: &PgPool, collab_storage: &CollabAccessControlStorage, diff --git a/tests/workspace/page_view.rs b/tests/workspace/page_view.rs index 76fb0f62..5a684e67 100644 --- a/tests/workspace/page_view.rs +++ b/tests/workspace/page_view.rs @@ -9,7 +9,7 @@ use collab_entity::CollabType; use collab_folder::{CollabOrigin, Folder}; use serde_json::{json, Value}; use shared_entity::dto::workspace_dto::{ - CreatePageParams, CreateSpaceParams, IconType, SpacePermission, UpdatePageParams, + CreatePageParams, CreateSpaceParams, IconType, MovePageParams, SpacePermission, UpdatePageParams, UpdateSpaceParams, ViewIcon, ViewLayout, }; use tokio::time::sleep; @@ -208,6 +208,60 @@ async fn create_new_document_page() { .unwrap(); } +#[tokio::test] +async fn move_page_to_another_space() { + let registered_user = generate_unique_registered_user().await; + let mut app_client = TestClient::user_with_new_device(registered_user.clone()).await; + let web_client = TestClient::user_with_new_device(registered_user.clone()).await; + let workspace_id = app_client.workspace_id().await; + let workspace_uuid = Uuid::parse_str(&workspace_id).unwrap(); + app_client.open_workspace_collab(&workspace_id).await; + app_client + .wait_object_sync_complete(&workspace_id) + .await + .unwrap(); + let folder_view = web_client + .api_client + .get_workspace_folder(&workspace_id, Some(2), None) + .await + .unwrap(); + let general_space = folder_view + .children + .iter() + .find(|v| v.name == "General") + .unwrap() + .clone(); + let todo_view_id = general_space + .children + .iter() + .find(|v| v.name == "To-dos") + .map(|v| v.view_id.clone()) + .unwrap(); + let shared_space = &folder_view + .children + .iter() + .find(|v| v.name == "Shared") + .unwrap() + .clone(); + web_client + .api_client + .move_workspace_page_view( + workspace_uuid, + &todo_view_id, + &MovePageParams { + new_parent_view_id: shared_space.view_id.clone(), + prev_view_id: None, + }, + ) + .await + .unwrap(); + let folder = get_latest_folder(&app_client, &workspace_id).await; + let first_children_id = folder.get_view(&shared_space.view_id).unwrap().children[0] + .id + .clone(); + assert_eq!(first_children_id, todo_view_id); +} + #[tokio::test] async fn move_page_to_trash() { let registered_user = generate_unique_registered_user().await;