feat: add retrieve sections endpoints

This commit is contained in:
khorshuheng 2024-09-11 17:22:41 +08:00
parent 96896101d4
commit c686aa91a6
6 changed files with 314 additions and 4 deletions

View File

@ -4,6 +4,7 @@ use client_api_entity::auth_dto::DeleteUserQuery;
use client_api_entity::workspace_dto::FolderView;
use client_api_entity::workspace_dto::QueryWorkspaceFolder;
use client_api_entity::workspace_dto::QueryWorkspaceParam;
use client_api_entity::workspace_dto::SectionItems;
use client_api_entity::AuthProvider;
use client_api_entity::CollabType;
use gotrue::grant::PasswordGrant;
@ -719,6 +720,57 @@ impl Client {
.into_data()
}
#[instrument(level = "info", skip_all, err)]
pub async fn get_workspace_favorite(
&self,
workspace_id: &str,
) -> Result<SectionItems, AppResponseError> {
let url = format!("{}/api/workspace/{}/favorite", self.base_url, workspace_id);
let resp = self
.http_client_with_auth(Method::GET, &url)
.await?
.send()
.await?;
log_request_id(&resp);
AppResponse::<SectionItems>::from_response(resp)
.await?
.into_data()
}
#[instrument(level = "info", skip_all, err)]
pub async fn get_workspace_recent(
&self,
workspace_id: &str,
) -> Result<SectionItems, AppResponseError> {
let url = format!("{}/api/workspace/{}/recent", self.base_url, workspace_id);
let resp = self
.http_client_with_auth(Method::GET, &url)
.await?
.send()
.await?;
log_request_id(&resp);
AppResponse::<SectionItems>::from_response(resp)
.await?
.into_data()
}
#[instrument(level = "info", skip_all, err)]
pub async fn get_workspace_trash(
&self,
workspace_id: &str,
) -> Result<SectionItems, AppResponseError> {
let url = format!("{}/api/workspace/{}/trash", self.base_url, workspace_id);
let resp = self
.http_client_with_auth(Method::GET, &url)
.await?
.send()
.await?;
log_request_id(&resp);
AppResponse::<SectionItems>::from_response(resp)
.await?
.into_data()
}
#[instrument(skip_all, err)]
pub async fn sign_in_password(
&self,

View File

@ -144,6 +144,11 @@ pub struct FolderView {
pub children: Vec<FolderView>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct SectionItems {
pub views: Vec<FolderView>,
}
#[derive(Eq, PartialEq, Debug, Hash, Clone, Serialize_repr, Deserialize_repr)]
#[repr(u8)]
pub enum IconType {

View File

@ -38,6 +38,9 @@ use crate::api::util::PayloadReader;
use crate::api::util::{compress_type_from_header_value, device_id_from_headers, CollabValidator};
use crate::api::ws::RealtimeServerAddr;
use crate::biz;
use crate::biz::collab::ops::{
get_user_favorite_folder_views, get_user_recent_folder_views, get_user_trash_folder_views,
};
use crate::biz::workspace;
use crate::biz::workspace::ops::{
create_comment_on_published_view, create_reaction_on_comment, get_comments_on_published_view,
@ -174,6 +177,11 @@ pub fn workspace_scope() -> Scope {
.service(
web::resource("/{workspace_id}/folder").route(web::get().to(get_workspace_folder_handler)),
)
.service(web::resource("/{workspace_id}/recent").route(web::get().to(get_recent_views_handler)))
.service(
web::resource("/{workspace_id}/favorite").route(web::get().to(get_favorite_views_handler)),
)
.service(web::resource("/{workspace_id}/trash").route(web::get().to(get_trash_views_handler)))
.service(
web::resource("/published-outline/{publish_namespace}")
.route(web::get().to(get_workspace_publish_outline_handler)),
@ -1322,6 +1330,65 @@ async fn get_workspace_folder_handler(
Ok(Json(AppResponse::Ok().with_data(folder_view)))
}
async fn get_recent_views_handler(
user_uuid: UserUuid,
workspace_id: web::Path<Uuid>,
state: Data<AppState>,
) -> Result<Json<AppResponse<SectionItems>>> {
let uid = state.user_cache.get_user_uid(&user_uuid).await?;
let workspace_id = workspace_id.into_inner();
let folder_views = get_user_recent_folder_views(
state.collab_access_control_storage.clone(),
&state.pg_pool,
uid,
workspace_id,
)
.await?;
let section_items = SectionItems {
views: folder_views,
};
Ok(Json(AppResponse::Ok().with_data(section_items)))
}
async fn get_favorite_views_handler(
user_uuid: UserUuid,
workspace_id: web::Path<Uuid>,
state: Data<AppState>,
) -> Result<Json<AppResponse<SectionItems>>> {
let uid = state.user_cache.get_user_uid(&user_uuid).await?;
let workspace_id = workspace_id.into_inner();
let folder_views = get_user_favorite_folder_views(
state.collab_access_control_storage.clone(),
&state.pg_pool,
uid,
workspace_id,
)
.await?;
let section_items = SectionItems {
views: folder_views,
};
Ok(Json(AppResponse::Ok().with_data(section_items)))
}
async fn get_trash_views_handler(
user_uuid: UserUuid,
workspace_id: web::Path<Uuid>,
state: Data<AppState>,
) -> Result<Json<AppResponse<SectionItems>>> {
let uid = state.user_cache.get_user_uid(&user_uuid).await?;
let workspace_id = workspace_id.into_inner();
let folder_views = get_user_trash_folder_views(
state.collab_access_control_storage.clone(),
uid,
workspace_id,
)
.await?;
let section_items = SectionItems {
views: folder_views,
};
Ok(Json(AppResponse::Ok().with_data(section_items)))
}
async fn get_workspace_publish_outline_handler(
publish_namespace: web::Path<String>,
state: Data<AppState>,

View File

@ -2,7 +2,7 @@ use std::collections::HashSet;
use app_error::AppError;
use chrono::DateTime;
use collab_folder::{Folder, ViewLayout as CollabFolderViewLayout};
use collab_folder::{Folder, SectionItem, ViewLayout as CollabFolderViewLayout};
use shared_entity::dto::workspace_dto::{FolderView, ViewLayout};
/// Return all folders belonging to a workspace, excluding private sections which the user does not have access to.
@ -107,14 +107,39 @@ fn to_folder_view(
is_private,
is_published: published_view_ids.contains(view_id),
layout: to_view_layout(&view.layout),
created_at: DateTime::from_timestamp(view.created_at, 0).unwrap_or(DateTime::default()),
last_edited_time: DateTime::from_timestamp(view.last_edited_time, 0)
.unwrap_or(DateTime::default()),
created_at: DateTime::from_timestamp(view.created_at, 0).unwrap_or_default(),
last_edited_time: DateTime::from_timestamp(view.last_edited_time, 0).unwrap_or_default(),
extra,
children,
})
}
pub fn section_items_to_folder_view(
section_items: &[SectionItem],
folder: &Folder,
published_view_ids: &HashSet<String>,
) -> Vec<FolderView> {
section_items
.iter()
.filter_map(|section_item| {
let view = folder.get_view(&section_item.id);
view.map(|v| FolderView {
view_id: v.id.clone(),
name: v.name.clone(),
icon: v.icon.as_ref().map(|icon| to_dto_view_icon(icon.clone())),
is_space: false,
is_private: false,
is_published: published_view_ids.contains(&v.id),
created_at: DateTime::from_timestamp(v.created_at, 0).unwrap_or_default(),
last_edited_time: DateTime::from_timestamp(v.last_edited_time, 0).unwrap_or_default(),
layout: to_view_layout(&v.layout),
extra: v.extra.as_ref().map(|e| parse_extra_field_as_json(e)),
children: vec![],
})
})
.collect()
}
pub fn view_is_space(view: &collab_folder::View) -> bool {
let extra = match view.extra.as_ref() {
Some(extra) => extra,
@ -133,6 +158,13 @@ pub fn view_is_space(view: &collab_folder::View) -> bool {
}
}
pub fn parse_extra_field_as_json(extra: &str) -> serde_json::Value {
serde_json::from_str::<serde_json::Value>(extra).unwrap_or_else(|e| {
tracing::warn!("failed to parse extra field({}): {}", extra, e);
serde_json::Value::Null
})
}
pub fn to_dto_view_icon(
icon: collab_folder::ViewIcon,
) -> shared_entity::dto::workspace_dto::ViewIcon {

View File

@ -4,6 +4,7 @@ use app_error::AppError;
use appflowy_collaborate::collab::storage::CollabAccessControlStorage;
use collab_entity::CollabType;
use collab_entity::EncodedCollab;
use collab_folder::SectionItem;
use collab_folder::{CollabOrigin, Folder};
use database::collab::{CollabStorage, GetCollabOrigin};
use database::publish::select_published_view_ids_for_workspace;
@ -27,6 +28,7 @@ use database_entity::dto::{
};
use super::folder_view::collab_folder_to_folder_view;
use super::folder_view::section_items_to_folder_view;
use super::publish_outline::collab_folder_to_published_outline;
/// Create a new collab member
@ -156,6 +158,93 @@ pub async fn get_collab_member_list(
Ok(collab_member)
}
pub async fn get_user_favorite_folder_views(
collab_storage: Arc<CollabAccessControlStorage>,
pg_pool: &PgPool,
uid: i64,
workspace_id: Uuid,
) -> Result<Vec<FolderView>, AppError> {
let folder = get_latest_collab_folder(
collab_storage,
GetCollabOrigin::User { uid },
&workspace_id.to_string(),
)
.await?;
let publish_view_ids = select_published_view_ids_for_workspace(pg_pool, workspace_id).await?;
let publish_view_ids: HashSet<String> = publish_view_ids
.into_iter()
.map(|id| id.to_string())
.collect();
let deleted_section_item_ids: Vec<String> = folder
.get_my_trash_sections()
.iter()
.map(|s| s.id.clone())
.collect();
let favorite_section_items: Vec<SectionItem> = folder
.get_my_favorite_sections()
.into_iter()
.filter(|s| !deleted_section_item_ids.contains(&s.id))
.collect();
Ok(section_items_to_folder_view(
&favorite_section_items,
&folder,
&publish_view_ids,
))
}
pub async fn get_user_recent_folder_views(
collab_storage: Arc<CollabAccessControlStorage>,
pg_pool: &PgPool,
uid: i64,
workspace_id: Uuid,
) -> Result<Vec<FolderView>, AppError> {
let folder = get_latest_collab_folder(
collab_storage,
GetCollabOrigin::User { uid },
&workspace_id.to_string(),
)
.await?;
let deleted_section_item_ids: Vec<String> = folder
.get_my_trash_sections()
.iter()
.map(|s| s.id.clone())
.collect();
let recent_section_items: Vec<SectionItem> = folder
.get_my_recent_sections()
.into_iter()
.filter(|s| !deleted_section_item_ids.contains(&s.id))
.collect();
let publish_view_ids = select_published_view_ids_for_workspace(pg_pool, workspace_id).await?;
let publish_view_ids: HashSet<String> = publish_view_ids
.into_iter()
.map(|id| id.to_string())
.collect();
Ok(section_items_to_folder_view(
&recent_section_items,
&folder,
&publish_view_ids,
))
}
pub async fn get_user_trash_folder_views(
collab_storage: Arc<CollabAccessControlStorage>,
uid: i64,
workspace_id: Uuid,
) -> Result<Vec<FolderView>, AppError> {
let folder = get_latest_collab_folder(
collab_storage,
GetCollabOrigin::User { uid },
&workspace_id.to_string(),
)
.await?;
let section_items = folder.get_my_trash_sections();
Ok(section_items_to_folder_view(
&section_items,
&folder,
&HashSet::default(),
))
}
pub async fn get_user_workspace_structure(
collab_storage: Arc<CollabAccessControlStorage>,
pg_pool: &PgPool,

View File

@ -1,4 +1,7 @@
use client_api::entity::{CreateCollabParams, QueryCollabParams};
use client_api_test::generate_unique_registered_user_client;
use collab::core::origin::CollabClient;
use collab_folder::{CollabOrigin, Folder};
#[tokio::test]
async fn get_workpace_folder() {
@ -31,3 +34,65 @@ async fn get_workpace_folder() {
.unwrap();
assert_eq!(folder_view.children.len(), 2);
}
#[tokio::test]
async fn get_section_items() {
let (c, _user) = generate_unique_registered_user_client().await;
let user_workspace_info = c.get_user_workspace_info().await.unwrap();
let workspaces = c.get_workspaces().await.unwrap();
assert_eq!(workspaces.len(), 1);
let workspace_id = workspaces[0].workspace_id.to_string();
let folder_collab = c
.get_collab(QueryCollabParams::new(
workspace_id.clone(),
collab_entity::CollabType::Folder,
workspace_id.clone(),
))
.await
.unwrap()
.encode_collab;
let uid = user_workspace_info.user_profile.uid;
let mut folder = Folder::from_collab_doc_state(
uid,
CollabOrigin::Client(CollabClient::new(uid, c.device_id.clone())),
folder_collab.into(),
&workspace_id,
vec![],
)
.unwrap();
let views = folder.get_views_belong_to(&workspace_id);
let new_favorite_id = views[0].children[0].id.clone();
let to_be_deleted_favorite_id = views[0].children[1].id.clone();
folder.add_favorite_view_ids(vec![
new_favorite_id.clone(),
to_be_deleted_favorite_id.clone(),
]);
folder.add_trash_view_ids(vec![to_be_deleted_favorite_id.clone()]);
let recent_id = folder.get_views_belong_to(&new_favorite_id)[0].id.clone();
folder.add_recent_view_ids(vec![recent_id.clone()]);
let collab_type = collab_entity::CollabType::Folder;
c.update_collab(CreateCollabParams {
workspace_id: workspace_id.clone(),
collab_type: collab_type.clone(),
object_id: workspace_id.clone(),
encoded_collab_v1: folder
.encode_collab_v1(|collab| collab_type.validate_require_data(collab))
.unwrap()
.encode_to_bytes()
.unwrap(),
})
.await
.unwrap();
let favorite_section_items = c.get_workspace_favorite(&workspace_id).await.unwrap();
assert_eq!(favorite_section_items.views.len(), 1);
assert_eq!(favorite_section_items.views[0].view_id, new_favorite_id);
let trash_section_items = c.get_workspace_trash(&workspace_id).await.unwrap();
assert_eq!(trash_section_items.views.len(), 1);
assert_eq!(
trash_section_items.views[0].view_id,
to_be_deleted_favorite_id
);
let recent_section_items = c.get_workspace_recent(&workspace_id).await.unwrap();
assert_eq!(recent_section_items.views.len(), 1);
assert_eq!(recent_section_items.views[0].view_id, recent_id);
}