folder tree

This commit is contained in:
2025-11-06 00:51:38 +01:00
parent 911051e75d
commit 4027ac66cb
5 changed files with 300 additions and 37 deletions
+89 -3
View File
@@ -2,7 +2,12 @@ use axum::{
extract::{Json, Path, Query, State},
http::StatusCode,
};
use diesel::{dsl::exists, prelude::*, PgConnection};
use diesel::{
dsl::{exists, sql},
prelude::*,
sql_types::Text,
PgConnection,
};
use serde::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use uuid::Uuid;
@@ -18,8 +23,7 @@ use crate::{
use super::documents::{hydrate_documents, DocumentResponse};
use crate::documents::ordering::{ordering_clauses, DocumentSortField, SortDirection};
use crate::utils::{json::deserialize_patch_field, time::to_iso};
use diesel::dsl::sql;
use diesel::sql_types::Text;
use std::collections::HashMap;
#[derive(Deserialize, ToSchema)]
pub struct CreateFolderRequest {
@@ -48,6 +52,18 @@ pub struct FolderContentsResponse {
pub documents: Vec<DocumentResponse>,
}
#[derive(Clone, Serialize, ToSchema)]
pub struct FolderTreeNode {
pub id: Uuid,
pub name: String,
#[schema(nullable)]
pub parent_id: Option<Uuid>,
pub created_at: String,
pub updated_at: String,
#[serde(default)]
pub children: Vec<FolderTreeNode>,
}
#[derive(Deserialize, IntoParams, ToSchema)]
#[into_params(parameter_in = Query)]
pub struct FolderContentsQuery {
@@ -418,6 +434,74 @@ pub async fn list_folder_contents(
}))
}
#[utoipa::path(
get,
path = "/api/folders/tree",
responses((status = 200, description = "Folder hierarchy", body = [FolderTreeNode])),
tag = "Folders"
)]
pub async fn list_folder_tree(
TenantScopedConn {
mut conn,
tenant_id,
..
}: TenantScopedConn,
) -> AppResult<Json<Vec<FolderTreeNode>>> {
let folders: Vec<Folder> = folders::table
.filter(folders::tenant_id.eq(tenant_id))
.order(sql::<Text>("name COLLATE \"unicode_ci\" ASC"))
.load(&mut conn)?;
let mut node_map: HashMap<Uuid, FolderTreeNode> = HashMap::with_capacity(folders.len());
let mut children_map: HashMap<Uuid, Vec<Uuid>> = HashMap::new();
let mut roots: Vec<Uuid> = Vec::new();
for folder in folders {
let id = folder.id;
let parent_id = folder.parent_id;
let node = FolderTreeNode {
id,
name: folder.name,
parent_id,
created_at: to_iso(folder.created_at),
updated_at: to_iso(folder.updated_at),
children: Vec::new(),
};
if let Some(parent) = parent_id {
children_map.entry(parent).or_default().push(id);
} else {
roots.push(id);
}
node_map.insert(id, node);
}
fn build_node(
id: Uuid,
nodes: &HashMap<Uuid, FolderTreeNode>,
child_map: &HashMap<Uuid, Vec<Uuid>>,
) -> FolderTreeNode {
let mut node = nodes.get(&id).cloned().expect("folder node must exist");
if let Some(children) = child_map.get(&id) {
node.children = children
.iter()
.map(|child_id| build_node(*child_id, nodes, child_map))
.collect();
}
node
}
let tree = roots
.iter()
.map(|root_id| build_node(*root_id, &node_map, &children_map))
.collect();
Ok(Json(tree))
}
#[utoipa::path(
delete,
path = "/api/folders/{id}",
@@ -639,6 +723,7 @@ pub(super) fn gather_descendant_folder_ids(
crate::routes::folders::ensure_folder_path,
crate::routes::folders::get_folder,
crate::routes::folders::list_folder_contents,
crate::routes::folders::list_folder_tree,
crate::routes::folders::delete_folder,
crate::routes::folders::update_folder
),
@@ -649,6 +734,7 @@ pub(super) fn gather_descendant_folder_ids(
crate::routes::folders::FolderInfo,
crate::routes::folders::FolderContentsQuery,
crate::routes::folders::FolderContentsResponse,
crate::routes::folders::FolderTreeNode,
crate::routes::folders::UpdateFolderRequest
))
)]