From d04a0d8871b84f9fa6d22ced74475d161e3733d8 Mon Sep 17 00:00:00 2001 From: khorshuheng Date: Fri, 8 Nov 2024 10:44:58 +0800 Subject: [PATCH] feat: api for restore page from trash --- libs/client-api/src/http_view.rs | 22 +++++- src/api/workspace.rs | 25 ++++++- src/biz/workspace/page_view.rs | 43 +++++++++++ tests/workspace/page_view.rs | 123 ++++++++++++++++--------------- 4 files changed, 151 insertions(+), 62 deletions(-) diff --git a/libs/client-api/src/http_view.rs b/libs/client-api/src/http_view.rs index 25e51331..dc179b4a 100644 --- a/libs/client-api/src/http_view.rs +++ b/libs/client-api/src/http_view.rs @@ -40,10 +40,28 @@ impl Client { AppResponse::<()>::from_response(resp).await?.into_error() } + pub async fn restore_workspace_page_view_from_trash( + &self, + workspace_id: Uuid, + view_id: &str, + ) -> Result<(), AppResponseError> { + let url = format!( + "{}/api/workspace/{}/page-view/{}/restore-from-trash", + self.base_url, workspace_id, view_id + ); + let resp = self + .http_client_with_auth(Method::POST, &url) + .await? + .json(&json!({})) + .send() + .await?; + AppResponse::<()>::from_response(resp).await?.into_error() + } + pub async fn update_workspace_page_view( &self, workspace_id: Uuid, - view_id: String, + view_id: &str, params: &UpdatePageParams, ) -> Result<(), AppResponseError> { let url = format!( @@ -62,7 +80,7 @@ impl Client { pub async fn get_workspace_page_view( &self, workspace_id: Uuid, - view_id: Uuid, + view_id: &str, ) -> Result { let url = format!( "{}/api/workspace/{}/page-view/{}", diff --git a/src/api/workspace.rs b/src/api/workspace.rs index af8b895d..ad9b31dd 100644 --- a/src/api/workspace.rs +++ b/src/api/workspace.rs @@ -49,7 +49,8 @@ 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, get_page_view_collab, move_page_to_trash, update_page, update_page_collab_data, + create_page, get_page_view_collab, move_page_to_trash, restore_page_from_trash, update_page, + update_page_collab_data, }; use crate::biz::workspace::publish::get_workspace_default_publish_view_info_meta; use crate::domain::compression::{ @@ -138,6 +139,10 @@ pub fn workspace_scope() -> Scope { web::resource("/{workspace_id}/page-view/{view_id}/move-to-trash") .route(web::post().to(move_page_to_trash_handler)), ) + .service( + web::resource("/{workspace_id}/page-view/{view_id}/restore-from-trash") + .route(web::post().to(restore_page_from_trash_handler)), + ) .service( web::resource("/{workspace_id}/batch/collab") .route(web::post().to(batch_create_collab_handler)), @@ -918,6 +923,24 @@ async fn move_page_to_trash_handler( Ok(Json(AppResponse::Ok())) } +async fn restore_page_from_trash_handler( + user_uuid: UserUuid, + path: web::Path<(Uuid, String)>, + state: Data, +) -> Result>> { + let uid = state.user_cache.get_user_uid(&user_uuid).await?; + let (workspace_uuid, view_id) = path.into_inner(); + restore_page_from_trash( + &state.pg_pool, + &state.collab_access_control_storage, + uid, + workspace_uuid, + &view_id, + ) + .await?; + Ok(Json(AppResponse::Ok())) +} + async fn update_page_view_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 b02e8284..30d9cf11 100644 --- a/src/biz/workspace/page_view.rs +++ b/src/biz/workspace/page_view.rs @@ -145,6 +145,25 @@ async fn move_view_to_trash(view_id: &str, folder: &mut Folder) -> Result Result { + let encoded_update = { + let mut txn = folder.collab.transact_mut(); + folder + .body + .views + .update_view(&mut txn, view_id, |update| update.set_trash(false).done()); + txn.encode_update_v1() + }; + + Ok(FolderUpdate { + updated_encoded_collab: folder_to_encoded_collab(folder)?, + encoded_updates: encoded_update, + }) +} + fn folder_to_encoded_collab(folder: &Folder) -> Result, AppError> { let collab_type = CollabType::Folder; let encoded_folder_collab = folder @@ -254,6 +273,30 @@ pub async fn move_page_to_trash( Ok(()) } +pub async fn restore_page_from_trash( + pg_pool: &PgPool, + collab_storage: &CollabAccessControlStorage, + uid: i64, + workspace_id: Uuid, + view_id: &str, +) -> 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_out_from_trash(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(()) +} + #[allow(clippy::too_many_arguments)] pub async fn update_page( pg_pool: &PgPool, diff --git a/tests/workspace/page_view.rs b/tests/workspace/page_view.rs index 51361925..2e3d9cfb 100644 --- a/tests/workspace/page_view.rs +++ b/tests/workspace/page_view.rs @@ -14,6 +14,32 @@ use shared_entity::dto::workspace_dto::{ use tokio::time::sleep; use uuid::Uuid; +async fn get_latest_folder(test_client: &TestClient, workspace_id: &str) -> Folder { + // Wait for websocket updates + sleep(Duration::from_secs(1)).await; + let lock = test_client + .collabs + .get(workspace_id) + .unwrap() + .collab + .read() + .await; + let collab: &Collab = (*lock).borrow(); + let collab_type = CollabType::Folder; + let encoded_collab = collab + .encode_collab_v1(|collab| collab_type.validate_require_data(collab)) + .unwrap(); + let uid = test_client.uid().await; + Folder::from_collab_doc_state( + uid, + CollabOrigin::Client(CollabClient::new(uid, test_client.device_id.clone())), + encoded_collab.into(), + workspace_id, + vec![], + ) + .unwrap() +} + #[tokio::test] async fn get_page_view() { let (c, _user) = generate_unique_registered_user_client().await; @@ -34,7 +60,7 @@ async fn get_page_view() { .iter() .find(|v| v.name == "To-dos") .unwrap(); - let todo_list_view_id = Uuid::parse_str(&todo.view_id).unwrap(); + let todo_list_view_id = &todo.view_id; let resp = c .get_workspace_page_view(workspace_id, todo_list_view_id) .await @@ -45,7 +71,7 @@ async fn get_page_view() { .iter() .find(|v| v.name == "Getting started") .unwrap(); - let getting_started_view_id = Uuid::parse_str(&getting_started.view_id).unwrap(); + let getting_started_view_id = &getting_started.view_id; let resp = c .get_workspace_page_view(workspace_id, getting_started_view_id) .await @@ -110,14 +136,9 @@ async fn move_page_to_trash() { 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; - 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.to_string(), Some(2), None) + .get_workspace_folder(&workspace_id, Some(2), None) .await .unwrap(); let general_space = &folder_view @@ -126,6 +147,11 @@ async fn move_page_to_trash() { .find(|v| v.name == "General") .unwrap(); let view_id_to_be_deleted = general_space.children[0].view_id.clone(); + app_client.open_workspace_collab(&workspace_id).await; + app_client + .wait_object_sync_complete(&workspace_id) + .await + .unwrap(); web_client .api_client .move_workspace_page_view_to_trash( @@ -134,35 +160,12 @@ async fn move_page_to_trash() { ) .await .unwrap(); - - // Wait for websocket to receive update - sleep(Duration::from_secs(1)).await; - let lock = app_client - .collabs - .get(&workspace_id) - .unwrap() - .collab - .read() - .await; - let collab: &Collab = (*lock).borrow(); - let collab_type = CollabType::Folder; - let encoded_collab = collab - .encode_collab_v1(|collab| collab_type.validate_require_data(collab)) - .unwrap(); - let uid = app_client.uid().await; - let folder = Folder::from_collab_doc_state( - uid, - CollabOrigin::Client(CollabClient::new(uid, app_client.device_id.clone())), - encoded_collab.into(), - &workspace_id, - vec![], - ) - .unwrap(); + let folder = get_latest_folder(&app_client, &workspace_id).await; assert!(folder .get_my_trash_sections() .iter() .any(|v| v.id == view_id_to_be_deleted.clone())); - web_client + let view_found = web_client .api_client .get_workspace_trash(&workspace_id) .await @@ -170,6 +173,30 @@ async fn move_page_to_trash() { .views .iter() .any(|v| v.view.view_id == view_id_to_be_deleted.clone()); + assert!(view_found); + + web_client + .api_client + .restore_workspace_page_view_from_trash( + Uuid::parse_str(&workspace_id).unwrap(), + &view_id_to_be_deleted, + ) + .await + .unwrap(); + let folder = get_latest_folder(&app_client, &workspace_id).await; + assert!(!folder + .get_my_trash_sections() + .iter() + .any(|v| v.id == view_id_to_be_deleted.clone())); + let view_found = web_client + .api_client + .get_workspace_trash(&workspace_id) + .await + .unwrap() + .views + .iter() + .any(|v| v.view.view_id == view_id_to_be_deleted.clone()); + assert!(!view_found); } #[tokio::test] @@ -198,42 +225,20 @@ async fn update_page() { .api_client .update_workspace_page_view( Uuid::parse_str(&workspace_id).unwrap(), - view_id_to_be_updated.clone(), + &view_id_to_be_updated, &UpdatePageParams { name: "New Name".to_string(), icon: Some(ViewIcon { ty: IconType::Emoji, value: "🚀".to_string(), }), - extra: Some(json!({"key": "value"})), + extra: Some(json!({"is_pinned": true})), }, ) .await .unwrap(); - // Wait for websocket to receive update - sleep(Duration::from_secs(1)).await; - let lock = app_client - .collabs - .get(&workspace_id) - .unwrap() - .collab - .read() - .await; - let collab: &Collab = (*lock).borrow(); - let collab_type = CollabType::Folder; - let encoded_collab = collab - .encode_collab_v1(|collab| collab_type.validate_require_data(collab)) - .unwrap(); - let uid = app_client.uid().await; - let folder = Folder::from_collab_doc_state( - uid, - CollabOrigin::Client(CollabClient::new(uid, app_client.device_id.clone())), - encoded_collab.into(), - &workspace_id, - vec![], - ) - .unwrap(); + let folder = get_latest_folder(&app_client, &workspace_id).await; let updated_view = folder.get_view(&view_id_to_be_updated).unwrap(); assert_eq!(updated_view.name, "New Name"); assert_eq!( @@ -245,6 +250,6 @@ async fn update_page() { ); assert_eq!( updated_view.extra, - Some(json!({"key": "value"}).to_string()) + Some(json!({"is_pinned": true}).to_string()) ); }