Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: api for duplicate view #1259

Merged
merged 1 commit into from
Mar 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -303,13 +303,13 @@ lto = false
[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 = "c2442a93704e14508ccee325da8d56ef0b34c7ce" }
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "c2442a93704e14508ccee325da8d56ef0b34c7ce" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "c2442a93704e14508ccee325da8d56ef0b34c7ce" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "c2442a93704e14508ccee325da8d56ef0b34c7ce" }
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "c2442a93704e14508ccee325da8d56ef0b34c7ce" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "c2442a93704e14508ccee325da8d56ef0b34c7ce" }
collab-importer = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "c2442a93704e14508ccee325da8d56ef0b34c7ce" }
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2ae871cc355ea2cc1d5d578e21c8263242" }
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2ae871cc355ea2cc1d5d578e21c8263242" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2ae871cc355ea2cc1d5d578e21c8263242" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2ae871cc355ea2cc1d5d578e21c8263242" }
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2ae871cc355ea2cc1d5d578e21c8263242" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2ae871cc355ea2cc1d5d578e21c8263242" }
collab-importer = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2ae871cc355ea2cc1d5d578e21c8263242" }

[features]
history = []
Expand Down
22 changes: 21 additions & 1 deletion libs/client-api/src/http_view.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use client_api_entity::workspace_dto::{
AppendBlockToPageParams, CreatePageDatabaseViewParams, CreatePageParams, CreateSpaceParams,
MovePageParams, Page, PageCollab, PublishPageParams, Space, UpdatePageParams, UpdateSpaceParams,
DuplicatePageParams, MovePageParams, Page, PageCollab, PublishPageParams, Space,
UpdatePageParams, UpdateSpaceParams,
};
use reqwest::Method;
use serde_json::json;
Expand Down Expand Up @@ -277,4 +278,23 @@ impl Client {
.await?;
AppResponse::<()>::from_response(resp).await?.into_error()
}

pub async fn duplicate_view_and_children(
&self,
workspace_id: Uuid,
view_id: &str,
params: &DuplicatePageParams,
) -> Result<(), AppResponseError> {
let url = format!(
"{}/api/workspace/{}/page-view/{}/duplicate",
self.base_url, workspace_id, view_id
);
let resp = self
.http_client_with_auth(Method::POST, &url)
.await?
.json(params)
.send()
.await?;
AppResponse::<()>::from_response(resp).await?.into_error()
}
}
5 changes: 5 additions & 0 deletions libs/shared-entity/src/dto/workspace_dto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,11 @@ pub struct MovePageParams {
pub prev_view_id: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DuplicatePageParams {
pub suffix: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreatePageDatabaseViewParams {
pub layout: ViewLayout,
Expand Down
32 changes: 32 additions & 0 deletions src/api/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::biz::collab::ops::{
use crate::biz::collab::utils::collab_from_doc_state;
use crate::biz::user::user_verify::verify_token;
use crate::biz::workspace;
use crate::biz::workspace::duplicate::duplicate_view_tree_and_collab;
use crate::biz::workspace::ops::{
create_comment_on_published_view, create_reaction_on_comment, get_comments_on_published_view,
get_reactions_on_published_view, remove_comment_on_published_view, remove_reaction_on_comment,
Expand Down Expand Up @@ -184,6 +185,10 @@ pub fn workspace_scope() -> Scope {
web::resource("/{workspace_id}/page-view/{view_id}/move")
.route(web::post().to(move_page_handler)),
)
.service(
web::resource("/{workspace_id}/page-view/{view_id}/duplicate")
.route(web::post().to(duplicate_page_handler)),
)
.service(
web::resource("/{workspace_id}/page-view/{view_id}/database-view")
.route(web::post().to(post_page_database_view_handler)),
Expand Down Expand Up @@ -1284,6 +1289,33 @@ async fn move_page_handler(
Ok(Json(AppResponse::Ok()))
}

async fn duplicate_page_handler(
user_uuid: UserUuid,
path: web::Path<(Uuid, Uuid)>,
payload: Json<DuplicatePageParams>,
state: Data<AppState>,
server: Data<RealtimeServerAddr>,
req: HttpRequest,
) -> Result<Json<AppResponse<()>>> {
let uid = state.user_cache.get_user_uid(&user_uuid).await?;
let (workspace_uuid, view_id) = path.into_inner();
let user = realtime_user_for_web_request(req.headers(), uid)?;
let suffix = payload.suffix.as_deref().unwrap_or(" (Copy)").to_string();
duplicate_view_tree_and_collab(
&state.metrics.appflowy_web_metrics,
server,
user,
state.collab_access_control_storage.clone(),
&state.pg_pool,
workspace_uuid,
view_id,
&suffix,
)
.await
.unwrap();
Ok(Json(AppResponse::Ok()))
}

async fn move_page_to_trash_handler(
user_uuid: UserUuid,
path: web::Path<(Uuid, String)>,
Expand Down
85 changes: 85 additions & 0 deletions src/biz/collab/database.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
use std::sync::Arc;

use app_error::AppError;
use appflowy_collaborate::collab::storage::CollabAccessControlStorage;
use async_trait::async_trait;
use collab::preclude::Collab;
use collab_database::{
database::{gen_database_group_id, gen_field_id},
entity::FieldType,
error::DatabaseError,
fields::{
date_type_option::DateTypeOption, default_field_settings_for_fields,
select_type_option::SingleSelectTypeOption, Field, TypeOptionData,
Expand All @@ -10,7 +16,16 @@ use collab_database::{
BoardLayoutSetting, CalendarLayoutSetting, DatabaseLayout, FieldSettingsByFieldIdMap, Group,
GroupSetting, GroupSettingMap, LayoutSettings,
},
workspace_database::{
DatabaseCollabPersistenceService, DatabaseCollabService, EncodeCollabByOid,
},
};
use collab_entity::{CollabType, EncodedCollab};
use collab_folder::CollabOrigin;
use database::collab::GetCollabOrigin;
use uuid::Uuid;

use super::utils::{batch_get_latest_collab_encoded, get_latest_collab_encoded};

pub struct LinkedViewDependencies {
pub layout_settings: LayoutSettings,
Expand Down Expand Up @@ -154,6 +169,76 @@ fn create_card_status_field() -> Field {
.with_type_option_data(field_type, default_select_type_option.into())
}

#[derive(Clone)]
pub struct PostgresDatabaseCollabService {
pub workspace_id: Uuid,
pub collab_storage: Arc<CollabAccessControlStorage>,
}

impl PostgresDatabaseCollabService {
pub async fn get_collab(&self, oid: &str, collab_type: CollabType) -> EncodedCollab {
get_latest_collab_encoded(
&self.collab_storage,
GetCollabOrigin::Server,
&self.workspace_id.to_string(),
oid,
collab_type,
)
.await
.unwrap()
}
}

#[async_trait]
impl DatabaseCollabService for PostgresDatabaseCollabService {
async fn build_collab(
&self,
object_id: &str,
object_type: CollabType,
encoded_collab: Option<(EncodedCollab, bool)>,
) -> Result<Collab, DatabaseError> {
match encoded_collab {
None => Collab::new_with_source(
CollabOrigin::Empty,
object_id,
self.get_collab(object_id, object_type).await.into(),
vec![],
false,
)
.map_err(|err| DatabaseError::Internal(err.into())),
Some((encoded_collab, _)) => Collab::new_with_source(
CollabOrigin::Empty,
object_id,
encoded_collab.into(),
vec![],
false,
)
.map_err(|err| DatabaseError::Internal(err.into())),
}
}

async fn get_collabs(
&self,
object_ids: Vec<String>,
collab_type: CollabType,
) -> Result<EncodeCollabByOid, DatabaseError> {
let encoded_collabs = batch_get_latest_collab_encoded(
&self.collab_storage,
GetCollabOrigin::Server,
&self.workspace_id.to_string(),
&object_ids,
collab_type,
)
.await
.unwrap();
Ok(encoded_collabs)
}

fn persistence(&self) -> Option<Arc<dyn DatabaseCollabPersistenceService>> {
None
}
}

#[cfg(test)]
mod tests {
use collab_database::{
Expand Down
38 changes: 37 additions & 1 deletion src/biz/collab/folder_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ use std::collections::HashSet;

use app_error::AppError;
use chrono::DateTime;
use collab_folder::{Folder, SectionItem, SpacePermission, ViewLayout as CollabFolderViewLayout};
use collab_folder::{
Folder, SectionItem, SpacePermission, View, ViewLayout as CollabFolderViewLayout,
};
use shared_entity::dto::workspace_dto::{
self, FavoriteFolderView, FolderView, FolderViewMinimal, RecentFolderView, TrashFolderView,
ViewLayout,
Expand Down Expand Up @@ -263,6 +265,40 @@ pub fn section_items_to_trash_folder_view(
.collect()
}

pub struct ViewTree {
pub view: View,
pub children: Vec<ViewTree>,
}

pub fn get_view_and_children(folder: &Folder, view_id: &str) -> Option<ViewTree> {
let private_space_and_trash_views = private_space_and_trash_view_ids(folder);
get_view_and_children_recursive(folder, &private_space_and_trash_views, view_id)
}

fn get_view_and_children_recursive(
folder: &Folder,
private_space_and_trash_views: &PrivateSpaceAndTrashViews,
view_id: &str,
) -> Option<ViewTree> {
if private_space_and_trash_views
.view_ids_in_trash
.contains(view_id)
{
return None;
}

folder.get_view(view_id).map(|view| ViewTree {
view: View::clone(&view),
children: view
.children
.iter()
.filter_map(|child_view_id| {
get_view_and_children_recursive(folder, private_space_and_trash_views, child_view_id)
})
.collect(),
})
}

pub fn check_if_view_ancestors_fulfil_condition(
view_id: &str,
collab_folder: &Folder,
Expand Down
Loading
Loading