diff --git a/libs/workspace-template/src/document/get_started.rs b/libs/workspace-template/src/document/get_started.rs new file mode 100644 index 000000000..72bd9ad11 --- /dev/null +++ b/libs/workspace-template/src/document/get_started.rs @@ -0,0 +1,55 @@ +use crate::document::parser::JsonToDocumentParser; +use crate::hierarchy_builder::WorkspaceViewBuilder; +use crate::{TemplateData, WorkspaceTemplate}; +use async_trait::async_trait; +use collab::core::collab::MutexCollab; +use collab::core::origin::CollabOrigin; +use collab_document::document::Document; +use collab_entity::CollabType; +use collab_folder::ViewLayout; +use std::sync::Arc; +use tokio::sync::RwLock; + +/// This template generates a document containing a 'read me' guide. +/// It ensures that at least one view is created for the document. +pub struct GetStartedDocumentTemplate; + +#[async_trait] +impl WorkspaceTemplate for GetStartedDocumentTemplate { + fn layout(&self) -> ViewLayout { + ViewLayout::Document + } + + async fn create_workspace_view( + &self, + _uid: i64, + workspace_view_builder: Arc>, + ) -> anyhow::Result { + let view_id = workspace_view_builder + .write() + .await + .with_view_builder(|view_builder| async { + view_builder + .with_name("Getting started") + .with_icon("⭐️") + .build() + }) + .await; + + // create a empty document + let data = tokio::task::spawn_blocking(|| { + let json_str = include_str!("../../assets/read_me.json"); + let document_data = JsonToDocumentParser::json_str_to_document(json_str).unwrap(); + let collab = Arc::new(MutexCollab::new(CollabOrigin::Empty, &view_id, vec![])); + let document = Document::create_with_data(collab, document_data)?; + let data = document.get_collab().encode_collab_v1(); + Ok::<_, anyhow::Error>(TemplateData { + object_id: view_id, + object_type: CollabType::Document, + object_data: data, + }) + }) + .await??; + Ok(data) + } +} diff --git a/libs/workspace-template/src/document/mod.rs b/libs/workspace-template/src/document/mod.rs index d04f22034..effd01a98 100644 --- a/libs/workspace-template/src/document/mod.rs +++ b/libs/workspace-template/src/document/mod.rs @@ -1,54 +1,2 @@ +pub mod get_started; mod parser; - -use crate::document::parser::JsonToDocumentParser; -use crate::hierarchy_builder::WorkspaceViewBuilder; -use crate::{TemplateData, WorkspaceTemplate}; -use async_trait::async_trait; -use collab::core::collab::MutexCollab; -use collab::core::origin::CollabOrigin; -use collab_document::document::Document; -use collab_entity::CollabType; -use std::sync::Arc; -use tokio::sync::RwLock; - -/// A default template for creating documents. -/// -/// This template generates a document containing a 'read me' guide. -/// It ensures that at least one view is created for the document. -pub struct DocumentTemplate; - -#[async_trait] -impl WorkspaceTemplate for DocumentTemplate { - async fn create_workspace_view( - &self, - _uid: i64, - workspace_view_builder: Arc>, - ) -> anyhow::Result { - let view_id = workspace_view_builder - .write() - .await - .with_view_builder(|view_builder| async { - view_builder - .with_name("Getting started") - .with_icon("⭐️") - .build() - }) - .await; - - // create a empty document - let data = tokio::task::spawn_blocking(|| { - let json_str = include_str!("../../assets/read_me.json"); - let document_data = JsonToDocumentParser::json_str_to_document(json_str).unwrap(); - let collab = Arc::new(MutexCollab::new(CollabOrigin::Empty, &view_id, vec![])); - let document = Document::create_with_data(collab, document_data)?; - let data = document.get_collab().encode_collab_v1(); - Ok::<_, anyhow::Error>(TemplateData { - object_id: view_id, - object_type: CollabType::Document, - object_data: data, - }) - }) - .await??; - Ok(data) - } -} diff --git a/libs/workspace-template/src/lib.rs b/libs/workspace-template/src/lib.rs index a8cedc788..d09242d26 100644 --- a/libs/workspace-template/src/lib.rs +++ b/libs/workspace-template/src/lib.rs @@ -1,4 +1,4 @@ -mod document; +pub mod document; mod hierarchy_builder; use crate::hierarchy_builder::{FlattedViews, WorkspaceViewBuilder}; @@ -17,6 +17,8 @@ use tokio::sync::RwLock; #[async_trait] pub trait WorkspaceTemplate { + fn layout(&self) -> ViewLayout; + async fn create_workspace_view( &self, uid: i64, @@ -32,6 +34,8 @@ pub struct TemplateData { pub type WorkspaceTemplateHandlers = HashMap>; +/// A builder for creating a workspace template. +/// workspace template is a set of views that are created when a workspace is created. pub struct WorkspaceTemplateBuilder { pub uid: i64, pub workspace_id: String, @@ -40,9 +44,7 @@ pub struct WorkspaceTemplateBuilder { impl WorkspaceTemplateBuilder { pub fn new(uid: i64, workspace_id: &str) -> Self { - let mut handlers = WorkspaceTemplateHandlers::default(); - // register the document template handler - handlers.insert(ViewLayout::Document, Arc::new(document::DocumentTemplate)); + let handlers = WorkspaceTemplateHandlers::default(); Self { uid, workspace_id: workspace_id.to_string(), @@ -50,7 +52,25 @@ impl WorkspaceTemplateBuilder { } } - pub async fn default_workspace(&self) -> Result> { + pub fn with_template(mut self, template: T) -> Self + where + T: WorkspaceTemplate + Send + Sync + 'static, + { + self.handlers.insert(template.layout(), Arc::new(template)); + self + } + + pub fn with_templates(mut self, templates: Vec) -> Self + where + T: WorkspaceTemplate + Send + Sync + 'static, + { + for template in templates { + self.handlers.insert(template.layout(), Arc::new(template)); + } + self + } + + pub async fn build(&self) -> Result> { let workspace_view_builder = Arc::new(RwLock::new(WorkspaceViewBuilder::new( self.workspace_id.clone(), self.uid, diff --git a/src/biz/user.rs b/src/biz/user.rs index 1a4b00787..bddeffc72 100644 --- a/src/biz/user.rs +++ b/src/biz/user.rs @@ -22,10 +22,11 @@ use database::user::{create_user, is_user_exist}; use realtime::entities::RealtimeUser; use shared_entity::dto::auth_dto::UpdateUserParams; use snowflake::Snowflake; -use sqlx::{types::uuid, PgPool}; +use sqlx::{types::uuid, PgPool, Transaction}; use tokio::sync::RwLock; use tracing::{debug, event, instrument}; -use workspace_template::WorkspaceTemplateBuilder; +use workspace_template::document::get_started::GetStartedDocumentTemplate; +use workspace_template::{WorkspaceTemplate, WorkspaceTemplateBuilder}; /// Verify the token from the gotrue server and create the user if it is a new user /// Return true if the user is a new user @@ -86,41 +87,15 @@ where ) .await?; - // Create the default workspace for the user. A default workspace might contain multiple - // templates, e.g. a document template, a database template, etc. - let templates = WorkspaceTemplateBuilder::new(new_uid, &workspace_id) - .default_workspace() - .await?; - - debug!("create {} templates for user:{}", templates.len(), new_uid); - for template in templates { - let object_id = template.object_id; - let encoded_collab_v1 = template - .object_data - .encode_to_bytes() - .map_err(|err| AppError::Internal(anyhow::Error::from(err)))?; - - collab_access_control - .cache_collab_access_level( - realtime::collaborate::CollabUserId::UserId(&new_uid), - &object_id, - AFAccessLevel::FullAccess, - ) - .await?; - - insert_into_af_collab( - &mut txn, - &new_uid, - &workspace_id, - &CollabParams { - object_id, - encoded_collab_v1, - collab_type: template.object_type, - override_if_exist: false, - }, - ) - .await?; - } + // Create a workspace with the GetStarted template + create_workspace_for_user( + new_uid, + &workspace_id, + collab_access_control, + &mut txn, + vec![GetStartedDocumentTemplate], + ) + .await?; } txn .commit() @@ -129,6 +104,57 @@ where Ok(is_new) } +/// Create a workspace for a user. +/// This function generates a workspace along with its templates and stores them in the database. +/// Each template is stored as an individual collaborative object. +async fn create_workspace_for_user( + new_uid: i64, + workspace_id: &str, + collab_access_control: &C, + txn: &mut Transaction<'_, sqlx::Postgres>, + templates: Vec, +) -> Result<(), AppError> +where + C: CollabAccessControl, + T: WorkspaceTemplate + Send + Sync + 'static, +{ + let templates = WorkspaceTemplateBuilder::new(new_uid, workspace_id) + .with_templates(templates) + .build() + .await?; + + debug!("create {} templates for user:{}", templates.len(), new_uid); + for template in templates { + let object_id = template.object_id; + let encoded_collab_v1 = template + .object_data + .encode_to_bytes() + .map_err(|err| AppError::Internal(anyhow::Error::from(err)))?; + + collab_access_control + .cache_collab_access_level( + realtime::collaborate::CollabUserId::UserId(&new_uid), + &object_id, + AFAccessLevel::FullAccess, + ) + .await?; + + insert_into_af_collab( + txn, + &new_uid, + workspace_id, + &CollabParams { + object_id, + encoded_collab_v1, + collab_type: template.object_type, + override_if_exist: false, + }, + ) + .await?; + } + Ok(()) +} + pub async fn get_profile(pg_pool: &PgPool, uuid: &Uuid) -> Result { let row = select_user_profile(pg_pool, uuid) .await?