diff --git a/Cargo.lock b/Cargo.lock index 1e9082fe..8576fb1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2171,7 +2171,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=be6bb90faac22ca8443a950ea3deafc7ec99b3a8#be6bb90faac22ca8443a950ea3deafc7ec99b3a8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0efc824a6e1a56e4485646e6428c07fdccf6e918#0efc824a6e1a56e4485646e6428c07fdccf6e918" dependencies = [ "anyhow", "arc-swap", @@ -2196,7 +2196,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=be6bb90faac22ca8443a950ea3deafc7ec99b3a8#be6bb90faac22ca8443a950ea3deafc7ec99b3a8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0efc824a6e1a56e4485646e6428c07fdccf6e918#0efc824a6e1a56e4485646e6428c07fdccf6e918" dependencies = [ "anyhow", "async-trait", @@ -2235,7 +2235,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=be6bb90faac22ca8443a950ea3deafc7ec99b3a8#be6bb90faac22ca8443a950ea3deafc7ec99b3a8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0efc824a6e1a56e4485646e6428c07fdccf6e918#0efc824a6e1a56e4485646e6428c07fdccf6e918" dependencies = [ "anyhow", "arc-swap", @@ -2256,7 +2256,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=be6bb90faac22ca8443a950ea3deafc7ec99b3a8#be6bb90faac22ca8443a950ea3deafc7ec99b3a8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0efc824a6e1a56e4485646e6428c07fdccf6e918#0efc824a6e1a56e4485646e6428c07fdccf6e918" dependencies = [ "anyhow", "bytes", @@ -2276,7 +2276,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=be6bb90faac22ca8443a950ea3deafc7ec99b3a8#be6bb90faac22ca8443a950ea3deafc7ec99b3a8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0efc824a6e1a56e4485646e6428c07fdccf6e918#0efc824a6e1a56e4485646e6428c07fdccf6e918" dependencies = [ "anyhow", "arc-swap", @@ -2298,7 +2298,7 @@ dependencies = [ [[package]] name = "collab-importer" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=be6bb90faac22ca8443a950ea3deafc7ec99b3a8#be6bb90faac22ca8443a950ea3deafc7ec99b3a8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0efc824a6e1a56e4485646e6428c07fdccf6e918#0efc824a6e1a56e4485646e6428c07fdccf6e918" dependencies = [ "anyhow", "async-recursion", @@ -2401,7 +2401,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=be6bb90faac22ca8443a950ea3deafc7ec99b3a8#be6bb90faac22ca8443a950ea3deafc7ec99b3a8" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0efc824a6e1a56e4485646e6428c07fdccf6e918#0efc824a6e1a56e4485646e6428c07fdccf6e918" dependencies = [ "anyhow", "collab", diff --git a/Cargo.toml b/Cargo.toml index 1a1d72ed..28b964af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -304,18 +304,18 @@ debug = true [profile.ci] inherits = "release" opt-level = 2 -lto = false # Disable Link-Time Optimization +lto = false # Disable Link-Time Optimization [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 = "be6bb90faac22ca8443a950ea3deafc7ec99b3a8" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "be6bb90faac22ca8443a950ea3deafc7ec99b3a8" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "be6bb90faac22ca8443a950ea3deafc7ec99b3a8" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "be6bb90faac22ca8443a950ea3deafc7ec99b3a8" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "be6bb90faac22ca8443a950ea3deafc7ec99b3a8" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "be6bb90faac22ca8443a950ea3deafc7ec99b3a8" } -collab-importer = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "be6bb90faac22ca8443a950ea3deafc7ec99b3a8" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0efc824a6e1a56e4485646e6428c07fdccf6e918" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0efc824a6e1a56e4485646e6428c07fdccf6e918" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0efc824a6e1a56e4485646e6428c07fdccf6e918" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0efc824a6e1a56e4485646e6428c07fdccf6e918" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0efc824a6e1a56e4485646e6428c07fdccf6e918" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0efc824a6e1a56e4485646e6428c07fdccf6e918" } +collab-importer = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0efc824a6e1a56e4485646e6428c07fdccf6e918" } [features] history = [] diff --git a/libs/database/src/collab/collab_storage.rs b/libs/database/src/collab/collab_storage.rs index a4ef1298..8f6ab8af 100644 --- a/libs/database/src/collab/collab_storage.rs +++ b/libs/database/src/collab/collab_storage.rs @@ -49,6 +49,7 @@ pub trait CollabStorageAccessControl: Send + Sync + 'static { async fn enforce_delete(&self, workspace_id: &str, uid: &i64, oid: &str) -> Result<(), AppError>; } +#[derive(Clone)] pub enum GetCollabOrigin { User { uid: i64 }, Server, diff --git a/libs/shared-entity/src/dto/workspace_dto.rs b/libs/shared-entity/src/dto/workspace_dto.rs index fb071bee..4a2b90b3 100644 --- a/libs/shared-entity/src/dto/workspace_dto.rs +++ b/libs/shared-entity/src/dto/workspace_dto.rs @@ -274,6 +274,19 @@ pub enum ViewLayout { Chat = 4, } +impl std::fmt::Display for ViewLayout { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + ViewLayout::Document => "Document", + ViewLayout::Grid => "Grid", + ViewLayout::Board => "Board", + ViewLayout::Calendar => "Calendar", + ViewLayout::Chat => "Chat", + }; + write!(f, "{}", s) + } +} + impl Default for ViewLayout { fn default() -> Self { Self::Document diff --git a/src/biz/collab/ops.rs b/src/biz/collab/ops.rs index c9140706..d4f42012 100644 --- a/src/biz/collab/ops.rs +++ b/src/biz/collab/ops.rs @@ -5,6 +5,7 @@ use app_error::AppError; use appflowy_collaborate::collab::storage::CollabAccessControlStorage; use chrono::DateTime; use chrono::Utc; +use collab::core::collab::DataSource; use collab::preclude::Collab; use collab_database::database::DatabaseBody; use collab_database::entity::FieldType; @@ -12,6 +13,7 @@ use collab_database::fields::Field; use collab_database::fields::TypeOptions; use collab_database::rows::RowDetail; use collab_database::workspace_database::NoPersistenceDatabaseCollabService; +use collab_database::workspace_database::WorkspaceDatabase; use collab_database::workspace_database::WorkspaceDatabaseBody; use collab_entity::CollabType; use collab_entity::EncodedCollab; @@ -295,6 +297,30 @@ pub async fn get_user_workspace_structure( collab_folder_to_folder_view(root_view_id, &folder, depth, &publish_view_ids) } +pub async fn get_latest_workspace_database( + collab_storage: &CollabAccessControlStorage, + pg_pool: &PgPool, + collab_origin: GetCollabOrigin, + workspace_id: Uuid, +) -> Result<(String, WorkspaceDatabase), AppError> { + let workspace_database_oid = select_workspace_database_oid(pg_pool, &workspace_id).await?; + let workspace_database_collab = { + let encoded_collab = get_latest_collab_encoded( + collab_storage, + collab_origin, + &workspace_id.to_string(), + &workspace_database_oid, + CollabType::WorkspaceDatabase, + ) + .await?; + collab_from_doc_state(encoded_collab.doc_state.to_vec(), &workspace_database_oid)? + }; + + let workspace_database = WorkspaceDatabase::open(workspace_database_collab) + .map_err(|err| AppError::Unhandled(format!("failed to open workspace database: {}", err)))?; + Ok((workspace_database_oid, workspace_database)) +} + pub async fn get_latest_collab_folder( collab_storage: &CollabAccessControlStorage, collab_origin: GetCollabOrigin, @@ -736,3 +762,15 @@ fn add_to_selection_from_type_options( } }; } + +pub fn collab_from_doc_state(doc_state: Vec, object_id: &str) -> Result { + let collab = Collab::new_with_source( + CollabOrigin::Server, + object_id, + DataSource::DocStateV1(doc_state), + vec![], + false, + ) + .map_err(|e| AppError::Unhandled(e.to_string()))?; + Ok(collab) +} diff --git a/src/biz/workspace/ops.rs b/src/biz/workspace/ops.rs index 25a16ce2..b108f279 100644 --- a/src/biz/workspace/ops.rs +++ b/src/biz/workspace/ops.rs @@ -1,6 +1,4 @@ use authentication::jwt::OptionalUserUuid; -use collab::core::collab::DataSource; -use collab::preclude::Collab; use collab_folder::CollabOrigin; use collab_rt_entity::{ClientCollabMessage, UpdateSync}; use collab_rt_protocol::{Message, SyncMessage}; @@ -750,15 +748,3 @@ pub async fn broadcast_update( Ok(()) } - -pub fn collab_from_doc_state(doc_state: Vec, object_id: &str) -> Result { - let collab = Collab::new_with_source( - CollabOrigin::Server, - object_id, - DataSource::DocStateV1(doc_state), - vec![], - false, - ) - .map_err(|e| AppError::Unhandled(e.to_string()))?; - Ok(collab) -} diff --git a/src/biz/workspace/page_view.rs b/src/biz/workspace/page_view.rs index c0c1a86f..833fe951 100644 --- a/src/biz/workspace/page_view.rs +++ b/src/biz/workspace/page_view.rs @@ -3,6 +3,20 @@ use app_error::AppError; use appflowy_collaborate::collab::storage::CollabAccessControlStorage; use chrono::DateTime; use collab::core::collab::Collab; +use collab_database::database::{ + gen_database_group_id, gen_database_id, gen_field_id, gen_row_id, Database, DatabaseContext, +}; +use collab_database::entity::{CreateDatabaseParams, CreateViewParams, EncodedDatabase, FieldType}; +use collab_database::fields::select_type_option::{ + SelectOption, SelectOptionColor, SelectOptionIds, SingleSelectTypeOption, +}; +use collab_database::fields::{default_field_settings_for_fields, Field}; +use collab_database::rows::{new_cell_builder, CreateRowParams}; +use collab_database::template::entity::CELL_DATA; +use collab_database::views::{ + BoardLayoutSetting, CalendarLayoutSetting, DatabaseLayout, Group, GroupSetting, GroupSettingMap, + LayoutSetting, LayoutSettings, +}; use collab_database::workspace_database::{NoPersistenceDatabaseCollabService, WorkspaceDatabase}; use collab_database::{database::DatabaseBody, rows::RowId}; use collab_document::document::Document; @@ -32,12 +46,18 @@ 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::ops::{collab_from_doc_state, get_latest_workspace_database}; use crate::biz::collab::{ folder_view::view_is_space, ops::{get_latest_collab_encoded, get_latest_collab_folder}, }; -use super::ops::{broadcast_update, collab_from_doc_state}; +use super::ops::broadcast_update; + +struct WorkspaceDatabaseUpdate { + pub updated_encoded_collab: Vec, + pub encoded_updates: Vec, +} struct FolderUpdate { pub updated_encoded_collab: Vec, @@ -140,20 +160,56 @@ pub async fn create_page( view_layout: &ViewLayout, name: Option<&str>, ) -> Result { - if *view_layout != ViewLayout::Document { - return Err(AppError::InvalidRequest( - "Only document layout is supported for page creation".to_string(), - )); + match view_layout { + ViewLayout::Document => { + create_document_page( + pg_pool, + collab_storage, + uid, + workspace_id, + parent_view_id, + name, + ) + .await + }, + ViewLayout::Grid => { + create_grid_page( + pg_pool, + collab_storage, + uid, + workspace_id, + parent_view_id, + name, + ) + .await + }, + ViewLayout::Calendar => { + create_calendar_page( + pg_pool, + collab_storage, + uid, + workspace_id, + parent_view_id, + name, + ) + .await + }, + ViewLayout::Board => { + create_board_page( + pg_pool, + collab_storage, + uid, + workspace_id, + parent_view_id, + name, + ) + .await + }, + layout => Err(AppError::InvalidRequest(format!( + "The layout type {} is not supported for page creation", + layout + ))), } - create_document_page( - pg_pool, - collab_storage, - uid, - workspace_id, - parent_view_id, - name, - ) - .await } fn prepare_default_document_collab_param() -> Result { @@ -173,6 +229,175 @@ fn prepare_default_document_collab_param() -> Result { }) } +#[allow(clippy::too_many_arguments)] +async fn prepare_new_encoded_database( + view_id: &str, + database_id: &str, + name: &str, + fields: Vec, + rows: Vec, + database_layout: DatabaseLayout, + layout_setting: Option, + group_settings: Vec, +) -> Result { + let timestamp = collab_database::database::timestamp(); + let context = DatabaseContext::new(Arc::new(NoPersistenceDatabaseCollabService)); + let field_settings = default_field_settings_for_fields(&fields, database_layout); + let mut layout_settings = LayoutSettings::default(); + if let Some(layout_setting) = layout_setting { + layout_settings.insert(database_layout, layout_setting); + } + let params = CreateDatabaseParams { + database_id: database_id.to_string(), + fields, + rows, + views: vec![CreateViewParams { + database_id: database_id.to_string(), + view_id: view_id.to_string(), + name: name.to_string(), + layout: database_layout, + layout_settings, + filters: vec![], + group_settings, + sorts: vec![], + field_settings, + created_at: timestamp, + modified_at: timestamp, + ..Default::default() + }], + }; + let database = Database::create_with_view(params, context) + .await + .map_err(|err| AppError::Internal(anyhow!("Failed to create database with view: {}", err)))?; + database + .encode_database_collabs() + .await + .map_err(|err| AppError::Internal(anyhow!("Failed to encode database: {}", err))) +} + +async fn prepare_default_calendar_encoded_database( + view_id: &str, + database_id: &str, + name: &str, +) -> Result { + let text_field = Field::from_field_type("Title", FieldType::RichText, true); + let date_field = Field::from_field_type("Date", FieldType::DateTime, true); + let date_field_id = date_field.id.clone(); + let multi_select_field = Field::from_field_type("Tags", FieldType::MultiSelect, true); + let fields = vec![text_field, date_field, multi_select_field]; + let layout_setting = CalendarLayoutSetting::new(date_field_id); + + prepare_new_encoded_database( + view_id, + database_id, + name, + fields, + vec![], + DatabaseLayout::Calendar, + Some(layout_setting.into()), + vec![], + ) + .await +} + +async fn prepare_default_grid_encoded_database( + view_id: &str, + database_id: &str, + name: &str, +) -> Result { + let text_field = Field::from_field_type("Name", FieldType::RichText, true); + let single_select_field = Field::from_field_type("Type", FieldType::SingleSelect, true); + let checkbox_field = Field::from_field_type("Done", FieldType::Checkbox, true); + let fields = vec![text_field, single_select_field, checkbox_field]; + let rows = (0..3) + .map(|_| CreateRowParams::new(gen_row_id(), database_id.to_string())) + .collect(); + + prepare_new_encoded_database( + view_id, + database_id, + name, + fields, + rows, + DatabaseLayout::Grid, + None, + vec![], + ) + .await +} + +async fn prepare_default_board_encoded_database( + view_id: &str, + database_id: &str, + name: &str, +) -> Result { + let card_title_field = Field::from_field_type("Description", FieldType::RichText, true); + let text_field_id = card_title_field.id.clone(); + + let to_do_option = SelectOption::with_color("To Do", SelectOptionColor::Purple); + let doing_option = SelectOption::with_color("Doing", SelectOptionColor::Orange); + let done_option = SelectOption::with_color("Done", SelectOptionColor::Yellow); + let default_option_id = to_do_option.id.clone(); + let options = vec![to_do_option, doing_option, done_option]; + let card_status_option_ids: Vec = + options.iter().map(|option| option.id.clone()).collect(); + let mut card_status_options = SingleSelectTypeOption::default(); + card_status_options.options.extend(options); + let mut card_status_field = Field::new( + gen_field_id(), + "Status".to_string(), + FieldType::SingleSelect.into(), + false, + ); + card_status_field.type_options.insert( + FieldType::SingleSelect.to_string(), + card_status_options.into(), + ); + + let card_status_field_id = card_status_field.id.clone(); + let card_status_field_type = card_status_field.field_type; + let mut group_ids = vec![card_status_field_id.clone()]; + group_ids.extend(card_status_option_ids); + let groups = group_ids.iter().map(|id| Group::new(id.clone())).collect(); + let group_settings: Vec = vec![GroupSetting { + id: gen_database_group_id(), + field_id: card_status_field_id.clone(), + field_type: card_status_field_type, + groups, + content: Default::default(), + } + .into()]; + + let mut rows = vec![]; + let card_status_select_option_ids = SelectOptionIds::from(vec![default_option_id.clone()]); + for i in 0..3 { + let card_status_cell_data = card_status_select_option_ids.to_cell_data(FieldType::SingleSelect); + let mut description_cell = new_cell_builder(FieldType::RichText); + let description_text = format!("Card {}", i + 1); + description_cell.insert(CELL_DATA.into(), description_text.into()); + let mut row = CreateRowParams::new(gen_row_id(), database_id.to_string()); + row + .cells + .insert(card_status_field_id.clone(), card_status_cell_data); + row.cells.insert(text_field_id.clone(), description_cell); + rows.push(row); + } + let fields = vec![card_title_field, card_status_field]; + let layout_setting = BoardLayoutSetting::new(); + + prepare_new_encoded_database( + view_id, + database_id, + name, + fields, + rows, + DatabaseLayout::Board, + Some(layout_setting.into()), + group_settings, + ) + .await +} + #[allow(clippy::too_many_arguments)] async fn add_new_space_to_folder( uid: i64, @@ -248,17 +473,36 @@ async fn update_space_properties( }) } +async fn add_new_database_to_workspace( + workspace_database: &mut WorkspaceDatabase, + database_id: &str, + view_id: &str, +) -> Result { + let view_ids_by_database_id = + HashMap::from([(database_id.to_string(), vec![view_id.to_string()])]); + let encoded_updates = workspace_database + .batch_add_database(view_ids_by_database_id) + .encode_update_v1(); + let updated_encoded_collab = workspace_database_to_encoded_collab(workspace_database)?; + Ok(WorkspaceDatabaseUpdate { + updated_encoded_collab, + encoded_updates, + }) +} + async fn add_new_view_to_folder( uid: i64, parent_view_id: &str, view_id: &str, folder: &mut Folder, name: Option<&str>, + layout: collab_folder::ViewLayout, ) -> Result { let encoded_update = { let view = NestedChildViewBuilder::new(uid, parent_view_id.to_string()) .with_view_id(view_id) .with_name(name.unwrap_or_default()) + .with_layout(layout) .build() .view; let mut txn = folder.collab.transact_mut(); @@ -372,6 +616,55 @@ fn folder_to_encoded_collab(folder: &Folder) -> Result, AppError> { }) } +fn workspace_database_to_encoded_collab( + workspace_db: &WorkspaceDatabase, +) -> Result, AppError> { + let encoded_workspace_db_collab = workspace_db + .encode_collab_v1() + .map_err(|err| AppError::Internal(anyhow!("Failed to encode workspace folder: {}", err)))?; + encoded_workspace_db_collab + .encode_to_bytes() + .map_err(|err| { + AppError::Internal(anyhow!( + "Failed to encode workspace folder to bytes: {}", + err + )) + }) +} + +async fn insert_and_broadcast_workspace_database_update( + uid: i64, + workspace_id: Uuid, + workspace_database_id: &str, + workspace_database_update: WorkspaceDatabaseUpdate, + collab_storage: &CollabAccessControlStorage, + transaction: &mut Transaction<'_, sqlx::Postgres>, +) -> Result<(), AppError> { + let params = CollabParams { + object_id: workspace_database_id.to_string(), + encoded_collab_v1: workspace_database_update.updated_encoded_collab.into(), + collab_type: CollabType::WorkspaceDatabase, + embeddings: None, + }; + let action_description = format!("Update workspace database: {}", workspace_id); + collab_storage + .insert_new_collab_with_transaction( + &workspace_id.to_string(), + &uid, + params, + transaction, + &action_description, + ) + .await?; + broadcast_update( + collab_storage, + workspace_database_id, + workspace_database_update.encoded_updates.clone(), + ) + .await?; + Ok(()) +} + async fn insert_and_broadcast_workspace_folder_update( uid: i64, workspace_id: Uuid, @@ -417,8 +710,15 @@ async fn create_document_page( 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_view_to_folder(uid, parent_view_id, &view_id, &mut folder, name).await?; + let folder_update = add_new_view_to_folder( + uid, + parent_view_id, + &view_id, + &mut folder, + name, + collab_folder::ViewLayout::Document, + ) + .await?; let mut transaction = pg_pool.begin().await?; let action = format!("Create new collab: {}", view_id); collab_storage @@ -442,6 +742,170 @@ async fn create_document_page( Ok(Page { view_id }) } +async fn create_grid_page( + pg_pool: &PgPool, + collab_storage: &CollabAccessControlStorage, + uid: i64, + workspace_id: Uuid, + parent_view_id: &str, + name: Option<&str>, +) -> Result { + let view_id = Uuid::new_v4().to_string(); + let database_id = gen_database_id(); + let default_grid_encoded_database = + prepare_default_grid_encoded_database(&view_id, &database_id, name.unwrap_or_default()).await?; + create_database_page( + pg_pool, + collab_storage, + uid, + workspace_id, + parent_view_id, + &view_id, + collab_folder::ViewLayout::Grid, + name, + &default_grid_encoded_database, + ) + .await +} + +async fn create_board_page( + pg_pool: &PgPool, + collab_storage: &CollabAccessControlStorage, + uid: i64, + workspace_id: Uuid, + parent_view_id: &str, + name: Option<&str>, +) -> Result { + let view_id = Uuid::new_v4().to_string(); + let database_id = gen_database_id(); + let default_board_encoded_database = + prepare_default_board_encoded_database(&view_id, &database_id, name.unwrap_or_default()) + .await?; + create_database_page( + pg_pool, + collab_storage, + uid, + workspace_id, + parent_view_id, + &view_id, + collab_folder::ViewLayout::Board, + name, + &default_board_encoded_database, + ) + .await +} + +async fn create_calendar_page( + pg_pool: &PgPool, + collab_storage: &CollabAccessControlStorage, + uid: i64, + workspace_id: Uuid, + parent_view_id: &str, + name: Option<&str>, +) -> Result { + let view_id = Uuid::new_v4().to_string(); + let database_id = gen_database_id(); + let default_calendar_encoded_database = + prepare_default_calendar_encoded_database(&view_id, &database_id, name.unwrap_or_default()) + .await?; + create_database_page( + pg_pool, + collab_storage, + uid, + workspace_id, + parent_view_id, + &view_id, + collab_folder::ViewLayout::Calendar, + name, + &default_calendar_encoded_database, + ) + .await +} + +#[allow(clippy::too_many_arguments)] +async fn create_database_page( + pg_pool: &PgPool, + collab_storage: &CollabAccessControlStorage, + uid: i64, + workspace_id: Uuid, + parent_view_id: &str, + view_id: &str, + view_layout: collab_folder::ViewLayout, + name: Option<&str>, + encoded_database: &EncodedDatabase, +) -> Result { + let collab_origin = GetCollabOrigin::User { uid }; + let mut folder = get_latest_collab_folder( + collab_storage, + collab_origin.clone(), + &workspace_id.to_string(), + ) + .await?; + let folder_update = + add_new_view_to_folder(uid, parent_view_id, view_id, &mut folder, name, view_layout).await?; + let (workspace_database_id, mut workspace_database) = + get_latest_workspace_database(collab_storage, pg_pool, collab_origin, workspace_id).await?; + let database_id = encoded_database.encoded_database_collab.object_id.clone(); + let workspace_database_update = + add_new_database_to_workspace(&mut workspace_database, &database_id, view_id).await?; + let database_collab_params = CollabParams { + object_id: database_id.clone(), + encoded_collab_v1: encoded_database + .encoded_database_collab + .encoded_collab + .encode_to_bytes()? + .into(), + collab_type: CollabType::Database, + embeddings: None, + }; + let row_collab_params_list = encoded_database + .encoded_row_collabs + .iter() + .map(|row_collab| CollabParams { + object_id: row_collab.object_id.clone(), + encoded_collab_v1: row_collab.encoded_collab.encode_to_bytes().unwrap().into(), + collab_type: CollabType::DatabaseRow, + embeddings: None, + }) + .collect_vec(); + + let mut transaction = pg_pool.begin().await?; + let action = format!("Create new database collab: {}", database_id); + collab_storage + .insert_new_collab_with_transaction( + &workspace_id.to_string(), + &uid, + database_collab_params, + &mut transaction, + &action, + ) + .await?; + collab_storage + .batch_insert_new_collab(&workspace_id.to_string(), &uid, row_collab_params_list) + .await?; + insert_and_broadcast_workspace_folder_update( + uid, + workspace_id, + folder_update, + collab_storage, + &mut transaction, + ) + .await?; + insert_and_broadcast_workspace_database_update( + uid, + workspace_id, + &workspace_database_id, + workspace_database_update, + collab_storage, + &mut transaction, + ) + .await?; + transaction.commit().await?; + Ok(Page { + view_id: view_id.to_string(), + }) +} + pub async fn move_page_to_trash( pg_pool: &PgPool, collab_storage: &CollabAccessControlStorage, diff --git a/src/biz/workspace/publish_dup.rs b/src/biz/workspace/publish_dup.rs index c904d425..6f5ee329 100644 --- a/src/biz/workspace/publish_dup.rs +++ b/src/biz/workspace/publish_dup.rs @@ -43,10 +43,10 @@ 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::collab_from_doc_state; use crate::biz::collab::ops::get_latest_collab_encoded; use super::ops::broadcast_update; -use super::ops::collab_from_doc_state; #[allow(clippy::too_many_arguments)] pub async fn duplicate_published_collab_to_workspace( diff --git a/tests/workspace/page_view.rs b/tests/workspace/page_view.rs index 07193d62..2cc5395f 100644 --- a/tests/workspace/page_view.rs +++ b/tests/workspace/page_view.rs @@ -80,6 +80,81 @@ async fn get_page_view() { assert_eq!(resp.data.row_data.len(), 0); } +#[tokio::test] +async fn create_new_page_with_database() { + let (c, _user) = generate_unique_registered_user_client().await; + let workspaces = c.get_workspaces().await.unwrap(); + assert_eq!(workspaces.len(), 1); + let workspace_id = workspaces[0].workspace_id; + let folder_view = c + .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 calendar_page = c + .create_workspace_page_view( + workspace_id, + &CreatePageParams { + parent_view_id: general_space.view_id.clone(), + layout: ViewLayout::Calendar, + name: Some("New calendar".to_string()), + }, + ) + .await + .unwrap(); + let grid_page = c + .create_workspace_page_view( + workspace_id, + &CreatePageParams { + parent_view_id: general_space.view_id.clone(), + layout: ViewLayout::Grid, + name: Some("New grid".to_string()), + }, + ) + .await + .unwrap(); + let board_page = c + .create_workspace_page_view( + workspace_id, + &CreatePageParams { + parent_view_id: general_space.view_id.clone(), + layout: ViewLayout::Grid, + name: Some("New board".to_string()), + }, + ) + .await + .unwrap(); + sleep(Duration::from_secs(1)).await; + let folder_view = c + .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 views_under_general_space: HashSet = general_space + .children + .iter() + .map(|v| v.view_id.clone()) + .collect(); + for view_id in &[ + calendar_page.view_id.clone(), + grid_page.view_id.clone(), + board_page.view_id.clone(), + ] { + assert!(views_under_general_space.contains(view_id)); + c.get_workspace_page_view(workspace_id, view_id) + .await + .unwrap(); + } +} + #[tokio::test] async fn create_new_document_page() { let (c, _user) = generate_unique_registered_user_client().await; diff --git a/tests/workspace/publish.rs b/tests/workspace/publish.rs index 72f774d0..a48a3c48 100644 --- a/tests/workspace/publish.rs +++ b/tests/workspace/publish.rs @@ -1,6 +1,6 @@ use app_error::ErrorCode; use appflowy_cloud::biz::collab::folder_view::collab_folder_to_folder_view; -use appflowy_cloud::biz::workspace::ops::collab_from_doc_state; +use appflowy_cloud::biz::collab::ops::collab_from_doc_state; use client_api::entity::{ AFRole, GlobalComment, PatchPublishedCollab, PublishCollabItem, PublishCollabMetadata, PublishInfoMeta,