diff --git a/libs/client-api/src/http_view.rs b/libs/client-api/src/http_view.rs index fcfd98a3..25e51331 100644 --- a/libs/client-api/src/http_view.rs +++ b/libs/client-api/src/http_view.rs @@ -1,4 +1,4 @@ -use client_api_entity::workspace_dto::{CreatePageParams, Page, PageCollab}; +use client_api_entity::workspace_dto::{CreatePageParams, Page, PageCollab, UpdatePageParams}; use reqwest::Method; use serde_json::json; use shared_entity::response::{AppResponse, AppResponseError}; @@ -40,6 +40,25 @@ impl Client { AppResponse::<()>::from_response(resp).await?.into_error() } + pub async fn update_workspace_page_view( + &self, + workspace_id: Uuid, + view_id: String, + params: &UpdatePageParams, + ) -> Result<(), AppResponseError> { + let url = format!( + "{}/api/workspace/{}/page-view/{}", + self.base_url, workspace_id, view_id + ); + let resp = self + .http_client_with_auth(Method::PATCH, &url) + .await? + .json(params) + .send() + .await?; + AppResponse::<()>::from_response(resp).await?.into_error() + } + pub async fn get_workspace_page_view( &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 c0429f32..629cf87c 100644 --- a/libs/shared-entity/src/dto/workspace_dto.rs +++ b/libs/shared-entity/src/dto/workspace_dto.rs @@ -2,6 +2,7 @@ use chrono::{DateTime, Utc}; use collab_entity::{CollabType, EncodedCollab}; use database_entity::dto::{AFRole, AFWebUser, AFWorkspaceInvitationStatus, PublishInfo}; use serde::{Deserialize, Serialize}; +use serde_json::Value; use serde_repr::{Deserialize_repr, Serialize_repr}; use std::{collections::HashMap, ops::Deref}; use uuid::Uuid; @@ -133,6 +134,13 @@ pub struct CreatePageParams { pub layout: ViewLayout, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UpdatePageParams { + pub name: String, + pub icon: Option, + pub extra: 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 90866431..af8b895d 100644 --- a/src/api/workspace.rs +++ b/src/api/workspace.rs @@ -49,7 +49,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, get_page_view_collab, move_page_to_trash, update_page_collab_data, + create_page, get_page_view_collab, move_page_to_trash, update_page, update_page_collab_data, }; use crate::biz::workspace::publish::get_workspace_default_publish_view_info_meta; use crate::domain::compression::{ @@ -131,7 +131,8 @@ pub fn workspace_scope() -> Scope { ) .service( web::resource("/{workspace_id}/page-view/{view_id}") - .route(web::get().to(get_page_view_handler)), + .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-to-trash") @@ -917,6 +918,33 @@ async fn move_page_to_trash_handler( Ok(Json(AppResponse::Ok())) } +async fn update_page_view_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(); + let icon = payload.icon.as_ref(); + let extra = payload + .extra + .as_ref() + .map(|json_value| json_value.to_string()); + update_page( + &state.pg_pool, + &state.collab_access_control_storage, + uid, + workspace_uuid, + &view_id, + &payload.name, + icon, + extra.as_ref(), + ) + .await?; + Ok(Json(AppResponse::Ok())) +} + async fn get_page_view_handler( user_uuid: UserUuid, path: web::Path<(Uuid, String)>, diff --git a/src/biz/collab/folder_view.rs b/src/biz/collab/folder_view.rs index f1051f35..783b22a7 100644 --- a/src/biz/collab/folder_view.rs +++ b/src/biz/collab/folder_view.rs @@ -4,7 +4,8 @@ use app_error::AppError; use chrono::DateTime; use collab_folder::{Folder, SectionItem, ViewLayout as CollabFolderViewLayout}; use shared_entity::dto::workspace_dto::{ - FavoriteFolderView, FolderView, FolderViewMinimal, RecentFolderView, TrashFolderView, ViewLayout, + self, FavoriteFolderView, FolderView, FolderViewMinimal, RecentFolderView, TrashFolderView, + ViewLayout, }; /// Return all folders belonging to a workspace, excluding private sections which the user does not have access to. @@ -275,3 +276,28 @@ pub fn to_dto_folder_view_miminal(collab_folder_view: &collab_folder::View) -> F layout: to_dto_view_layout(&collab_folder_view.layout), } } + +pub fn to_folder_view_icon(icon: workspace_dto::ViewIcon) -> collab_folder::ViewIcon { + collab_folder::ViewIcon { + ty: to_folder_view_icon_type(icon.ty), + value: icon.value, + } +} + +pub fn to_folder_view_icon_type(icon: workspace_dto::IconType) -> collab_folder::IconType { + match icon { + workspace_dto::IconType::Emoji => collab_folder::IconType::Emoji, + workspace_dto::IconType::Url => collab_folder::IconType::Url, + workspace_dto::IconType::Icon => collab_folder::IconType::Icon, + } +} + +pub fn to_folder_view_layout(layout: workspace_dto::ViewLayout) -> collab_folder::ViewLayout { + match layout { + ViewLayout::Document => collab_folder::ViewLayout::Document, + ViewLayout::Grid => collab_folder::ViewLayout::Grid, + ViewLayout::Board => collab_folder::ViewLayout::Board, + ViewLayout::Calendar => collab_folder::ViewLayout::Calendar, + ViewLayout::Chat => collab_folder::ViewLayout::Chat, + } +} diff --git a/src/biz/workspace/page_view.rs b/src/biz/workspace/page_view.rs index 5781003a..b02e8284 100644 --- a/src/biz/workspace/page_view.rs +++ b/src/biz/workspace/page_view.rs @@ -16,7 +16,9 @@ use database::user::select_web_user_from_uid; use database_entity::dto::{CollabParams, QueryCollab, QueryCollabParams, QueryCollabResult}; use itertools::Itertools; use rayon::iter::{IntoParallelIterator, ParallelIterator}; -use shared_entity::dto::workspace_dto::{FolderView, Page, PageCollab, PageCollabData, ViewLayout}; +use shared_entity::dto::workspace_dto::{ + FolderView, Page, PageCollab, PageCollabData, ViewIcon, ViewLayout, +}; use sqlx::{PgPool, Transaction}; use std::collections::{HashMap, HashSet}; use std::sync::Arc; @@ -26,7 +28,7 @@ use yrs::Update; use crate::api::metrics::AppFlowyWebMetrics; use crate::biz::collab::folder_view::{ - parse_extra_field_as_json, to_dto_view_icon, to_dto_view_layout, + parse_extra_field_as_json, to_dto_view_icon, to_dto_view_layout, to_folder_view_icon, }; use crate::biz::collab::{ folder_view::view_is_space, @@ -94,6 +96,31 @@ async fn add_new_view_to_folder( }) } +async fn update_view_properties( + view_id: &str, + folder: &mut Folder, + name: &str, + icon: Option<&ViewIcon>, + extra: Option>, +) -> Result { + let encoded_update = { + let mut txn = folder.collab.transact_mut(); + let icon = icon.map(|icon| to_folder_view_icon(icon.clone())); + folder.body.views.update_view(&mut txn, view_id, |update| { + update + .set_name(name) + .set_icon(icon) + .set_extra_if_not_none(extra) + .done() + }); + txn.encode_update_v1() + }; + Ok(FolderUpdate { + updated_encoded_collab: folder_to_encoded_collab(folder)?, + encoded_updates: encoded_update, + }) +} + async fn move_view_to_trash(view_id: &str, folder: &mut Folder) -> Result { let mut current_view_and_descendants = folder .get_views_belong_to(view_id) @@ -227,6 +254,35 @@ pub async fn move_page_to_trash( Ok(()) } +#[allow(clippy::too_many_arguments)] +pub async fn update_page( + pg_pool: &PgPool, + collab_storage: &CollabAccessControlStorage, + uid: i64, + workspace_id: Uuid, + view_id: &str, + name: &str, + icon: Option<&ViewIcon>, + extra: Option>, +) -> 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 = update_view_properties(view_id, &mut folder, name, icon, extra).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 get_page_view_collab( pg_pool: &PgPool, collab_access_control_storage: &CollabAccessControlStorage, diff --git a/src/biz/workspace/publish_dup.rs b/src/biz/workspace/publish_dup.rs index e3438937..c904d425 100644 --- a/src/biz/workspace/publish_dup.rs +++ b/src/biz/workspace/publish_dup.rs @@ -27,7 +27,6 @@ use database::publish::select_published_data_for_view_id; use database::publish::select_published_metadata_for_view_id; use database_entity::dto::CollabParams; use shared_entity::dto::publish_dto::{PublishDatabaseData, PublishViewInfo, PublishViewMetaData}; -use shared_entity::dto::workspace_dto; use shared_entity::dto::workspace_dto::ViewLayout; use sqlx::PgPool; use std::collections::HashSet; @@ -42,6 +41,8 @@ use yrs::ArrayRef; use yrs::Out; use yrs::{Map, MapRef}; +use crate::biz::collab::folder_view::to_folder_view_icon; +use crate::biz::collab::folder_view::to_folder_view_layout; use crate::biz::collab::ops::get_latest_collab_encoded; use super::ops::broadcast_update; @@ -1171,31 +1172,6 @@ fn add_to_view_info(acc: &mut HashMap, view_infos: &[Pu } } -fn to_folder_view_icon(icon: workspace_dto::ViewIcon) -> collab_folder::ViewIcon { - collab_folder::ViewIcon { - ty: to_folder_view_icon_type(icon.ty), - value: icon.value, - } -} - -fn to_folder_view_icon_type(icon: workspace_dto::IconType) -> collab_folder::IconType { - match icon { - workspace_dto::IconType::Emoji => collab_folder::IconType::Emoji, - workspace_dto::IconType::Url => collab_folder::IconType::Url, - workspace_dto::IconType::Icon => collab_folder::IconType::Icon, - } -} - -fn to_folder_view_layout(layout: workspace_dto::ViewLayout) -> collab_folder::ViewLayout { - match layout { - ViewLayout::Document => collab_folder::ViewLayout::Document, - ViewLayout::Grid => collab_folder::ViewLayout::Grid, - ViewLayout::Board => collab_folder::ViewLayout::Board, - ViewLayout::Calendar => collab_folder::ViewLayout::Calendar, - ViewLayout::Chat => collab_folder::ViewLayout::Chat, - } -} - async fn collab_to_bin(collab: Collab, collab_type: CollabType) -> Result, AppError> { tokio::task::spawn_blocking(move || { let bin = collab diff --git a/tests/workspace/page_view.rs b/tests/workspace/page_view.rs index 9e3ac641..51361925 100644 --- a/tests/workspace/page_view.rs +++ b/tests/workspace/page_view.rs @@ -7,7 +7,10 @@ use client_api_test::{ use collab::{core::origin::CollabClient, preclude::Collab}; use collab_entity::CollabType; use collab_folder::{CollabOrigin, Folder}; -use shared_entity::dto::workspace_dto::{CreatePageParams, ViewLayout}; +use serde_json::json; +use shared_entity::dto::workspace_dto::{ + CreatePageParams, IconType, UpdatePageParams, ViewIcon, ViewLayout, +}; use tokio::time::sleep; use uuid::Uuid; @@ -168,3 +171,80 @@ async fn move_page_to_trash() { .iter() .any(|v| v.view.view_id == view_id_to_be_deleted.clone()); } + +#[tokio::test] +async fn update_page() { + 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; + 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) + .await + .unwrap(); + let general_space = &folder_view + .children + .into_iter() + .find(|v| v.name == "General") + .unwrap(); + let view_id_to_be_updated = general_space.children[0].view_id.clone(); + web_client + .api_client + .update_workspace_page_view( + Uuid::parse_str(&workspace_id).unwrap(), + view_id_to_be_updated.clone(), + &UpdatePageParams { + name: "New Name".to_string(), + icon: Some(ViewIcon { + ty: IconType::Emoji, + value: "🚀".to_string(), + }), + extra: Some(json!({"key": "value"})), + }, + ) + .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 updated_view = folder.get_view(&view_id_to_be_updated).unwrap(); + assert_eq!(updated_view.name, "New Name"); + assert_eq!( + updated_view.icon, + Some(collab_folder::ViewIcon { + ty: collab_folder::IconType::Emoji, + value: "🚀".to_string(), + }) + ); + assert_eq!( + updated_view.extra, + Some(json!({"key": "value"}).to_string()) + ); +}