tags, folder rename, other stuff
This commit is contained in:
@@ -167,7 +167,8 @@ pub async fn update_correspondent(
|
||||
}
|
||||
|
||||
if new_name.is_none() && new_metadata.is_none() {
|
||||
return Err(AppError::bad_request("no changes supplied"));
|
||||
let usage = load_usage_for_correspondent(&mut conn, correspondent_id)?;
|
||||
return Ok(Json(build_summary(existing.clone(), usage)));
|
||||
}
|
||||
|
||||
let mut changeset = CorrespondentChangeset::default();
|
||||
|
||||
@@ -39,7 +39,9 @@ pub struct EnsureFolderPathRequest {
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateFolderRequest {
|
||||
pub parent_id: Option<Uuid>,
|
||||
#[serde(default)]
|
||||
pub parent_id: Option<Option<Uuid>>,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -597,50 +599,69 @@ pub async fn delete_folder(
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
pub async fn update_folder_parent(
|
||||
pub async fn update_folder(
|
||||
State(state): State<AppState>,
|
||||
Path(folder_id): Path<Uuid>,
|
||||
Json(payload): Json<UpdateFolderRequest>,
|
||||
) -> AppResult<StatusCode> {
|
||||
if payload.parent_id == Some(folder_id) {
|
||||
return Err(AppError::bad_request("folder cannot be its own parent"));
|
||||
}
|
||||
|
||||
let mut conn = state.db()?;
|
||||
|
||||
conn.transaction::<(), AppError, _>(|conn| {
|
||||
let folder: Folder = folders::table.find(folder_id).first(conn)?;
|
||||
let current_parent = folder.parent_id;
|
||||
let next_parent = payload.parent_id;
|
||||
|
||||
if current_parent == next_parent {
|
||||
return Ok(());
|
||||
}
|
||||
let mut next_parent = folder.parent_id;
|
||||
let mut parent_changed = false;
|
||||
|
||||
if let Some(parent_id) = next_parent {
|
||||
let _parent: Folder = folders::table.find(parent_id).first(conn)?;
|
||||
}
|
||||
|
||||
if let Some(parent_id) = next_parent {
|
||||
let descendant_ids = gather_descendant_folder_ids(conn, folder_id)?;
|
||||
if descendant_ids.contains(&parent_id) {
|
||||
return Err(AppError::bad_request(
|
||||
"cannot move folder into itself or a descendant",
|
||||
));
|
||||
if let Some(parent_request) = payload.parent_id {
|
||||
if parent_request == Some(folder_id) {
|
||||
return Err(AppError::bad_request("folder cannot be its own parent"));
|
||||
}
|
||||
|
||||
if let Some(parent_id) = parent_request {
|
||||
let _parent: Folder = folders::table.find(parent_id).first(conn)?;
|
||||
|
||||
let descendant_ids = gather_descendant_folder_ids(conn, folder_id)?;
|
||||
if descendant_ids.contains(&parent_id) {
|
||||
return Err(AppError::bad_request(
|
||||
"cannot move folder into itself or a descendant",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
parent_changed = parent_request != folder.parent_id;
|
||||
next_parent = parent_request;
|
||||
}
|
||||
|
||||
let mut new_name = folder.name.clone();
|
||||
let mut name_changed = false;
|
||||
|
||||
if let Some(name) = payload.name {
|
||||
let trimmed = name.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err(AppError::bad_request("name must not be empty"));
|
||||
}
|
||||
|
||||
if trimmed != folder.name {
|
||||
new_name = trimmed.to_string();
|
||||
name_changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !parent_changed && !name_changed {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let conflict = if let Some(parent_id) = next_parent {
|
||||
folders::table
|
||||
.filter(folders::parent_id.eq(Some(parent_id)))
|
||||
.filter(folders::name.eq(&folder.name))
|
||||
.filter(folders::name.eq(&new_name))
|
||||
.filter(folders::id.ne(folder_id))
|
||||
.first::<Folder>(conn)
|
||||
.optional()?
|
||||
} else {
|
||||
folders::table
|
||||
.filter(folders::parent_id.is_null())
|
||||
.filter(folders::name.eq(&folder.name))
|
||||
.filter(folders::name.eq(&new_name))
|
||||
.filter(folders::id.ne(folder_id))
|
||||
.first::<Folder>(conn)
|
||||
.optional()?
|
||||
@@ -652,11 +673,12 @@ pub async fn update_folder_parent(
|
||||
));
|
||||
}
|
||||
|
||||
let new_path = build_path_cache(conn, next_parent, &folder.name)?;
|
||||
let new_path = build_path_cache(conn, next_parent, &new_name)?;
|
||||
|
||||
diesel::update(folders::table.find(folder_id))
|
||||
.set((
|
||||
folders::parent_id.eq(next_parent),
|
||||
folders::name.eq(&new_name),
|
||||
folders::path_cache.eq(new_path),
|
||||
))
|
||||
.execute(conn)?;
|
||||
|
||||
@@ -95,7 +95,7 @@ pub fn create_router(state: AppState) -> Router<()> {
|
||||
.route("/path", post(folders::ensure_folder_path))
|
||||
.route(
|
||||
"/:id",
|
||||
delete(folders::delete_folder).patch(folders::update_folder_parent),
|
||||
delete(folders::delete_folder).patch(folders::update_folder),
|
||||
)
|
||||
.route("/:id/contents", get(folders::list_folder_contents))
|
||||
.route("/:id/documents", get(folders::search_documents));
|
||||
|
||||
@@ -112,7 +112,16 @@ pub async fn update_tag(
|
||||
if matches!(label_class, NullableValue::Omitted)
|
||||
&& matches!(color_class, NullableValue::Omitted)
|
||||
{
|
||||
return Err(AppError::bad_request("no changes supplied"));
|
||||
let usage_count: i64 = document_tags::table
|
||||
.filter(document_tags::tag_id.eq(tag_id))
|
||||
.select(count_star())
|
||||
.first(&mut conn)?;
|
||||
return Ok(Json(TagCatalogEntry {
|
||||
id: existing.id,
|
||||
label: existing.label.clone(),
|
||||
color: existing.color.clone(),
|
||||
usage_count,
|
||||
}));
|
||||
}
|
||||
|
||||
let mut new_label: Option<String> = None;
|
||||
@@ -163,7 +172,16 @@ pub async fn update_tag(
|
||||
}
|
||||
|
||||
if !label_changed && !color_changed {
|
||||
return Err(AppError::bad_request("no changes supplied"));
|
||||
let usage_count: i64 = document_tags::table
|
||||
.filter(document_tags::tag_id.eq(tag_id))
|
||||
.select(count_star())
|
||||
.first(&mut conn)?;
|
||||
return Ok(Json(TagCatalogEntry {
|
||||
id: existing.id,
|
||||
label: existing.label.clone(),
|
||||
color: existing.color.clone(),
|
||||
usage_count,
|
||||
}));
|
||||
}
|
||||
|
||||
let changeset = UpdateTagChangeset {
|
||||
|
||||
@@ -15,10 +15,14 @@ struct FolderResponse {
|
||||
#[derive(Deserialize)]
|
||||
struct FolderInfo {
|
||||
id: Uuid,
|
||||
name: String,
|
||||
path_cache: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct FolderContents {
|
||||
folder: Option<FolderInfo>,
|
||||
subfolders: Vec<FolderInfo>,
|
||||
documents: Vec<DocSummary>,
|
||||
}
|
||||
|
||||
@@ -39,6 +43,14 @@ struct EnsureFolderPath<'a> {
|
||||
segments: &'a [&'a str],
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct UpdateFolderRequest {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
parent_id: Option<Option<Uuid>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct MoveDocumentRequest {
|
||||
folder_id: Option<Uuid>,
|
||||
@@ -215,3 +227,80 @@ async fn ensure_path_creates_nested_folders() -> Result<()> {
|
||||
app.cleanup().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn folder_rename_updates_name_and_child_paths() -> Result<()> {
|
||||
let _lock = acquire_db_lock().await;
|
||||
let app = TestApp::new().await?;
|
||||
|
||||
let password = "renamepass";
|
||||
app.insert_user("rename-admin", password, "admin").await?;
|
||||
let token = app.login_token("rename-admin", password).await?;
|
||||
|
||||
let parent_resp = app
|
||||
.post_json(
|
||||
"/api/folders",
|
||||
&CreateFolder {
|
||||
name: "Projects",
|
||||
parent_id: None,
|
||||
},
|
||||
Some(&token),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(parent_resp.status(), StatusCode::OK);
|
||||
let parent_body = body_to_vec(parent_resp.into_body()).await?;
|
||||
let parent: FolderResponse = serde_json::from_slice(&parent_body)?;
|
||||
|
||||
let child_resp = app
|
||||
.post_json(
|
||||
"/api/folders",
|
||||
&CreateFolder {
|
||||
name: "Q1",
|
||||
parent_id: Some(parent.folder.id),
|
||||
},
|
||||
Some(&token),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(child_resp.status(), StatusCode::OK);
|
||||
let child_body = body_to_vec(child_resp.into_body()).await?;
|
||||
let child: FolderResponse = serde_json::from_slice(&child_body)?;
|
||||
|
||||
let rename_resp = app
|
||||
.patch_json(
|
||||
&format!("/api/folders/{}", parent.folder.id),
|
||||
&UpdateFolderRequest {
|
||||
parent_id: None,
|
||||
name: Some("Archive".to_string()),
|
||||
},
|
||||
Some(&token),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(rename_resp.status(), StatusCode::NO_CONTENT);
|
||||
|
||||
let root_contents = app.get("/api/folders/root/contents", Some(&token)).await?;
|
||||
assert_eq!(root_contents.status(), StatusCode::OK);
|
||||
let root_body = body_to_vec(root_contents.into_body()).await?;
|
||||
let root: FolderContents = serde_json::from_slice(&root_body)?;
|
||||
let renamed = root
|
||||
.subfolders
|
||||
.iter()
|
||||
.find(|f| f.id == parent.folder.id)
|
||||
.expect("renamed folder present");
|
||||
assert_eq!(renamed.name, "Archive");
|
||||
assert_eq!(renamed.path_cache.as_deref(), Some("/Archive"));
|
||||
|
||||
let child_contents = app
|
||||
.get(
|
||||
&format!("/api/folders/{}/contents", child.folder.id),
|
||||
Some(&token),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(child_contents.status(), StatusCode::OK);
|
||||
let child_contents_body = body_to_vec(child_contents.into_body()).await?;
|
||||
let child_details: FolderContents = serde_json::from_slice(&child_contents_body)?;
|
||||
let child_folder = child_details.folder.expect("child folder info");
|
||||
assert_eq!(child_folder.path_cache.as_deref(), Some("/Archive/Q1"));
|
||||
|
||||
app.cleanup().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user