diff --git a/libs/client-api/src/http_view.rs b/libs/client-api/src/http_view.rs index 14101253..a5b55cd6 100644 --- a/libs/client-api/src/http_view.rs +++ b/libs/client-api/src/http_view.rs @@ -1,4 +1,6 @@ -use client_api_entity::workspace_dto::{CreatePageParams, Page, PageCollab, UpdatePageParams}; +use client_api_entity::workspace_dto::{ + CreatePageParams, CreateSpaceParams, Page, PageCollab, Space, UpdatePageParams, +}; use reqwest::Method; use serde_json::json; use shared_entity::response::{AppResponse, AppResponseError}; @@ -112,4 +114,19 @@ impl Client { .await? .into_data() } + + pub async fn create_space( + &self, + workspace_id: Uuid, + params: &CreateSpaceParams, + ) -> Result { + let url = format!("{}/api/workspace/{}/space", self.base_url, workspace_id,); + let resp = self + .http_client_with_auth(Method::POST, &url) + .await? + .json(params) + .send() + .await?; + AppResponse::::from_response(resp).await?.into_data() + } } diff --git a/libs/shared-entity/src/dto/workspace_dto.rs b/libs/shared-entity/src/dto/workspace_dto.rs index 67d46ffe..63eae514 100644 --- a/libs/shared-entity/src/dto/workspace_dto.rs +++ b/libs/shared-entity/src/dto/workspace_dto.rs @@ -123,11 +123,24 @@ pub struct CollabResponse { pub object_id: String, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Space { + pub view_id: String, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Page { pub view_id: String, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CreateSpaceParams { + pub space_permission: SpacePermission, + pub name: String, + pub space_icon: String, + pub space_icon_color: String, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CreatePageParams { pub parent_view_id: String, @@ -259,6 +272,13 @@ impl Default for ViewLayout { } } +#[derive(Eq, PartialEq, Debug, Hash, Clone, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum SpacePermission { + PublicToAll = 0, + Private = 1, +} + #[derive(Default, Debug, Deserialize, Serialize)] pub struct QueryWorkspaceParam { pub include_member_count: Option, diff --git a/src/api/workspace.rs b/src/api/workspace.rs index 85060345..97d12bcb 100644 --- a/src/api/workspace.rs +++ b/src/api/workspace.rs @@ -49,8 +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, restore_all_pages_from_trash, - restore_page_from_trash, update_page, update_page_collab_data, + create_page, create_space, get_page_view_collab, move_page_to_trash, + restore_all_pages_from_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::{ @@ -127,6 +127,7 @@ pub fn workspace_scope() -> Scope { web::resource("/v1/{workspace_id}/collab/{object_id}/web-update") .route(web::post().to(post_web_update_handler)), ) + .service(web::resource("/{workspace_id}/space").route(web::post().to(post_space_handler))) .service( web::resource("/{workspace_id}/page-view").route(web::post().to(post_page_view_handler)), ) @@ -901,6 +902,28 @@ async fn post_web_update_handler( Ok(Json(AppResponse::Ok())) } +async fn post_space_handler( + user_uuid: UserUuid, + path: web::Path, + payload: Json, + state: Data, +) -> Result>> { + let uid = state.user_cache.get_user_uid(&user_uuid).await?; + let workspace_uuid = path.into_inner(); + let space = create_space( + &state.pg_pool, + &state.collab_access_control_storage, + uid, + workspace_uuid, + &payload.space_permission, + &payload.name, + &payload.space_icon, + &payload.space_icon_color, + ) + .await?; + Ok(Json(AppResponse::Ok().with_data(space))) +} + async fn post_page_view_handler( user_uuid: UserUuid, path: web::Path, diff --git a/src/biz/collab/folder_view.rs b/src/biz/collab/folder_view.rs index 783b22a7..46b77b8b 100644 --- a/src/biz/collab/folder_view.rs +++ b/src/biz/collab/folder_view.rs @@ -2,7 +2,9 @@ use std::collections::HashSet; use app_error::AppError; use chrono::DateTime; -use collab_folder::{Folder, SectionItem, ViewLayout as CollabFolderViewLayout}; +use collab_folder::{ + hierarchy_builder::SpacePermission, Folder, SectionItem, ViewLayout as CollabFolderViewLayout, +}; use shared_entity::dto::workspace_dto::{ self, FavoriteFolderView, FolderView, FolderViewMinimal, RecentFolderView, TrashFolderView, ViewLayout, @@ -301,3 +303,10 @@ pub fn to_folder_view_layout(layout: workspace_dto::ViewLayout) -> collab_folder ViewLayout::Chat => collab_folder::ViewLayout::Chat, } } + +pub fn to_space_permission(space_permission: &workspace_dto::SpacePermission) -> SpacePermission { + match space_permission { + workspace_dto::SpacePermission::PublicToAll => SpacePermission::PublicToAll, + workspace_dto::SpacePermission::Private => SpacePermission::Private, + } +} diff --git a/src/biz/workspace/page_view.rs b/src/biz/workspace/page_view.rs index 5c837174..ef61a202 100644 --- a/src/biz/workspace/page_view.rs +++ b/src/biz/workspace/page_view.rs @@ -16,8 +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 serde_json::json; use shared_entity::dto::workspace_dto::{ - FolderView, Page, PageCollab, PageCollabData, ViewIcon, ViewLayout, + FolderView, Page, PageCollab, PageCollabData, Space, SpacePermission, ViewIcon, ViewLayout, }; use sqlx::{PgPool, Transaction}; use std::collections::{HashMap, HashSet}; @@ -29,6 +30,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, to_folder_view_icon, + to_space_permission, }; use crate::biz::collab::{ folder_view::view_is_space, @@ -42,6 +44,56 @@ struct FolderUpdate { pub encoded_updates: Vec, } +#[allow(clippy::too_many_arguments)] +pub async fn create_space( + pg_pool: &PgPool, + collab_storage: &CollabAccessControlStorage, + uid: i64, + workspace_id: Uuid, + space_permission: &SpacePermission, + name: &str, + space_icon: &str, + space_color: &str, +) -> Result { + let default_document_collab_params = prepare_default_document_collab_param()?; + let view_id = default_document_collab_params.object_id.clone(); + 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 = add_new_space_to_folder( + uid, + &workspace_id.to_string(), + &view_id, + &mut folder, + space_permission, + name, + space_icon, + space_color, + ) + .await?; + let mut transaction = pg_pool.begin().await?; + let action = format!("Create new space: {}", view_id); + collab_storage + .insert_new_collab_with_transaction( + &workspace_id.to_string(), + &uid, + default_document_collab_params, + &mut transaction, + &action, + ) + .await?; + insert_and_broadcast_workspace_folder_update( + uid, + workspace_id, + folder_update, + collab_storage, + &mut transaction, + ) + .await?; + transaction.commit().await?; + Ok(Space { view_id }) +} + pub async fn create_page( pg_pool: &PgPool, collab_storage: &CollabAccessControlStorage, @@ -84,6 +136,47 @@ fn prepare_default_document_collab_param() -> Result { }) } +#[allow(clippy::too_many_arguments)] +async fn add_new_space_to_folder( + uid: i64, + workspace_id: &str, + view_id: &str, + folder: &mut Folder, + space_permission: &SpacePermission, + name: &str, + space_icon: &str, + space_color: &str, +) -> Result { + let encoded_update = { + let view = NestedChildViewBuilder::new(uid, workspace_id.to_string()) + .with_view_id(view_id) + .with_name(name) + .with_extra(|builder| { + let mut extra = builder + .is_space(true, to_space_permission(space_permission)) + .build(); + extra["space_icon_color"] = json!(space_color); + extra["space_icon"] = json!(space_icon); + extra + }) + .build() + .view; + let mut txn = folder.collab.transact_mut(); + folder.body.views.insert(&mut txn, view, None); + if *space_permission == SpacePermission::Private { + folder + .body + .views + .update_view(&mut txn, view_id, |update| update.set_private(true).done()); + } + txn.encode_update_v1() + }; + Ok(FolderUpdate { + updated_encoded_collab: folder_to_encoded_collab(folder)?, + encoded_updates: encoded_update, + }) +} + async fn add_new_view_to_folder( uid: i64, parent_view_id: &str, @@ -99,6 +192,7 @@ async fn add_new_view_to_folder( .view; let mut txn = folder.collab.transact_mut(); folder.body.views.insert(&mut txn, view, None); + txn.encode_update_v1() }; Ok(FolderUpdate { diff --git a/tests/workspace/page_view.rs b/tests/workspace/page_view.rs index 461c83d6..88734fed 100644 --- a/tests/workspace/page_view.rs +++ b/tests/workspace/page_view.rs @@ -7,9 +7,10 @@ use client_api_test::{ use collab::{core::origin::CollabClient, preclude::Collab}; use collab_entity::CollabType; use collab_folder::{CollabOrigin, Folder}; -use serde_json::json; +use serde_json::{json, Value}; use shared_entity::dto::workspace_dto::{ - CreatePageParams, IconType, UpdatePageParams, ViewIcon, ViewLayout, + CreatePageParams, CreateSpaceParams, IconType, SpacePermission, UpdatePageParams, ViewIcon, + ViewLayout, }; use tokio::time::sleep; use uuid::Uuid; @@ -283,3 +284,72 @@ async fn update_page() { Some(json!({"is_pinned": true}).to_string()) ); } + +#[tokio::test] +async fn create_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; + app_client.open_workspace_collab(&workspace_id).await; + app_client + .wait_object_sync_complete(&workspace_id) + .await + .unwrap(); + let workspace_uuid = Uuid::parse_str(&workspace_id).unwrap(); + let public_space = web_client + .api_client + .create_space( + workspace_uuid, + &CreateSpaceParams { + space_permission: SpacePermission::PublicToAll, + name: "Public Space".to_string(), + space_icon: "space_icon_1".to_string(), + space_icon_color: "0xFFA34AFD".to_string(), + }, + ) + .await + .unwrap(); + web_client + .api_client + .create_space( + workspace_uuid, + &CreateSpaceParams { + space_permission: SpacePermission::Private, + name: "Private Space".to_string(), + space_icon: "space_icon_2".to_string(), + space_icon_color: "0xFFA34AFD".to_string(), + }, + ) + .await + .unwrap(); + let folder = get_latest_folder(&app_client, &workspace_id).await; + let view = folder.get_view(&public_space.view_id).unwrap(); + let space_info: Value = serde_json::from_str(view.extra.as_ref().unwrap()).unwrap(); + assert!(space_info["is_space"].as_bool().unwrap()); + assert_eq!( + space_info["space_permission"].as_u64().unwrap() as u8, + SpacePermission::PublicToAll as u8 + ); + assert_eq!(space_info["space_icon"].as_str().unwrap(), "space_icon_1"); + assert_eq!( + space_info["space_icon_color"].as_str().unwrap(), + "0xFFA34AFD" + ); + let folder_view = web_client + .api_client + .get_workspace_folder(&workspace_id, Some(2), Some(workspace_id.to_string())) + .await + .unwrap(); + folder_view + .children + .iter() + .find(|v| v.name == "Public Space") + .unwrap(); + let private_space = folder_view + .children + .iter() + .find(|v| v.name == "Private Space") + .unwrap(); + assert!(private_space.is_private); +}