Merge pull request #811 from AppFlowy-IO/add-is-published-to-folder-struct

feat: add additional fields to FolderView and support different root id
This commit is contained in:
Khor Shu Heng 2024-09-11 23:23:37 +08:00 committed by GitHub
commit 57a30817fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 176 additions and 168 deletions

View File

@ -139,6 +139,9 @@ pub enum AppError {
#[error("{0}")]
InvalidPublishedOutline(String),
#[error("{0}")]
InvalidFolderView(String),
}
impl AppError {
@ -204,6 +207,7 @@ impl AppError {
AppError::StringLengthLimitReached(_) => ErrorCode::StringLengthLimitReached,
AppError::InvalidContentType(_) => ErrorCode::InvalidContentType,
AppError::InvalidPublishedOutline(_) => ErrorCode::InvalidPublishedOutline,
AppError::InvalidFolderView(_) => ErrorCode::InvalidFolderView,
}
}
}
@ -323,6 +327,7 @@ pub enum ErrorCode {
SingleUploadLimitExceeded = 1037,
AppleRevokeTokenError = 1038,
InvalidPublishedOutline = 1039,
InvalidFolderView = 1040,
}
impl ErrorCode {

View File

@ -687,12 +687,16 @@ impl Client {
&self,
workspace_id: &str,
depth: Option<u32>,
root_view_id: Option<String>,
) -> Result<FolderView, AppResponseError> {
let url = format!("{}/api/workspace/{}/folder", self.base_url, workspace_id);
let resp = self
.http_client_with_auth(Method::GET, &url)
.await?
.query(&QueryWorkspaceFolder { depth })
.query(&QueryWorkspaceFolder {
depth,
root_view_id,
})
.send()
.await?;
log_request_id(&resp);

View File

@ -135,6 +135,10 @@ pub struct FolderView {
pub icon: Option<ViewIcon>,
pub is_space: bool,
pub is_private: bool,
pub is_published: bool,
pub layout: ViewLayout,
pub created_at: DateTime<Utc>,
pub last_edited_time: DateTime<Utc>,
/// contains fields like `is_space`, and font information
pub extra: Option<serde_json::Value>,
pub children: Vec<FolderView>,
@ -178,6 +182,7 @@ pub struct QueryWorkspaceParam {
#[derive(Default, Debug, Deserialize, Serialize)]
pub struct QueryWorkspaceFolder {
pub depth: Option<u32>,
pub root_view_id: Option<String>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]

View File

@ -1298,17 +1298,25 @@ async fn get_workspace_usage_handler(
async fn get_workspace_folder_handler(
user_uuid: UserUuid,
workspace_id: web::Path<String>,
workspace_id: web::Path<Uuid>,
state: Data<AppState>,
query: web::Query<QueryWorkspaceFolder>,
) -> Result<Json<AppResponse<FolderView>>> {
let depth = query.depth.unwrap_or(1);
let uid = state.user_cache.get_user_uid(&user_uuid).await?;
let workspace_id = workspace_id.into_inner();
let root_view_id = if let Some(root_view_id) = query.root_view_id.as_ref() {
root_view_id.to_string()
} else {
workspace_id.to_string()
};
let folder_view = biz::collab::ops::get_user_workspace_structure(
state.collab_access_control_storage.clone(),
&state.pg_pool,
uid,
workspace_id.into_inner(),
workspace_id,
depth,
&root_view_id,
)
.await?;
Ok(Json(AppResponse::Ok().with_data(folder_view)))

View File

@ -1,10 +1,17 @@
use std::collections::HashSet;
use app_error::AppError;
use chrono::DateTime;
use collab_folder::{Folder, ViewLayout as CollabFolderViewLayout};
use shared_entity::dto::workspace_dto::{FolderView, ViewLayout};
use uuid::Uuid;
pub fn collab_folder_to_folder_view(folder: &Folder, depth: u32) -> FolderView {
/// Return all folders belonging to a workspace, excluding private sections which the user does not have access to.
pub fn collab_folder_to_folder_view(
root_view_id: &str,
folder: &Folder,
max_depth: u32,
pubished_view_ids: &HashSet<String>,
) -> Result<FolderView, AppError> {
let mut unviewable = HashSet::new();
for private_section in folder.get_all_private_sections() {
unviewable.insert(private_section.id);
@ -13,117 +20,99 @@ pub fn collab_folder_to_folder_view(folder: &Folder, depth: u32) -> FolderView {
unviewable.insert(trash_view.id);
}
let mut private_views = HashSet::new();
let mut private_view_ids = HashSet::new();
for private_section in folder.get_my_private_sections() {
unviewable.remove(&private_section.id);
private_views.insert(private_section.id);
private_view_ids.insert(private_section.id);
}
let workspace_id = folder.get_workspace_id().unwrap_or_else(|| {
tracing::error!("failed to get workspace_id");
Uuid::nil().to_string()
});
let root = match folder.get_view(&workspace_id) {
Some(root) => root,
to_folder_view(
"",
root_view_id,
folder,
&unviewable,
&private_view_ids,
pubished_view_ids,
false,
0,
max_depth,
)
.ok_or(AppError::InvalidFolderView(format!(
"There is no valid folder view belonging to the root view id: {}",
root_view_id
)))
}
#[allow(clippy::too_many_arguments)]
fn to_folder_view(
parent_view_id: &str,
view_id: &str,
folder: &Folder,
unviewable: &HashSet<String>,
private_view_ids: &HashSet<String>,
published_view_ids: &HashSet<String>,
parent_is_private: bool,
depth: u32,
max_depth: u32,
) -> Option<FolderView> {
if depth > max_depth || unviewable.contains(view_id) {
return None;
}
let view = match folder.get_view(view_id) {
Some(view) => view,
None => {
tracing::error!("failed to get root view, workspace_id: {}", workspace_id);
return FolderView::default();
return None;
},
};
let extra = root.extra.as_deref().map(|extra| {
// There is currently a bug, in which the parent_view_id is not always set correctly
if !(parent_view_id.is_empty() || view.parent_view_id == parent_view_id) {
return None;
}
let is_private =
parent_is_private || (view_is_space(&view) && private_view_ids.contains(view_id));
let extra = view.extra.as_deref().map(|extra| {
serde_json::from_str::<serde_json::Value>(extra).unwrap_or_else(|e| {
tracing::error!("failed to parse extra field({}): {}", extra, e);
tracing::warn!("failed to parse extra field({}): {}", extra, e);
serde_json::Value::Null
})
});
FolderView {
view_id: root.id.clone(),
name: root.name.clone(),
icon: root
let children: Vec<FolderView> = view
.children
.iter()
.filter_map(|child_view_id| {
to_folder_view(
view_id,
&child_view_id.id,
folder,
unviewable,
private_view_ids,
published_view_ids,
is_private,
depth + 1,
max_depth,
)
})
.collect();
Some(FolderView {
view_id: view_id.to_string(),
name: view.name.clone(),
icon: view
.icon
.as_ref()
.map(|icon| to_dto_view_icon(icon.clone())),
is_space: false,
is_private: false,
is_space: view_is_space(&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()),
extra,
children: if depth == 0 {
vec![]
} else {
root
.children
.iter()
.filter(|v| !unviewable.contains(&v.id))
.map(|v| {
let intermediate = FolderViewIntermediate {
folder,
view_id: &v.id,
unviewable: &unviewable,
private_views: &private_views,
depth,
};
FolderView::from(intermediate)
})
.collect()
},
}
}
struct FolderViewIntermediate<'a> {
folder: &'a Folder,
view_id: &'a str,
unviewable: &'a HashSet<String>,
private_views: &'a HashSet<String>,
depth: u32,
}
impl<'a> From<FolderViewIntermediate<'a>> for FolderView {
fn from(fv: FolderViewIntermediate) -> Self {
let view = match fv.folder.get_view(fv.view_id) {
Some(view) => view,
None => {
tracing::error!("failed to get view, view_id: {}", fv.view_id);
return Self::default();
},
};
let extra = view.extra.as_deref().map(|extra| {
serde_json::from_str::<serde_json::Value>(extra).unwrap_or_else(|e| {
tracing::error!("failed to parse extra field({}): {}", extra, e);
serde_json::Value::Null
})
});
Self {
view_id: view.id.clone(),
name: view.name.clone(),
icon: view
.icon
.as_ref()
.map(|icon| to_dto_view_icon(icon.clone())),
is_space: view_is_space(&view),
is_private: fv.private_views.contains(&view.id),
extra,
children: if fv.depth == 1 {
vec![]
} else {
view
.children
.iter()
.filter(|v| !fv.unviewable.contains(&v.id))
.map(|v| {
FolderView::from(FolderViewIntermediate {
folder: fv.folder,
view_id: &v.id,
unviewable: fv.unviewable,
private_views: fv.private_views,
depth: fv.depth - 1,
})
})
.collect()
},
}
}
children,
})
}
pub fn view_is_space(view: &collab_folder::View) -> bool {

View File

@ -158,9 +158,11 @@ pub async fn get_collab_member_list(
pub async fn get_user_workspace_structure(
collab_storage: Arc<CollabAccessControlStorage>,
pg_pool: &PgPool,
uid: i64,
workspace_id: String,
workspace_id: Uuid,
depth: u32,
root_view_id: &str,
) -> Result<FolderView, AppError> {
let depth_limit = 10;
if depth > depth_limit {
@ -169,10 +171,18 @@ pub async fn get_user_workspace_structure(
depth, depth_limit
)));
}
let folder =
get_latest_collab_folder(collab_storage, GetCollabOrigin::User { uid }, &workspace_id).await?;
let folder_view: FolderView = collab_folder_to_folder_view(&folder, depth);
Ok(folder_view)
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();
collab_folder_to_folder_view(root_view_id, &folder, depth, &publish_view_ids)
}
pub async fn get_latest_collab_folder(
@ -245,6 +255,6 @@ pub async fn get_published_view(
.map(|id| id.to_string())
.collect();
let published_view: PublishedView =
collab_folder_to_published_outline(&folder, &publish_view_ids)?;
collab_folder_to_published_outline(&workspace_id.to_string(), &folder, &publish_view_ids)?;
Ok(published_view)
}

View File

@ -6,7 +6,10 @@ use shared_entity::dto::workspace_dto::PublishedView;
use super::folder_view::{to_dto_view_icon, to_view_layout};
/// Returns only folders that are published, or one of the nested subfolders is published.
/// Exclude folders that are in the trash.
pub fn collab_folder_to_published_outline(
root_view_id: &str,
folder: &Folder,
publish_view_ids: &HashSet<String>,
) -> Result<PublishedView, AppError> {
@ -15,55 +18,20 @@ pub fn collab_folder_to_published_outline(
unviewable.insert(trash_view.id);
}
let workspace_id = folder
.get_workspace_id()
.ok_or_else(|| AppError::InvalidPublishedOutline("failed to get workspace_id".to_string()))?;
let root = match folder.get_view(&workspace_id) {
Some(root) => root,
None => {
return Err(AppError::InvalidPublishedOutline(
"failed to get root view".to_string(),
));
},
};
let extra = root.extra.as_deref().map(|extra| {
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
})
});
// Set a reasonable max depth to prevent execessive recursion
let max_depth = 10;
let published_view = PublishedView {
view_id: root.id.clone(),
name: root.name.clone(),
icon: root
.icon
.as_ref()
.map(|icon| to_dto_view_icon(icon.clone())),
layout: to_view_layout(&root.layout),
is_published: false,
extra,
children: root
.children
.iter()
.filter(|v| !unviewable.contains(&v.id))
.filter_map(|v| {
to_publish_view(
&root.id,
&v.id,
folder,
&unviewable,
publish_view_ids,
0,
max_depth,
)
})
.collect(),
};
Ok(published_view)
to_publish_view(
"",
root_view_id,
folder,
&unviewable,
publish_view_ids,
0,
max_depth,
)
.ok_or(AppError::InvalidPublishedOutline(format!(
"failed to get published outline for root view id: {}",
root_view_id
)))
}
fn to_publish_view(
@ -75,7 +43,7 @@ fn to_publish_view(
depth: u32,
max_depth: u32,
) -> Option<PublishedView> {
if depth > max_depth {
if depth > max_depth || unviewable.contains(view_id) {
return None;
}
@ -87,7 +55,7 @@ fn to_publish_view(
};
// There is currently a bug, in which the parent_view_id is not always set correctly
if view.parent_view_id != parent_view_id {
if !(parent_view_id.is_empty() || view.parent_view_id == parent_view_id) {
return None;
}
@ -100,7 +68,6 @@ fn to_publish_view(
let pruned_view: Vec<PublishedView> = view
.children
.iter()
.filter(|v| !unviewable.contains(&v.id))
.filter_map(|child_view_id| {
to_publish_view(
view_id,
@ -114,7 +81,7 @@ fn to_publish_view(
})
.collect();
let is_published = publish_view_ids.contains(view_id);
if is_published || !pruned_view.is_empty() {
if parent_view_id.is_empty() || is_published || !pruned_view.is_empty() {
Some(PublishedView {
view_id: view.id.clone(),
name: view.name.clone(),

View File

@ -740,7 +740,7 @@ async fn duplicate_to_workspace_references() {
let workspace_id_2 = client_2.workspace_id().await;
let fv = client_2
.api_client
.get_workspace_folder(&workspace_id_2, Some(5))
.get_workspace_folder(&workspace_id_2, Some(5), None)
.await
.unwrap();
@ -765,7 +765,7 @@ async fn duplicate_to_workspace_references() {
let fv = client_2
.api_client
.get_workspace_folder(&workspace_id_2, Some(5))
.get_workspace_folder(&workspace_id_2, Some(5), None)
.await
.unwrap();
@ -847,7 +847,7 @@ async fn duplicate_to_workspace_doc_inline_database() {
let fv = client_2
.api_client
.get_workspace_folder(&workspace_id_2, Some(5))
.get_workspace_folder(&workspace_id_2, Some(5), None)
.await
.unwrap();
@ -873,7 +873,7 @@ async fn duplicate_to_workspace_doc_inline_database() {
{
let fv = client_2
.api_client
.get_workspace_folder(&workspace_id_2, Some(5))
.get_workspace_folder(&workspace_id_2, Some(5), None)
.await
.unwrap();
let doc_3_fv = fv
@ -913,7 +913,8 @@ async fn duplicate_to_workspace_doc_inline_database() {
)
.unwrap();
let folder_view = collab_folder_to_folder_view(&folder, 5);
let folder_view =
collab_folder_to_folder_view(&workspace_id_2, &folder, 5, &HashSet::default()).unwrap();
let doc_3_fv = folder_view
.children
.into_iter()
@ -1040,7 +1041,7 @@ async fn duplicate_to_workspace_db_embedded_in_doc() {
let fv = client_2
.api_client
.get_workspace_folder(&workspace_id_2, Some(5))
.get_workspace_folder(&workspace_id_2, Some(5), None)
.await
.unwrap();
@ -1064,7 +1065,7 @@ async fn duplicate_to_workspace_db_embedded_in_doc() {
{
let fv = client_2
.api_client
.get_workspace_folder(&workspace_id_2, Some(5))
.get_workspace_folder(&workspace_id_2, Some(5), None)
.await
.unwrap();
let doc_with_embedded_db = fv
@ -1151,7 +1152,7 @@ async fn duplicate_to_workspace_db_with_relation() {
let fv = client_2
.api_client
.get_workspace_folder(&workspace_id_2, Some(5))
.get_workspace_folder(&workspace_id_2, Some(5), None)
.await
.unwrap();
@ -1178,7 +1179,7 @@ async fn duplicate_to_workspace_db_with_relation() {
{
let fv = client_2
.api_client
.get_workspace_folder(&workspace_id_2, Some(5))
.get_workspace_folder(&workspace_id_2, Some(5), None)
.await
.unwrap();
let db_with_rel_col = fv

View File

@ -7,8 +7,27 @@ async fn get_workpace_folder() {
assert_eq!(workspaces.len(), 1);
let workspace_id = workspaces[0].workspace_id.to_string();
let folder_view = c.get_workspace_folder(&workspace_id, None).await.unwrap();
let folder_view = c
.get_workspace_folder(&workspace_id, None, None)
.await
.unwrap();
assert_eq!(folder_view.name, "Workspace");
assert_eq!(folder_view.children[0].name, "General");
assert_eq!(folder_view.children[0].children.len(), 0);
let folder_view = c
.get_workspace_folder(&workspace_id, Some(2), None)
.await
.unwrap();
assert_eq!(folder_view.name, "Workspace");
assert_eq!(folder_view.children[0].name, "General");
assert_eq!(folder_view.children[0].children.len(), 2);
let folder_view = c
.get_workspace_folder(
&workspace_id,
Some(1),
Some(folder_view.children[0].view_id.clone()),
)
.await
.unwrap();
assert_eq!(folder_view.children.len(), 2);
}