From faf8fec9c7e37bdc626e04db221a3c74dfaf5ee9 Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Fri, 31 Oct 2025 12:55:44 +0100 Subject: [PATCH] revert document types --- .../202510300002_add_document_types/down.sql | 6 - .../202510300002_add_document_types/up.sql | 11 - backend/src/models.rs | 18 - backend/src/openapi.rs | 11 +- backend/src/routes/document_types.rs | 187 -------- backend/src/routes/documents.rs | 196 +-------- backend/src/routes/mod.rs | 13 - backend/src/schema.rs | 13 - backend/tests/common/mod.rs | 10 - backend/tests/document_types.rs | 127 ------ backend/tests/documents_flow.rs | 307 ------------- frontend/src/app/AppLayout.jsx | 408 +----------------- frontend/src/app/DocumentsRoute.jsx | 4 +- frontend/src/app/useManagementModals.js | 60 --- frontend/src/detail/DetailPanel.jsx | 170 -------- .../src/documentTypes/DocumentTypesPanel.jsx | 233 ---------- frontend/src/documents/DocumentsTable.jsx | 105 ----- frontend/src/preview/PreviewWorkspace.jsx | 4 - frontend/src/sidebar/Sidebar.jsx | 87 ---- frontend/src/styles.css | 26 -- 20 files changed, 12 insertions(+), 1984 deletions(-) delete mode 100644 backend/migrations/202510300002_add_document_types/down.sql delete mode 100644 backend/migrations/202510300002_add_document_types/up.sql delete mode 100644 backend/src/routes/document_types.rs delete mode 100644 backend/tests/document_types.rs delete mode 100644 frontend/src/documentTypes/DocumentTypesPanel.jsx diff --git a/backend/migrations/202510300002_add_document_types/down.sql b/backend/migrations/202510300002_add_document_types/down.sql deleted file mode 100644 index a2ba105..0000000 --- a/backend/migrations/202510300002_add_document_types/down.sql +++ /dev/null @@ -1,6 +0,0 @@ -DROP INDEX IF EXISTS documents_document_type_idx; - -ALTER TABLE documents - DROP COLUMN IF EXISTS document_type_id; - -DROP TABLE IF EXISTS document_types; diff --git a/backend/migrations/202510300002_add_document_types/up.sql b/backend/migrations/202510300002_add_document_types/up.sql deleted file mode 100644 index 7f72e15..0000000 --- a/backend/migrations/202510300002_add_document_types/up.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE TABLE document_types ( - id UUID PRIMARY KEY, - tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, - name VARCHAR(100) NOT NULL, - CONSTRAINT document_types_unique_name UNIQUE (tenant_id, name) -); - -ALTER TABLE documents - ADD COLUMN document_type_id UUID REFERENCES document_types(id) ON DELETE SET NULL; - -CREATE INDEX documents_document_type_idx ON documents(document_type_id); diff --git a/backend/src/models.rs b/backend/src/models.rs index fd4ec24..3273eaa 100644 --- a/backend/src/models.rs +++ b/backend/src/models.rs @@ -248,23 +248,6 @@ pub struct Document { pub title: String, pub current_version_id: Uuid, pub tenant_id: Uuid, - pub document_type_id: Option, -} - -#[derive(Debug, Clone, Queryable, Identifiable)] -#[diesel(table_name = document_types)] -pub struct DocumentType { - pub id: Uuid, - pub tenant_id: Uuid, - pub name: String, -} - -#[derive(Debug, Insertable)] -#[diesel(table_name = document_types)] -pub struct NewDocumentType { - pub id: Uuid, - pub tenant_id: Uuid, - pub name: String, } #[derive(Debug, Insertable)] @@ -280,7 +263,6 @@ pub struct NewDocument { pub issued_at: Option, pub title: String, pub tenant_id: Uuid, - pub document_type_id: Option, } #[derive(Debug, Clone, Queryable, Identifiable, Associations)] diff --git a/backend/src/openapi.rs b/backend/src/openapi.rs index 81ce2ba..24f3525 100644 --- a/backend/src/openapi.rs +++ b/backend/src/openapi.rs @@ -10,7 +10,6 @@ impl OpenApi for ApiDoc { doc.merge(crate::routes::documents::DocumentsApiDoc::openapi()); doc.merge(crate::routes::folders::FoldersApiDoc::openapi()); doc.merge(crate::routes::tags::TagsApiDoc::openapi()); - doc.merge(crate::routes::document_types::DocumentTypesApiDoc::openapi()); doc.merge(crate::routes::correspondents::CorrespondentsApiDoc::openapi()); doc.merge(crate::routes::profile::ProfileApiDoc::openapi()); @@ -44,10 +43,6 @@ impl OpenApi for ApiDoc { .name("Tags") .description(Some("Tag catalog")) .build(), - TagBuilder::new() - .name("DocumentTypes") - .description(Some("Document type catalog")) - .build(), TagBuilder::new() .name("Correspondents") .description(Some("Correspondent catalog")) @@ -82,16 +77,14 @@ pub mod schemas { CorrespondentSummary, CorrespondentUsage, CreateCorrespondentRequest, UpdateCorrespondentRequest, }; - pub use crate::routes::document_types::{CreateDocumentTypeRequest, UpdateDocumentTypeRequest}; pub use crate::routes::documents::{ AssetObjectsQuery, AssetRequestQuery, AssignCorrespondentsRequest, AssignTagsRequest, BulkCorrespondentAction, BulkCorrespondentResponse, BulkCorrespondentsRequest, BulkMoveRequest, BulkMoveResponse, BulkReanalyzeResponse, BulkReanalyzeSelectionRequest, BulkTagAction, BulkTagRequest, BulkTagResponse, CorrespondentAssignmentInput, DocumentCheckQuery, DocumentCheckResponse, DocumentDetailResponse, DocumentListQuery, - DocumentMetadataUpdate, DocumentResponse, DocumentStatusFilter, DocumentTypeResponse, - MoveDocumentRequest, RestoreDocumentRequest, TagResponse, UpdateDocumentRequest, - UploadDocumentForm, + DocumentMetadataUpdate, DocumentResponse, DocumentStatusFilter, MoveDocumentRequest, + RestoreDocumentRequest, TagResponse, UpdateDocumentRequest, UploadDocumentForm, }; pub use crate::routes::folders::{ CreateFolderRequest, EnsureFolderPathRequest, FolderContentsQuery, FolderContentsResponse, diff --git a/backend/src/routes/document_types.rs b/backend/src/routes/document_types.rs deleted file mode 100644 index a355ef8..0000000 --- a/backend/src/routes/document_types.rs +++ /dev/null @@ -1,187 +0,0 @@ -use axum::{ - extract::{Json, Path}, - http::StatusCode, -}; -use diesel::prelude::*; -use serde::Deserialize; -use utoipa::ToSchema; -use uuid::Uuid; - -use crate::{ - auth::TenantScopedConn, - error::{AppError, AppResult}, - models::{DocumentType, NewDocumentType}, - schema::document_types, - utils::named_entity::normalize_name, -}; - -use super::documents::DocumentTypeResponse; - -#[derive(Deserialize, ToSchema)] -pub struct CreateDocumentTypeRequest { - pub name: String, -} - -#[derive(Deserialize, ToSchema)] -pub struct UpdateDocumentTypeRequest { - pub name: Option, -} - -#[utoipa::path( - get, - path = "/api/document-types", - responses((status = 200, description = "Document types", body = [DocumentTypeResponse])), - tag = "DocumentTypes" -)] -pub async fn list_document_types( - TenantScopedConn { - mut conn, - tenant_id, - .. - }: TenantScopedConn, -) -> AppResult>> { - let types: Vec = document_types::table - .filter(document_types::tenant_id.eq(tenant_id)) - .order(document_types::name.asc()) - .load(&mut conn)?; - - Ok(Json( - types.into_iter().map(DocumentTypeResponse::from).collect(), - )) -} - -#[utoipa::path( - post, - path = "/api/document-types", - request_body = CreateDocumentTypeRequest, - responses((status = 201, description = "Document type created", body = DocumentTypeResponse)), - tag = "DocumentTypes" -)] -pub async fn create_document_type( - TenantScopedConn { - mut conn, - tenant_id, - .. - }: TenantScopedConn, - Json(payload): Json, -) -> AppResult<(StatusCode, Json)> { - let name = normalize_name(&payload.name, || { - AppError::bad_request("name must not be empty") - })?; - - let new_type = NewDocumentType { - id: Uuid::new_v4(), - tenant_id, - name: name.clone(), - }; - - match diesel::insert_into(document_types::table) - .values(&new_type) - .get_result::(&mut conn) - { - Ok(created) => Ok(( - StatusCode::CREATED, - Json(DocumentTypeResponse::from(created)), - )), - Err(diesel::result::Error::DatabaseError( - diesel::result::DatabaseErrorKind::UniqueViolation, - _, - )) => Err( - AppError::conflict("a document type with that name already exists") - .with_code("duplicate_document_type"), - ), - Err(err) => Err(AppError::from(err)), - } -} - -#[utoipa::path( - patch, - path = "/api/document-types/{id}", - params(("id" = Uuid, Path, description = "Document type ID")), - request_body = UpdateDocumentTypeRequest, - responses((status = 200, description = "Document type updated", body = DocumentTypeResponse)), - tag = "DocumentTypes" -)] -pub async fn update_document_type( - Path(document_type_id): Path, - TenantScopedConn { - mut conn, - tenant_id, - .. - }: TenantScopedConn, - Json(payload): Json, -) -> AppResult> { - let name_raw = payload - .name - .as_ref() - .ok_or_else(|| AppError::bad_request("name must be provided and not empty"))?; - let name = normalize_name(name_raw, || { - AppError::bad_request("name must be provided and not empty") - })?; - - let target = document_types::table - .filter(document_types::tenant_id.eq(tenant_id)) - .find(document_type_id); - - let update_result = diesel::update(target.clone()) - .set(document_types::name.eq(&name)) - .get_result::(&mut conn); - - match update_result { - Ok(updated) => Ok(Json(DocumentTypeResponse::from(updated))), - Err(diesel::result::Error::NotFound) => Err(AppError::not_found()), - Err(diesel::result::Error::DatabaseError( - diesel::result::DatabaseErrorKind::UniqueViolation, - _, - )) => Err( - AppError::conflict("a document type with that name already exists") - .with_code("duplicate_document_type"), - ), - Err(err) => Err(AppError::from(err)), - } -} - -#[utoipa::path( - delete, - path = "/api/document-types/{id}", - params(("id" = Uuid, Path, description = "Document type ID")), - responses((status = 204, description = "Document type deleted")), - tag = "DocumentTypes" -)] -pub async fn delete_document_type( - Path(document_type_id): Path, - TenantScopedConn { - mut conn, - tenant_id, - .. - }: TenantScopedConn, -) -> AppResult { - let affected = diesel::delete( - document_types::table - .filter(document_types::tenant_id.eq(tenant_id)) - .find(document_type_id), - ) - .execute(&mut conn)?; - - if affected == 0 { - return Err(AppError::not_found()); - } - - Ok(StatusCode::NO_CONTENT) -} - -#[derive(utoipa::OpenApi)] -#[openapi( - paths( - crate::routes::document_types::list_document_types, - crate::routes::document_types::create_document_type, - crate::routes::document_types::update_document_type, - crate::routes::document_types::delete_document_type - ), - components(schemas( - crate::routes::document_types::CreateDocumentTypeRequest, - crate::routes::document_types::UpdateDocumentTypeRequest, - crate::routes::documents::DocumentTypeResponse - )) -)] -pub struct DocumentTypesApiDoc; diff --git a/backend/src/routes/documents.rs b/backend/src/routes/documents.rs index 52c2fd4..c3caa9d 100644 --- a/backend/src/routes/documents.rs +++ b/backend/src/routes/documents.rs @@ -37,13 +37,12 @@ use crate::documents::{ use crate::error::{AppError, AppResult}; use crate::jobs::{enqueue_job, JOB_ANALYZE_DOCUMENT, JOB_INDEX_DOCUMENT_TEXT}; use crate::models::{ - Document, DocumentAsset, DocumentAssetObject, DocumentType, DocumentVersion, NewDocument, - NewDocumentTag, NewDocumentVersion, Tag, + Document, DocumentAsset, DocumentAssetObject, DocumentVersion, NewDocument, NewDocumentTag, + NewDocumentVersion, Tag, }; use crate::schema::{ document_asset_objects, document_assets, document_correspondents, document_tags, - document_types, document_versions, documents, folders, refresh_tokens::dsl as refresh_dsl, - tags, + document_versions, documents, folders, refresh_tokens::dsl as refresh_dsl, tags, }; use crate::state::AppState; use crate::utils::{ @@ -68,7 +67,6 @@ pub struct DocumentListQuery { pub query: Option, pub tags: Option, pub correspondents: Option, - pub document_types: Option, #[serde(default = "default_document_status_filter")] #[schema(default = "active")] pub status: DocumentStatusFilter, @@ -141,21 +139,6 @@ impl From for TagResponse { } } -#[derive(Clone, Serialize, Deserialize, ToSchema)] -pub struct DocumentTypeResponse { - pub id: Uuid, - pub name: String, -} - -impl From for DocumentTypeResponse { - fn from(value: DocumentType) -> Self { - Self { - id: value.id, - name: value.name, - } - } -} - #[derive(Serialize, ToSchema)] pub struct DocumentResponse { pub id: Uuid, @@ -174,12 +157,6 @@ pub struct DocumentResponse { pub issued_at: Option, #[schema(value_type = Object)] pub metadata: Value, - #[serde(skip_serializing_if = "Option::is_none")] - #[schema(nullable)] - pub document_type_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - #[schema(nullable)] - pub document_type: Option, pub tags: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub correspondents: Vec, @@ -223,9 +200,6 @@ pub struct UpdateDocumentRequest { #[serde(default)] #[schema(nullable, value_type = Object)] pub metadata: Option, - #[serde(default)] - #[schema(nullable, value_type = Option)] - pub document_type_id: Option, } #[derive(Serialize, ToSchema)] @@ -280,7 +254,6 @@ struct DocumentUpdateChangeset { issued_at: Option>, metadata: Option, updated_at: Option, - document_type_id: Option>, } #[derive(Deserialize, Copy, Clone, PartialEq, Eq, ToSchema)] @@ -325,7 +298,6 @@ struct UploadRequest { correspondents: Vec, issued_at_override: Option, skip_if_existing: bool, - document_type_id: Option, } enum UploadOutcome { @@ -402,7 +374,6 @@ pub async fn list_documents( query, tags, correspondents, - document_types, status, } = params; @@ -435,12 +406,6 @@ pub async fn list_documents( .map(|s| s.trim()) .filter(|s| !s.is_empty()) .map(|s| s.to_owned()); - let document_types_param = document_types - .as_ref() - .map(|s| s.trim()) - .filter(|s| !s.is_empty()) - .map(|s| s.to_owned()); - let include_descendants = include_descendants.unwrap_or(true); match (folder_id, include_descendants) { @@ -579,20 +544,6 @@ pub async fn list_documents( } } - if let Some(document_types_param) = document_types_param.as_ref() { - let type_ids: Result, _> = document_types_param - .split(',') - .map(|s| Uuid::parse_str(s.trim())) - .collect(); - - if let Ok(ids) = type_ids { - if !ids.is_empty() { - let filter_values: Vec> = ids.into_iter().map(Some).collect(); - docs_query = docs_query.filter(documents::document_type_id.eq_any(filter_values)); - } - } - } - if let Some(ref set) = filter_ids { if set.is_empty() { return Ok(Json(vec![])); @@ -735,8 +686,6 @@ pub async fn get_document( .find(doc.current_version_id) .first(&mut conn)?; - let doc_type = load_document_type(&mut conn, tenant_id, doc.document_type_id)?; - let tags_map = load_tags_for_documents(&mut conn, &[document_id])?; let mut correspondents_map = load_correspondents_for_documents(&mut conn, &[document_id])?; let version_id = current_version.id; @@ -753,7 +702,6 @@ pub async fn get_document( tags_map.get(&document_id).cloned(), correspondents_map.remove(&document_id).unwrap_or_default(), Some((version_response, assets)), - doc_type, )?, })) } @@ -786,7 +734,6 @@ pub async fn upload_document( let mut issued_at_override: Option = None; let mut skip_if_existing = false; let mut title_override: Option = None; - let mut document_type_id: Option = None; while let Some(field) = multipart.next_field().await.map_err(|err| { let msg = format!("invalid multipart data: {err}"); @@ -905,20 +852,6 @@ pub async fn upload_document( "1" | "true" | "yes" ); } - Some("document_type_id") => { - let value = field.text().await.map_err(|err| { - let msg = format!("invalid document_type_id: {err}"); - error!(error = %err, "invalid document_type payload"); - AppError::bad_request(msg) - })?; - let trimmed = value.trim(); - if !trimmed.is_empty() { - let parsed = Uuid::parse_str(trimmed).map_err(|_| { - AppError::bad_request("document_type_id must be a valid UUID") - })?; - document_type_id = Some(parsed); - } - } _ => {} } } @@ -949,7 +882,6 @@ pub async fn upload_document( correspondents, issued_at_override, skip_if_existing, - document_type_id, }; let outcome = match process_upload(&state, request, tenant_id, user_id).await { @@ -1482,46 +1414,6 @@ pub async fn update_document( } } - let doc_type_update = - classify_nullable(payload_obj.get("document_type_id")).map_err(AppError::bad_request)?; - - match doc_type_update { - NullableValue::Omitted => {} - NullableValue::Null => { - if document.document_type_id.is_some() { - changes.document_type_id = Some(None); - has_changes = true; - } - } - NullableValue::String(raw) => { - let trimmed = raw.trim(); - if trimmed.is_empty() { - return Err(AppError::bad_request( - "document_type_id must not be empty when provided", - )); - } - let parsed = Uuid::parse_str(trimmed) - .map_err(|_| AppError::bad_request("document_type_id must be a valid UUID"))?; - - if document.document_type_id != Some(parsed) { - let exists = document_types::table - .filter(document_types::tenant_id.eq(tenant_id)) - .find(parsed) - .first::(&mut conn) - .optional()?; - - if exists.is_none() { - return Err(AppError::bad_request( - "document_type_id does not exist for this tenant", - )); - } - - changes.document_type_id = Some(Some(parsed)); - has_changes = true; - } - } - } - if !has_changes { return Err(AppError::bad_request("no changes provided")); } @@ -1555,8 +1447,6 @@ pub async fn update_document( .find(document.current_version_id) .first(&mut conn)?; - let doc_type = load_document_type(&mut conn, tenant_id, document.document_type_id)?; - if title_changed { if let Err(err) = enqueue_job( &mut conn, @@ -1593,7 +1483,6 @@ pub async fn update_document( tags_map.get(&document_id).cloned(), correspondents_map.remove(&document_id).unwrap_or_default(), Some((version_response, assets)), - doc_type, )?, })) } @@ -2195,23 +2084,8 @@ async fn process_upload( correspondents, issued_at_override, skip_if_existing, - document_type_id, } = request; - if let Some(type_id) = document_type_id { - let mut conn = state.db_for_tenant(tenant_id)?; - let exists = document_types::table - .filter(document_types::tenant_id.eq(tenant_id)) - .find(type_id) - .first::(&mut conn) - .optional()?; - if exists.is_none() { - return Err(AppError::bad_request( - "document_type_id does not exist for this tenant", - )); - } - } - if let Some(folder) = folder_id { let mut conn = state.db_for_tenant(tenant_id)?; ensure_folder_exists_on_conn(&mut conn, tenant_id, folder)?; @@ -2292,24 +2166,6 @@ async fn process_upload( )?; } - if let Some(type_id) = document_type_id { - if document.document_type_id != Some(type_id) { - let now = Utc::now().naive_utc(); - diesel::update( - documents::table - .find(document.id) - .filter(documents::tenant_id.eq(tenant_id)), - ) - .set(( - documents::document_type_id.eq(Some(type_id)), - documents::updated_at.eq(now), - )) - .execute(&mut conn)?; - document.document_type_id = Some(type_id); - document.updated_at = now; - } - } - if document.deleted_at.is_some() { let now = Utc::now().naive_utc(); diesel::update(documents::table.find(document.id)) @@ -2322,7 +2178,6 @@ async fn process_upload( document.updated_at = now; } - let doc_type = load_document_type(&mut conn, tenant_id, document.document_type_id)?; let tags_map = load_tags_for_documents(&mut conn, &[document.id])?; let mut correspondents_map = load_correspondents_for_documents(&mut conn, &[document.id])?; @@ -2346,7 +2201,6 @@ async fn process_upload( tags, correspondents, Some((version_response, assets)), - doc_type, )?, })); } @@ -2386,7 +2240,6 @@ async fn process_upload( title: derived_title.clone(), metadata: metadata_value.clone(), tenant_id, - document_type_id, }; diesel::insert_into(documents::table) .values(&new_document) @@ -2445,7 +2298,6 @@ async fn process_upload( )?; } - let doc_type = load_document_type(&mut conn, tenant_id, document.document_type_id)?; let tags_map = load_tags_for_documents(&mut conn, &[doc_id])?; let mut correspondents_map = load_correspondents_for_documents(&mut conn, &[doc_id])?; let tags = tags_map.get(&doc_id).cloned(); @@ -2460,7 +2312,6 @@ async fn process_upload( tags, correspondents, Some((to_version_response(version.clone()), Vec::new())), - doc_type, )?, } }; @@ -2493,7 +2344,6 @@ pub(crate) fn to_document_response( tags: Option>, correspondents: Vec, current_version: Option<(DocumentVersionResponse, Vec)>, - doc_type: Option, ) -> AppResult { let current_version = if let Some((version, assets)) = current_version { let download_path = build_download_path(state, &doc, user_id)?; @@ -2518,11 +2368,6 @@ pub(crate) fn to_document_response( deleted_at: doc.deleted_at.map(to_iso), issued_at: doc.issued_at.map(to_iso), metadata: doc.metadata, - document_type_id: doc.document_type_id, - document_type: doc_type.map(|typ| DocumentTypeResponse { - id: typ.id, - name: typ.name, - }), tags: tags .unwrap_or_default() .into_iter() @@ -2533,22 +2378,6 @@ pub(crate) fn to_document_response( }) } -fn load_document_type( - conn: &mut PgConnection, - tenant_id: Uuid, - type_id: Option, -) -> diesel::QueryResult> { - if let Some(id) = type_id { - document_types::table - .filter(document_types::tenant_id.eq(tenant_id)) - .find(id) - .first::(conn) - .optional() - } else { - Ok(None) - } -} - pub(crate) fn hydrate_documents( state: &AppState, conn: &mut PgConnection, @@ -2560,20 +2389,6 @@ pub(crate) fn hydrate_documents( return Ok(Vec::new()); } - let type_ids: HashSet = docs.iter().filter_map(|doc| doc.document_type_id).collect(); - let doc_type_map: HashMap = if type_ids.is_empty() { - HashMap::new() - } else { - let ids: Vec = type_ids.iter().copied().collect(); - document_types::table - .filter(document_types::tenant_id.eq(tenant_id)) - .filter(document_types::id.eq_any(&ids)) - .load::(conn)? - .into_iter() - .map(|typ| (typ.id, typ)) - .collect() - }; - let doc_ids: Vec = docs.iter().map(|doc| doc.id).collect(); let tags_map = load_tags_for_documents(conn, &doc_ids)?; let mut correspondents_map = load_correspondents_for_documents(conn, &doc_ids)?; @@ -2584,9 +2399,6 @@ pub(crate) fn hydrate_documents( let tags = tags_map.get(&doc.id).cloned(); let correspondents = correspondents_map.remove(&doc.id).unwrap_or_default(); let current_version = primary_versions.get(&doc.id).cloned(); - let doc_type = doc - .document_type_id - .and_then(|id| doc_type_map.get(&id).cloned()); responses.push(to_document_response( state, user_id, @@ -2594,7 +2406,6 @@ pub(crate) fn hydrate_documents( tags, correspondents, current_version, - doc_type, )?); } @@ -2636,7 +2447,6 @@ pub(crate) fn hydrate_documents( crate::routes::documents::DocumentResponse, crate::routes::documents::DocumentDetailResponse, crate::routes::documents::DocumentMetadataUpdate, - crate::routes::documents::DocumentTypeResponse, crate::routes::documents::TagResponse, crate::routes::documents::CorrespondentAssignmentInput, crate::routes::documents::AssignCorrespondentsRequest, diff --git a/backend/src/routes/mod.rs b/backend/src/routes/mod.rs index 7f468cb..c5b163c 100644 --- a/backend/src/routes/mod.rs +++ b/backend/src/routes/mod.rs @@ -17,7 +17,6 @@ use crate::{auth::AuthenticatedUser, openapi::ApiDoc, state::AppState}; pub mod auth; pub mod correspondents; -pub mod document_types; pub mod documents; pub mod folders; pub mod health; @@ -133,17 +132,6 @@ pub fn create_router(state: AppState) -> Router<()> { .route("/", get(tags::list_tags).post(tags::create_tag)) .route("/:id", patch(tags::update_tag).delete(tags::delete_tag)); - let document_types_routes = Router::new() - .route( - "/", - get(document_types::list_document_types).post(document_types::create_document_type), - ) - .route( - "/:id", - patch(document_types::update_document_type) - .delete(document_types::delete_document_type), - ); - let correspondents_routes = Router::new() .route( "/", @@ -171,7 +159,6 @@ pub fn create_router(state: AppState) -> Router<()> { .nest("/api/documents", documents_routes) .nest("/api/folders", folders_routes) .nest("/api/tags", tags_routes) - .nest("/api/document-types", document_types_routes) .nest("/api/correspondents", correspondents_routes) .nest("/api/profile", profile_routes) .nest("/api/assets", assets_routes) diff --git a/backend/src/schema.rs b/backend/src/schema.rs index ffd0299..bcb4116 100644 --- a/backend/src/schema.rs +++ b/backend/src/schema.rs @@ -62,15 +62,6 @@ diesel::table! { } } -diesel::table! { - document_types (id) { - id -> Uuid, - tenant_id -> Uuid, - #[max_length = 100] - name -> Varchar, - } -} - diesel::table! { document_versions (id) { id -> Uuid, @@ -106,7 +97,6 @@ diesel::table! { title -> Varchar, current_version_id -> Uuid, tenant_id -> Uuid, - document_type_id -> Nullable, } } @@ -260,9 +250,7 @@ diesel::joinable!(document_tags -> documents (document_id)); diesel::joinable!(document_tags -> tags (tag_id)); diesel::joinable!(document_tags -> tenants (tenant_id)); diesel::joinable!(document_tags -> users (assigned_by)); -diesel::joinable!(document_types -> tenants (tenant_id)); diesel::joinable!(document_versions -> tenants (tenant_id)); -diesel::joinable!(documents -> document_types (document_type_id)); diesel::joinable!(documents -> folders (folder_id)); diesel::joinable!(documents -> tenants (tenant_id)); diesel::joinable!(folders -> tenants (tenant_id)); @@ -283,7 +271,6 @@ diesel::allow_tables_to_appear_in_same_query!( document_assets, document_correspondents, document_tags, - document_types, document_versions, documents, folders, diff --git a/backend/tests/common/mod.rs b/backend/tests/common/mod.rs index c2e6957..25ff4fc 100644 --- a/backend/tests/common/mod.rs +++ b/backend/tests/common/mod.rs @@ -538,7 +538,6 @@ impl TestApp { correspondents_json: None, issued_at: None, skip_existing: false, - document_type_id: None, }; self.upload_document_with_extras( path, @@ -618,13 +617,6 @@ impl TestApp { body.extend(b"\r\n"); } - if let Some(document_type_id) = extras.document_type_id { - body.extend(format!("--{boundary}\r\n").as_bytes()); - body.extend(b"Content-Disposition: form-data; name=\"document_type_id\"\r\n\r\n"); - body.extend(document_type_id.to_string().as_bytes()); - body.extend(b"\r\n"); - } - if extras.skip_existing { body.extend(format!("--{boundary}\r\n").as_bytes()); body.extend(b"Content-Disposition: form-data; name=\"skip_existing\"\r\n\r\ntrue\r\n"); @@ -674,7 +666,6 @@ pub struct UploadExtras<'a> { pub correspondents_json: Option<&'a str>, pub issued_at: Option<&'a str>, pub skip_existing: bool, - pub document_type_id: Option, } impl<'a> UploadExtras<'a> { @@ -686,7 +677,6 @@ impl<'a> UploadExtras<'a> { correspondents_json: None, issued_at: None, skip_existing: false, - document_type_id: None, } } } diff --git a/backend/tests/document_types.rs b/backend/tests/document_types.rs deleted file mode 100644 index 9080fac..0000000 --- a/backend/tests/document_types.rs +++ /dev/null @@ -1,127 +0,0 @@ -mod common; - -use anyhow::Result; -use axum::http::StatusCode; -use common::{acquire_db_lock, body_to_vec, TestApp}; -use serde_json::json; -use uuid::Uuid; - -#[derive(serde::Deserialize)] -struct DocumentTypeInfo { - id: Uuid, - name: String, -} - -#[derive(serde::Deserialize)] -struct ApiErrorResponse { - error: String, - #[serde(default)] - code: Option, -} - -#[tokio::test] -async fn document_type_crud_flow() -> Result<()> { - let _lock = acquire_db_lock().await; - let app = TestApp::new().await?; - - let password = "doctypecrud"; - let username = "doctype_admin"; - app.insert_user(username, password, "admin").await?; - let token = app.login_token(username, password).await?; - - let create_invoices = app - .post_json( - "/api/document-types", - &json!({"name": "Invoices"}), - Some(&token), - ) - .await?; - assert_eq!(create_invoices.status(), StatusCode::CREATED); - let invoices_body = body_to_vec(create_invoices.into_body()).await?; - let invoices: DocumentTypeInfo = serde_json::from_slice(&invoices_body)?; - let invoices_id = invoices.id; - - let create_receipts = app - .post_json( - "/api/document-types", - &json!({"name": "Receipts"}), - Some(&token), - ) - .await?; - assert_eq!(create_receipts.status(), StatusCode::CREATED); - let receipts_body = body_to_vec(create_receipts.into_body()).await?; - let receipts: DocumentTypeInfo = serde_json::from_slice(&receipts_body)?; - - let list_resp = app.get("/api/document-types", Some(&token)).await?; - assert!(list_resp.status().is_success()); - let list_body = body_to_vec(list_resp.into_body()).await?; - let mut all_types: Vec = serde_json::from_slice(&list_body)?; - all_types.sort_by(|a, b| a.name.cmp(&b.name)); - assert_eq!(all_types.len(), 2); - assert_eq!(all_types[0].name, "Invoices"); - assert_eq!(all_types[1].name, "Receipts"); - - let update_resp = app - .patch_json( - &format!("/api/document-types/{}", invoices_id), - &json!({"name": "Bills"}), - Some(&token), - ) - .await?; - assert_eq!(update_resp.status(), StatusCode::OK); - let update_body = body_to_vec(update_resp.into_body()).await?; - let updated: DocumentTypeInfo = serde_json::from_slice(&update_body)?; - assert_eq!(updated.id, invoices_id); - assert_eq!(updated.name, "Bills"); - - let conflict_resp = app - .patch_json( - &format!("/api/document-types/{}", receipts.id), - &json!({"name": "Bills"}), - Some(&token), - ) - .await?; - assert_eq!(conflict_resp.status(), StatusCode::CONFLICT); - let conflict_body = body_to_vec(conflict_resp.into_body()).await?; - let conflict_json: ApiErrorResponse = serde_json::from_slice(&conflict_body)?; - assert_eq!( - conflict_json.error, - "a document type with that name already exists" - ); - assert_eq!( - conflict_json.code.as_deref(), - Some("duplicate_document_type") - ); - - let delete_receipts = app - .delete( - &format!("/api/document-types/{}", receipts.id), - Some(&token), - ) - .await?; - assert_eq!(delete_receipts.status(), StatusCode::NO_CONTENT); - - let list_after_resp = app.get("/api/document-types", Some(&token)).await?; - assert!(list_after_resp.status().is_success()); - let list_after_body = body_to_vec(list_after_resp.into_body()).await?; - let list_after: Vec = serde_json::from_slice(&list_after_body)?; - assert_eq!(list_after.len(), 1); - assert_eq!(list_after[0].id, invoices_id); - assert_eq!(list_after[0].name, "Bills"); - - let delete_bills = app - .delete( - &format!("/api/document-types/{}", invoices_id), - Some(&token), - ) - .await?; - assert_eq!(delete_bills.status(), StatusCode::NO_CONTENT); - - let final_list_resp = app.get("/api/document-types", Some(&token)).await?; - let final_list_body = body_to_vec(final_list_resp.into_body()).await?; - let final_list: Vec = serde_json::from_slice(&final_list_body)?; - assert!(final_list.is_empty()); - - app.cleanup().await?; - Ok(()) -} diff --git a/backend/tests/documents_flow.rs b/backend/tests/documents_flow.rs index c48525a..41b659b 100644 --- a/backend/tests/documents_flow.rs +++ b/backend/tests/documents_flow.rs @@ -30,21 +30,11 @@ struct DocumentInfo { deleted_at: Option, issued_at: Option, metadata: Value, - #[serde(default)] - document_type_id: Option, - #[serde(default)] - document_type: Option, tags: Vec, #[serde(default)] current_version: Option, } -#[derive(Deserialize)] -struct DocumentTypeInfo { - id: Uuid, - name: String, -} - #[derive(Deserialize)] struct DocumentVersionPayload { id: Uuid, @@ -72,8 +62,6 @@ struct DocumentAssetInfo { struct DocumentListItem { id: Uuid, #[serde(default)] - document_type_id: Option, - #[serde(default)] current_version: Option, } @@ -186,7 +174,6 @@ async fn upload_and_list_document() -> Result<()> { assert_eq!(detail.document.title, "doc"); assert_eq!(detail.document.deleted_at, None); assert!(detail.document.issued_at.is_none()); - assert!(detail.document.document_type_id.is_none()); assert!(detail.document.tags.is_empty()); let current_version = detail .document @@ -420,7 +407,6 @@ async fn upload_skips_existing_when_requested() -> Result<()> { correspondents_json: None, issued_at: None, skip_existing: false, - document_type_id: None, }; let first_upload = app @@ -471,7 +457,6 @@ async fn upload_skips_existing_when_requested() -> Result<()> { correspondents_json: None, issued_at: None, skip_existing: true, - document_type_id: None, }; let skip_resp = app @@ -1453,298 +1438,6 @@ async fn list_documents_by_status_filter() -> Result<()> { Ok(()) } -#[tokio::test] -async fn document_upload_and_patch_document_type() -> Result<()> { - let _lock = acquire_db_lock().await; - let app = TestApp::new().await?; - - let password = "doctypeupload"; - let username = "doctype_user"; - app.insert_user(username, password, "admin").await?; - let token = app.login_token(username, password).await?; - - let invoices_resp = app - .post_json( - "/api/document-types", - &json!({"name": "Invoices"}), - Some(&token), - ) - .await?; - assert_eq!(invoices_resp.status(), StatusCode::CREATED); - let invoices_body = body_to_vec(invoices_resp.into_body()).await?; - let invoices: DocumentTypeInfo = serde_json::from_slice(&invoices_body)?; - - let receipts_resp = app - .post_json( - "/api/document-types", - &json!({"name": "Receipts"}), - Some(&token), - ) - .await?; - assert_eq!(receipts_resp.status(), StatusCode::CREATED); - let receipts_body = body_to_vec(receipts_resp.into_body()).await?; - let receipts: DocumentTypeInfo = serde_json::from_slice(&receipts_body)?; - - let upload_extras = UploadExtras { - title: Some("Invoice #1"), - metadata_json: None, - tag_ids_json: None, - correspondents_json: None, - issued_at: None, - skip_existing: false, - document_type_id: Some(invoices.id), - }; - - let upload_resp = app - .upload_document_with_extras( - "/api/documents", - "invoice.pdf", - "application/pdf", - b"invoice-bytes", - None, - upload_extras, - &token, - ) - .await?; - assert!(upload_resp.status().is_success()); - let upload_body = body_to_vec(upload_resp.into_body()).await?; - let mut detail: DocumentDetail = serde_json::from_slice(&upload_body)?; - assert_eq!(detail.document.document_type_id, Some(invoices.id)); - let doc_type_info = detail - .document - .document_type - .as_ref() - .expect("document type present"); - assert_eq!(doc_type_info.name, "Invoices"); - - let document_id = detail.document.id; - - let update_resp = app - .patch_json( - &format!("/api/documents/{}", document_id), - &json!({"document_type_id": receipts.id}), - Some(&token), - ) - .await?; - assert!(update_resp.status().is_success()); - let update_body = body_to_vec(update_resp.into_body()).await?; - detail = serde_json::from_slice(&update_body)?; - assert_eq!(detail.document.document_type_id, Some(receipts.id)); - assert_eq!( - detail - .document - .document_type - .as_ref() - .map(|info| info.name.as_str()), - Some("Receipts") - ); - - let clear_resp = app - .patch_json( - &format!("/api/documents/{}", document_id), - &json!({"document_type_id": null}), - Some(&token), - ) - .await?; - assert!(clear_resp.status().is_success()); - let clear_body = body_to_vec(clear_resp.into_body()).await?; - detail = serde_json::from_slice(&clear_body)?; - assert!(detail.document.document_type_id.is_none()); - assert!(detail.document.document_type.is_none()); - - let invalid_id = Uuid::new_v4(); - let invalid_resp = app - .patch_json( - &format!("/api/documents/{}", document_id), - &json!({"document_type_id": invalid_id}), - Some(&token), - ) - .await?; - assert_eq!(invalid_resp.status(), StatusCode::BAD_REQUEST); - let invalid_body = body_to_vec(invalid_resp.into_body()).await?; - let invalid_error: ApiErrorResponse = serde_json::from_slice(&invalid_body)?; - assert_eq!( - invalid_error.error, - "document_type_id does not exist for this tenant" - ); - - let final_detail_resp = app - .get(&format!("/api/documents/{}", document_id), Some(&token)) - .await?; - let final_detail_body = body_to_vec(final_detail_resp.into_body()).await?; - let final_detail: DocumentDetail = serde_json::from_slice(&final_detail_body)?; - assert!(final_detail.document.document_type_id.is_none()); - assert!(final_detail.document.document_type.is_none()); - - app.cleanup().await?; - Ok(()) -} - -#[tokio::test] -async fn list_documents_filtered_by_document_type() -> Result<()> { - let _lock = acquire_db_lock().await; - let app = TestApp::new().await?; - - let password = "doctypefilter"; - app.insert_user("doctypeuser", password, "admin").await?; - let token = app.login_token("doctypeuser", password).await?; - - let invoices_resp = app - .post_json( - "/api/document-types", - &json!({"name": "Invoices"}), - Some(&token), - ) - .await?; - assert_eq!(invoices_resp.status(), StatusCode::CREATED); - let invoices_body = body_to_vec(invoices_resp.into_body()).await?; - let invoices: DocumentTypeInfo = serde_json::from_slice(&invoices_body)?; - - let receipts_resp = app - .post_json( - "/api/document-types", - &json!({"name": "Receipts"}), - Some(&token), - ) - .await?; - assert_eq!(receipts_resp.status(), StatusCode::CREATED); - let receipts_body = body_to_vec(receipts_resp.into_body()).await?; - let receipts: DocumentTypeInfo = serde_json::from_slice(&receipts_body)?; - assert_eq!(invoices.name, "Invoices"); - assert_eq!(receipts.name, "Receipts"); - - let doc_a = app - .upload_document( - "/api/documents", - "invoice-a.pdf", - "application/pdf", - b"invoice-a", - None, - &token, - ) - .await?; - let doc_a_body = body_to_vec(doc_a.into_body()).await?; - let detail_a: DocumentDetail = serde_json::from_slice(&doc_a_body)?; - - let doc_b = app - .upload_document( - "/api/documents", - "receipt-b.pdf", - "application/pdf", - b"receipt-b", - None, - &token, - ) - .await?; - let doc_b_body = body_to_vec(doc_b.into_body()).await?; - let detail_b: DocumentDetail = serde_json::from_slice(&doc_b_body)?; - - let doc_c = app - .upload_document( - "/api/documents", - "notes.txt", - "text/plain", - b"notes", - None, - &token, - ) - .await?; - let doc_c_body = body_to_vec(doc_c.into_body()).await?; - let detail_c: DocumentDetail = serde_json::from_slice(&doc_c_body)?; - - let assign_a = app - .patch_json( - &format!("/api/documents/{}", detail_a.document.id), - &json!({"document_type_id": invoices.id}), - Some(&token), - ) - .await?; - assert_eq!(assign_a.status(), StatusCode::OK); - - let assign_b = app - .patch_json( - &format!("/api/documents/{}", detail_b.document.id), - &json!({"document_type_id": receipts.id}), - Some(&token), - ) - .await?; - assert_eq!(assign_b.status(), StatusCode::OK); - - let invoices_resp = app - .get( - &format!("/api/documents?document_types={}", invoices.id), - Some(&token), - ) - .await?; - assert!(invoices_resp.status().is_success()); - let invoices_list_body = body_to_vec(invoices_resp.into_body()).await?; - let invoices_docs: Vec = serde_json::from_slice(&invoices_list_body)?; - assert_eq!(invoices_docs.len(), 1); - assert_eq!(invoices_docs[0].id, detail_a.document.id); - assert_eq!(invoices_docs[0].document_type_id, Some(invoices.id)); - - let receipts_resp = app - .get( - &format!("/api/documents?document_types={}", receipts.id), - Some(&token), - ) - .await?; - assert!(receipts_resp.status().is_success()); - let receipts_list_body = body_to_vec(receipts_resp.into_body()).await?; - let receipts_docs: Vec = serde_json::from_slice(&receipts_list_body)?; - assert_eq!(receipts_docs.len(), 1); - assert_eq!(receipts_docs[0].id, detail_b.document.id); - assert_eq!(receipts_docs[0].document_type_id, Some(receipts.id)); - - let combined_resp = app - .get( - &format!( - "/api/documents?document_types={},{}", - invoices.id, receipts.id - ), - Some(&token), - ) - .await?; - assert!(combined_resp.status().is_success()); - let combined_body = body_to_vec(combined_resp.into_body()).await?; - let combined_docs: Vec = serde_json::from_slice(&combined_body)?; - let combined_ids: Vec = combined_docs.iter().map(|doc| doc.id).collect(); - assert!(combined_ids.contains(&detail_a.document.id)); - assert!(combined_ids.contains(&detail_b.document.id)); - assert!(!combined_ids.contains(&detail_c.document.id)); - - let refreshed_a = app - .get( - &format!("/api/documents/{}", detail_a.document.id), - Some(&token), - ) - .await?; - assert!(refreshed_a.status().is_success()); - let refreshed_a_body = body_to_vec(refreshed_a.into_body()).await?; - let refreshed_a_detail: DocumentDetail = serde_json::from_slice(&refreshed_a_body)?; - assert_eq!( - refreshed_a_detail.document.document_type_id, - Some(invoices.id) - ); - - let refreshed_b = app - .get( - &format!("/api/documents/{}", detail_b.document.id), - Some(&token), - ) - .await?; - assert!(refreshed_b.status().is_success()); - let refreshed_b_body = body_to_vec(refreshed_b.into_body()).await?; - let refreshed_b_detail: DocumentDetail = serde_json::from_slice(&refreshed_b_body)?; - assert_eq!( - refreshed_b_detail.document.document_type_id, - Some(receipts.id) - ); - - app.cleanup().await?; - Ok(()) -} - #[tokio::test] async fn restore_document_to_original_and_custom_folder() -> Result<()> { let _lock = acquire_db_lock().await; diff --git a/frontend/src/app/AppLayout.jsx b/frontend/src/app/AppLayout.jsx index c1496d1..67c243d 100644 --- a/frontend/src/app/AppLayout.jsx +++ b/frontend/src/app/AppLayout.jsx @@ -267,7 +267,6 @@ const AppLayout = () => { const previewInflightRef = useRef(new Map()); const [tags, setTags] = useState([]); const [correspondents, setCorrespondents] = useState([]); - const [documentTypes, setDocumentTypes] = useState([]); const [webdavTokens, setWebdavTokens] = useState([]); const [webdavTokensLoading, setWebdavTokensLoading] = useState(false); const [creatingWebdavToken, setCreatingWebdavToken] = useState(false); @@ -276,7 +275,6 @@ const AppLayout = () => { const [searchQuery, setSearchQuery] = useState(''); const [activeTagFilters, setActiveTagFilters] = useState([]); const [activeCorrespondentFilters, setActiveCorrespondentFilters] = useState([]); - const [activeDocumentTypeFilters, setActiveDocumentTypeFilters] = useState([]); const [searchLoading, setSearchLoading] = useState(false); const documentsRouteMatch = useMatch('/documents'); const documentsFolderRouteMatch = useMatch('/documents/folder/:folderId'); @@ -302,15 +300,6 @@ const AppLayout = () => { }); }, []); - const toggleDocumentTypeFilter = useCallback((documentTypeId) => { - setActiveDocumentTypeFilters((previous) => { - if (!documentTypeId) { - return []; - } - return previous.includes(documentTypeId) ? [] : [documentTypeId]; - }); - }, []); - const initialRefreshAttemptedRef = useRef(Boolean(token)); useEffect(() => { @@ -346,7 +335,6 @@ const AppLayout = () => { setSearchQuery(''); setActiveTagFilters([]); setActiveCorrespondentFilters([]); - setActiveDocumentTypeFilters([]); setSearchLoading(false); }, []); @@ -451,7 +439,6 @@ const AppLayout = () => { setSearchResults(null); setTags([]); setCorrespondents([]); - setDocumentTypes([]); setWebdavTokens([]); setWebdavTokensLoading(false); setCreatingWebdavToken(false); @@ -460,7 +447,6 @@ const AppLayout = () => { setSearchQuery(''); setActiveTagFilters([]); setActiveCorrespondentFilters([]); - setActiveDocumentTypeFilters([]); setDropOverlayState({ active: false, folderName: DEFAULT_FOLDER_NAME }); setActivePreviewId(null); setDetailPanelOpen(false); @@ -496,27 +482,6 @@ const AppLayout = () => { }); return map; }, [correspondents]); - - const documentTypeLookupByName = useMemo(() => { - const map = new Map(); - documentTypes.forEach((entry) => { - if (entry?.name) { - map.set(entry.name.toLowerCase(), entry); - } - }); - return map; - }, [documentTypes]); - - const documentTypeLookupById = useMemo(() => { - const map = new Map(); - documentTypes.forEach((entry) => { - if (entry?.id) { - map.set(entry.id, entry); - } - }); - return map; - }, [documentTypes]); - const updateSelectionOrder = useCallback((nextSelection, interactedKeys = []) => { const nextSet = new Set(nextSelection); const previousOrder = selectionOrderRef.current.filter((id) => nextSet.has(id)); @@ -652,9 +617,8 @@ const AppLayout = () => { () => searchQuery.trim().length > 0 || activeTagFilters.length > 0 || - activeCorrespondentFilters.length > 0 || - activeDocumentTypeFilters.length > 0, - [searchQuery, activeTagFilters, activeCorrespondentFilters, activeDocumentTypeFilters], + activeCorrespondentFilters.length > 0, + [searchQuery, activeTagFilters, activeCorrespondentFilters], ); const applySelectedFolder = useCallback( @@ -1479,22 +1443,6 @@ const AppLayout = () => { } }, [notifyApiError]); - const refreshDocumentTypes = useCallback(async () => { - const requestTenantId = tenantIdRef.current; - try { - const { data } = await api.get('/document-types'); - if (tenantIdRef.current !== requestTenantId) { - return; - } - setDocumentTypes(data || []); - } catch (error) { - if (tenantIdRef.current !== requestTenantId) { - return; - } - notifyApiError(error, 'Unable to load document types.'); - } - }, [notifyApiError]); - const refreshWebdavTokens = useCallback(async () => { if (!token) { return; @@ -1834,211 +1782,6 @@ const AppLayout = () => { ], ); - const handleDocumentTypeUpdate = useCallback( - async (documentTypeId, changes) => { - if (!documentTypeId) { - throw new Error('Missing document type identifier.'); - } - - const payload = {}; - if (typeof changes.name === 'string') { - const trimmed = changes.name.trim(); - if (!trimmed) { - throw new Error('Document type name cannot be empty.'); - } - payload.name = trimmed; - } - - if (Object.keys(payload).length === 0) { - return false; - } - - try { - await api.patch(`/document-types/${documentTypeId}`, payload); - await refreshDocumentTypes(); - setStatusMessage('Document type updated.', 'success'); - return true; - } catch (error) { - const message = error.response?.data?.error || 'Failed to update document type.'; - notifyApiError(error, message); - throw new Error(message); - } - }, - [refreshDocumentTypes, notifyApiError, setStatusMessage], - ); - - const handleDocumentTypeCreate = useCallback( - async ({ name }) => { - const trimmed = typeof name === 'string' ? name.trim() : ''; - if (!trimmed) { - throw new Error('Document type name is required.'); - } - try { - const { data } = await api.post('/document-types', { name: trimmed }); - await refreshDocumentTypes(); - setStatusMessage('Document type created.', 'success'); - return data; - } catch (error) { - const message = error.response?.data?.error || 'Failed to create document type.'; - notifyApiError(error, message); - throw new Error(message); - } - }, - [refreshDocumentTypes, notifyApiError, setStatusMessage], - ); - - const handleDocumentTypeDelete = useCallback( - async (documentTypeId) => { - if (!documentTypeId) { - throw new Error('Missing document type identifier.'); - } - - const deletedEntry = documentTypeLookupById.get(documentTypeId); - - try { - await api.delete(`/document-types/${documentTypeId}`); - - mapDocumentCaches((doc) => { - if (!doc) { - return doc; - } - - const existingType = doc.document_type; - const existingTypeId = doc.document_type_id ?? existingType?.id ?? null; - const existingTypeName = typeof existingType?.name === 'string' ? existingType.name : undefined; - - const shouldClear = - existingTypeId === documentTypeId || - (!!deletedEntry?.name && - existingTypeName && - existingTypeName.toLowerCase() === deletedEntry.name.toLowerCase()); - - if (!shouldClear) { - return doc; - } - - return { ...doc, document_type: null, document_type_id: null }; - }); - - await refreshDocumentTypes(); - setStatusMessage('Document type deleted.', 'success'); - return true; - } catch (error) { - const message = error.response?.data?.error || 'Failed to delete document type.'; - notifyApiError(error, message); - throw new Error(message); - } - }, - [ - refreshDocumentTypes, - notifyApiError, - setStatusMessage, - mapDocumentCaches, - documentTypeLookupById, - ], - ); - - const handleDocumentTypeAssign = useCallback( - async ({ documentId, documentTypeId, documentType }, { notify = true } = {}) => { - if (!documentId) { - throw new Error('Missing document identifier.'); - } - - try { - const payload = { document_type_id: documentTypeId ?? null }; - const { data } = await api.patch(`/documents/${documentId}`, payload); - const updatedDocument = extractDocumentFromResponse(data); - - updateDocumentCaches(documentId, (doc) => { - if (updatedDocument) { - return { ...doc, ...updatedDocument }; - } - - const next = { ...doc }; - if (documentTypeId) { - const entry = - documentTypeLookupById.get(documentTypeId) || - (documentType?.name ? documentType : null); - next.document_type = entry - ? { id: entry.id ?? documentTypeId, name: entry.name } - : { id: documentTypeId, name: '' }; - } else { - next.document_type = null; - } - next.document_type_id = documentTypeId ?? null; - return next; - }); - - if (notify) { - setStatusMessage( - documentTypeId ? 'Document type updated.' : 'Document type cleared.', - 'success', - ); - } - return true; - } catch (error) { - const message = error.response?.data?.error || 'Failed to update document type.'; - notifyApiError(error, message); - return false; - } - }, - [ - extractDocumentFromResponse, - updateDocumentCaches, - documentTypeLookupById, - notifyApiError, - setStatusMessage, - ], - ); - - const handleDocumentTypeClear = useCallback( - async ({ documentId }, options = {}) => - handleDocumentTypeAssign({ documentId, documentTypeId: null }, options), - [handleDocumentTypeAssign], - ); - - const handleDocumentTypeSet = useCallback( - async ({ document, name, input }) => { - if (!document?.id) { - throw new Error('Missing document for document type assignment.'); - } - const trimmed = typeof name === 'string' ? name.trim() : ''; - if (!trimmed) { - setStatusMessage('Document type name is required.', 'error'); - return; - } - - let target = documentTypeLookupByName.get(trimmed.toLowerCase()) || null; - if (!target) { - try { - target = await handleDocumentTypeCreate({ name: trimmed }); - } catch (error) { - return; - } - } - - if (!target?.id) { - setStatusMessage('Unable to resolve document type.', 'error'); - return; - } - - const success = await handleDocumentTypeAssign( - { documentId: document.id, documentTypeId: target.id, documentType: target }, - { notify: true }, - ); - - if (success && input) { - input.value = ''; - } - }, - [ - handleDocumentTypeCreate, - handleDocumentTypeAssign, - documentTypeLookupByName, - setStatusMessage, - ], - ); - const resolveTargetDocumentIds = useCallback( (candidateIds) => { const normalized = Array.isArray(candidateIds) @@ -2052,90 +1795,6 @@ const AppLayout = () => { [selectedDocumentIds], ); - const handleBulkDocumentTypeSet = useCallback( - async ({ name, input, documentIds }) => { - const trimmed = typeof name === 'string' ? name.trim() : ''; - if (!trimmed) { - setStatusMessage('Document type name is required.', 'error'); - return; - } - - const targets = resolveTargetDocumentIds(documentIds); - if (!targets.length) { - setStatusMessage('Select documents before assigning a document type.', 'error'); - return; - } - - let target = documentTypeLookupByName.get(trimmed.toLowerCase()) || null; - if (!target) { - try { - target = await handleDocumentTypeCreate({ name: trimmed }); - } catch (error) { - return; - } - } - - if (!target?.id) { - setStatusMessage('Unable to resolve document type.', 'error'); - return; - } - - try { - await Promise.all( - targets.map((documentId) => - api.patch(`/documents/${documentId}`, { document_type_id: target.id }), - ), - ); - await refreshCurrentFolder(); - const suffix = targets.length === 1 ? '' : 's'; - setStatusMessage( - `Document type assigned to ${targets.length} document${suffix}.`, - 'success', - ); - if (input) { - input.value = ''; - } - } catch (error) { - const message = error.response?.data?.error || 'Failed to assign document type.'; - notifyApiError(error, message); - } - }, - [ - documentTypeLookupByName, - handleDocumentTypeCreate, - refreshCurrentFolder, - resolveTargetDocumentIds, - setStatusMessage, - notifyApiError, - ], - ); - - const handleBulkDocumentTypeClear = useCallback( - async ({ documentIds }) => { - const targets = resolveTargetDocumentIds(documentIds); - if (!targets.length) { - setStatusMessage('Select documents before clearing the document type.', 'error'); - return; - } - - try { - await Promise.all( - targets.map((documentId) => api.patch(`/documents/${documentId}`, { document_type_id: null })), - ); - await refreshCurrentFolder(); - const suffix = targets.length === 1 ? '' : 's'; - setStatusMessage( - `Document type cleared from ${targets.length} document${suffix}.`, - 'success', - ); - } catch (error) { - const message = error.response?.data?.error || 'Failed to clear document type.'; - notifyApiError(error, message); - } - }, - [resolveTargetDocumentIds, refreshCurrentFolder, setStatusMessage, notifyApiError], - ); - const handleTagDelete = useCallback( async (tagId) => { if (!tagId) { @@ -2336,7 +1995,7 @@ const AppLayout = () => { const initializeAfterLogin = useCallback(async () => { setLoading(true); try { - await Promise.all([refreshTags(), refreshCorrespondents(), refreshDocumentTypes()]); + await Promise.all([refreshTags(), refreshCorrespondents()]); const initialFolder = routeFolderId && routeFolderId !== 'root' ? routeFolderId : 'root'; await loadFolder(initialFolder, { showLoading: false }); } catch (error) { @@ -2345,7 +2004,7 @@ const AppLayout = () => { } finally { setLoading(false); } - }, [refreshTags, refreshCorrespondents, refreshDocumentTypes, routeFolderId, loadFolder, notifyApiError]); + }, [refreshTags, refreshCorrespondents, routeFolderId, loadFolder, notifyApiError]); useEffect(() => { if (!token) { @@ -4038,12 +3697,7 @@ const AppLayout = () => { } }, [creatingFolder, handleFolderCreate, setStatusMessage]); - const { - managementModals, - openTagsModal, - openCorrespondentsModal, - openDocumentTypesModal, - } = useManagementModals({ + const { managementModals, openTagsModal, openCorrespondentsModal } = useManagementModals({ locationPathname: location.pathname, tags, refreshTags, @@ -4055,11 +3709,6 @@ const AppLayout = () => { onCorrespondentCreate: handleCorrespondentCreate, onCorrespondentUpdate: handleCorrespondentUpdate, onCorrespondentDelete: handleCorrespondentDelete, - documentTypes, - refreshDocumentTypes, - onDocumentTypeCreate: handleDocumentTypeCreate, - onDocumentTypeUpdate: handleDocumentTypeUpdate, - onDocumentTypeDelete: handleDocumentTypeDelete, setStatusMessage, }); @@ -4095,9 +3744,6 @@ const AppLayout = () => { if (activeCorrespondentFilters.length) { params.correspondents = activeCorrespondentFilters.join(','); } - if (activeDocumentTypeFilters.length) { - params.document_types = activeDocumentTypeFilters.join(','); - } const folderIdentifier = selectedFolder === 'root' ? null : selectedFolder; if (folderIdentifier) { params.folder_id = folderIdentifier; @@ -4179,7 +3825,6 @@ const AppLayout = () => { searchQuery, activeTagFilters, activeCorrespondentFilters, - activeDocumentTypeFilters, selectedFolder, notifyApiError, assetManager, @@ -4981,7 +4626,7 @@ const AppLayout = () => { setWorkspaceMode('table'); navigate('/documents', { replace: true }); - await Promise.all([refreshTags(), refreshCorrespondents(), refreshDocumentTypes()]); + await Promise.all([refreshTags(), refreshCorrespondents()]); await loadFolder('root', { showLoading: false, preserveSearch: false }); const tenantLabel = data?.tenant?.name || data?.tenant?.id || 'tenant'; @@ -5002,7 +4647,6 @@ const AppLayout = () => { navigate, refreshTags, refreshCorrespondents, - refreshDocumentTypes, loadFolder, ], ); @@ -5128,10 +4772,6 @@ const AppLayout = () => { activeCorrespondentIds: activeCorrespondentFilters, onToggleCorrespondentFilter: toggleCorrespondentFilter, onCreateCorrespondent: (name) => handleCorrespondentCreate({ name }), - documentTypes, - activeDocumentTypeIds: activeDocumentTypeFilters, - onToggleDocumentTypeFilter: toggleDocumentTypeFilter, - onCreateDocumentType: (name) => handleDocumentTypeCreate({ name }), appStatus, loading, previewActive, @@ -5155,7 +4795,6 @@ const AppLayout = () => { clearFilters, correspondents, currentTenantId, - documentTypes, folderClickHandlers, folderNodes, handleFolderDelete, @@ -5181,13 +4820,10 @@ const AppLayout = () => { toggleTagFilter, handleTagCreate, handleCorrespondentCreate, - toggleDocumentTypeFilter, - handleDocumentTypeCreate, handlePromptCreateFolder, creatingFolder, handleNeutralHueChange, neutralHue, - activeDocumentTypeFilters, ], ); @@ -5222,11 +4858,6 @@ const AppLayout = () => { correspondents, onCorrespondentAdd: handleCorrespondentAdd, onCorrespondentRemove: handleCorrespondentRemove, - documentTypes, - onDocumentTypeSet: handleDocumentTypeSet, - onDocumentTypeClear: handleDocumentTypeClear, - onBulkDocumentTypeSet: handleBulkDocumentTypeSet, - onBulkDocumentTypeClear: handleBulkDocumentTypeClear, resolveApiPath, onFolderNavigate: selectFolder, onClose: handleDetailPanelClose, @@ -5241,15 +4872,11 @@ const AppLayout = () => { getDocumentAsset, handleBulkCorrespondentAdd, handleBulkCorrespondentRemove, - handleBulkDocumentTypeSet, - handleBulkDocumentTypeClear, handleBulkSelectionReanalyze, handleBulkTagAddFromDetail, handleBulkTagRemoveFromDetail, handleCorrespondentAdd, handleCorrespondentRemove, - handleDocumentTypeSet, - handleDocumentTypeClear, handleDetailPanelClose, handleDocumentTitleUpdate, handleTagAdd, @@ -5262,7 +4889,6 @@ const AppLayout = () => { selectedPreviewEntry, tags, tagLookupById, - documentTypes, ], ); @@ -5321,16 +4947,6 @@ const AppLayout = () => { handleDocumentCorrespondentAttach, handleCorrespondentRemove, handleCorrespondentAdd, - documentTypes, - refreshDocumentTypes, - handleDocumentTypeUpdate, - handleDocumentTypeCreate, - handleDocumentTypeDelete, - handleDocumentTypeAssign, - handleDocumentTypeClear, - handleDocumentTypeSet, - handleBulkDocumentTypeSet, - handleBulkDocumentTypeClear, webdavTokens, webdavTokensLoading, creatingWebdavToken, @@ -5366,7 +4982,6 @@ const AppLayout = () => { notifyApiError, openTagsModal, openCorrespondentsModal, - openDocumentTypesModal, openSettings, detailPanelOpen, setDetailPanelOpen, @@ -5392,16 +5007,6 @@ const AppLayout = () => { handleDocumentCorrespondentAttach, handleCorrespondentRemove, handleCorrespondentAdd, - documentTypes, - refreshDocumentTypes, - handleDocumentTypeUpdate, - handleDocumentTypeCreate, - handleDocumentTypeDelete, - handleDocumentTypeAssign, - handleDocumentTypeClear, - handleDocumentTypeSet, - handleBulkDocumentTypeSet, - handleBulkDocumentTypeClear, webdavTokens, webdavTokensLoading, creatingWebdavToken, @@ -5436,7 +5041,6 @@ const AppLayout = () => { notifyApiError, openTagsModal, openCorrespondentsModal, - openDocumentTypesModal, openSettings, detailPanelOpen, setDetailPanelOpen, diff --git a/frontend/src/app/DocumentsRoute.jsx b/frontend/src/app/DocumentsRoute.jsx index 2435085..0a66381 100644 --- a/frontend/src/app/DocumentsRoute.jsx +++ b/frontend/src/app/DocumentsRoute.jsx @@ -14,7 +14,6 @@ const DocumentsRoute = () => { skeuoWorkspaceProps, openTagsModal, openCorrespondentsModal, - openDocumentTypesModal, previewWorkspaceDocument, previewWorkspaceEntry, closeDocumentPreview, @@ -36,10 +35,9 @@ const DocumentsRoute = () => { ...sidebarProps, onManageTags: openTagsModal, onManageCorrespondents: openCorrespondentsModal, - onManageDocumentTypes: openDocumentTypesModal, onCollapse: collapseSidebar, }), - [sidebarProps, openTagsModal, openCorrespondentsModal, openDocumentTypesModal, collapseSidebar], + [sidebarProps, openTagsModal, openCorrespondentsModal, collapseSidebar], ); const breadcrumbs = documentsTableProps?.breadcrumbs || null; diff --git a/frontend/src/app/useManagementModals.js b/frontend/src/app/useManagementModals.js index e9e7650..f433ee0 100644 --- a/frontend/src/app/useManagementModals.js +++ b/frontend/src/app/useManagementModals.js @@ -1,11 +1,9 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import TagsPanel from '../tags/TagsPanel'; import CorrespondentsPanel from '../correspondents/CorrespondentsPanel'; -import DocumentTypesPanel from '../documentTypes/DocumentTypesPanel'; const TAGS_MODAL = 'tags'; const CORRESPONDENTS_MODAL = 'correspondents'; -const DOCUMENT_TYPES_MODAL = 'document-types'; export const useManagementModals = ({ locationPathname, @@ -19,11 +17,6 @@ export const useManagementModals = ({ onCorrespondentCreate, onCorrespondentUpdate, onCorrespondentDelete, - documentTypes, - refreshDocumentTypes, - onDocumentTypeCreate, - onDocumentTypeUpdate, - onDocumentTypeDelete, setStatusMessage, }) => { const [activeModal, setActiveModal] = useState(null); @@ -33,10 +26,6 @@ export const useManagementModals = ({ () => setActiveModal(CORRESPONDENTS_MODAL), [], ); - const openDocumentTypesModal = useCallback( - () => setActiveModal(DOCUMENT_TYPES_MODAL), - [], - ); const closeActiveModal = useCallback(() => setActiveModal(null), []); useEffect(() => { @@ -151,58 +140,10 @@ export const useManagementModals = ({ setStatusMessage, ]); - const documentTypesModal = useMemo(() => { - if (activeModal !== DOCUMENT_TYPES_MODAL) { - return null; - } - return ( -
-
event.stopPropagation()} - > -
-

Manage Document Types

- -
-
- -
-
-
- ); - }, [ - activeModal, - closeActiveModal, - documentTypes, - onDocumentTypeCreate, - onDocumentTypeDelete, - onDocumentTypeUpdate, - refreshDocumentTypes, - setStatusMessage, - ]); - const managementModals = ( <> {tagModal} {correspondentsModal} - {documentTypesModal} ); @@ -210,7 +151,6 @@ export const useManagementModals = ({ managementModals, openTagsModal, openCorrespondentsModal, - openDocumentTypesModal, closeActiveModal, activeModal, }; diff --git a/frontend/src/detail/DetailPanel.jsx b/frontend/src/detail/DetailPanel.jsx index 88c7e9f..00c39cd 100644 --- a/frontend/src/detail/DetailPanel.jsx +++ b/frontend/src/detail/DetailPanel.jsx @@ -34,25 +34,6 @@ const sortCorrespondents = (entries = []) => .map(({ id, name, count }) => ({ id, name, count })) .sort((a, b) => a.name.localeCompare(b.name)); -const resolveDocumentType = (doc) => { - if (!doc) { - return null; - } - const type = doc.document_type; - const fallbackId = doc.document_type_id ?? null; - if (!type) { - return null; - } - if (typeof type === 'object' && typeof type.name === 'string') { - const name = type.name.trim(); - if (!name) { - return null; - } - return { id: type.id ?? fallbackId, name }; - } - return null; -}; - const CorrespondentPills = ({ entries = [], onRemove, showCount = false }) => (
{entries.length ? ( @@ -318,11 +299,6 @@ const DetailPanel = ({ correspondents = [], onCorrespondentAdd, onCorrespondentRemove, - documentTypes = [], - onDocumentTypeSet, - onDocumentTypeClear, - onBulkDocumentTypeSet, - onBulkDocumentTypeClear, resolveApiPath, onFolderNavigate = null, resolveFolderPath = null, @@ -617,28 +593,6 @@ const DetailPanel = ({ }, []); }, [availableCorrespondents]); - const documentTypeOptions = useMemo(() => { - const seen = new Set(); - return (documentTypes || []).reduce((options, entry) => { - if (typeof entry?.name !== 'string') { - return options; - } - const name = entry.name.trim(); - if (!name) { - return options; - } - const lower = name.toLowerCase(); - if (seen.has(lower)) { - return options; - } - seen.add(lower); - options.push(name); - return options; - }, []); - }, [documentTypes]); - - const singleDocumentType = useMemo(() => resolveDocumentType(singleDoc), [singleDoc]); - const singleCorrespondents = useMemo(() => { if (!singleDoc) return []; return sortCorrespondents(singleDoc.correspondents || []); @@ -774,42 +728,6 @@ const DetailPanel = ({ .sort((a, b) => a.name.localeCompare(b.name)); }, [selectedDocuments]); - const bulkDocumentTypes = useMemo(() => { - if (!selectedDocuments.length) { - return []; - } - - const map = new Map(); - selectedDocuments.forEach((doc) => { - const type = resolveDocumentType(doc); - if (!type) return; - const key = type.id ?? type.name.toLowerCase(); - if (!map.has(key)) { - map.set(key, { id: type.id ?? null, name: type.name, count: 0 }); - } - map.get(key).count += 1; - }); - - return [...map.values()].sort((a, b) => a.name.localeCompare(b.name)); - }, [selectedDocuments]); - - const bulkDocumentTypeSummary = useMemo(() => { - if (!bulkDocumentTypes.length) { - return ''; - } - - return bulkDocumentTypes - .map((entry) => { - if (!entry?.name) { - return null; - } - const suffix = entry.count === selectedDocuments.length ? '' : ` (${entry.count})`; - return `${entry.name}${suffix}`; - }) - .filter(Boolean) - .join(', '); - }, [bulkDocumentTypes, selectedDocuments.length]); - const handleBulkCorrespondentRemove = useCallback( (entry) => { if (!entry?.id) return; @@ -1177,53 +1095,6 @@ const DetailPanel = ({ ); })}
-
-
Document type
-
- {singleDocumentType?.name ? ( - {singleDocumentType.name} - ) : ( - None assigned. - )} - {singleDocumentType && onDocumentTypeClear ? ( - - ) : null} -
- {onDocumentTypeSet ? ( -
{ - event.preventDefault(); - const form = event.currentTarget; - const input = form.elements.documentType; - const value = input?.value?.trim(); - if (!value) { - return; - } - onDocumentTypeSet?.({ document: singleDoc, name: value, input }); - }} - > - - - - {documentTypeOptions.map((name) => ( - -
- ) : null} -
({ @@ -1348,47 +1219,6 @@ const DetailPanel = ({ datalistOptions={tags} className="bulk-tags" /> -
-
Document type
-
- {bulkDocumentTypeSummary || 'None assigned.'} - {onBulkDocumentTypeClear && bulkDocumentTypes.length > 0 ? ( - - ) : null} -
- {onBulkDocumentTypeSet ? ( -
{ - event.preventDefault(); - const form = event.currentTarget; - const input = form.elements.documentType; - const value = input?.value?.trim(); - if (!value) { - return; - } - onBulkDocumentTypeSet?.({ name: value, input, documentIds }); - }} - > - - - - {documentTypeOptions.map((name) => ( -
- ) : null} -
{ - setEditingId(entry.id); - setDraftName(entry.name); - }, []); - - const cancelEdit = useCallback(() => { - setEditingId(null); - setDraftName(''); - setSaving(false); - }, []); - - const handleSave = useCallback(async () => { - if (!editingId) return; - const trimmed = draftName.trim(); - if (!trimmed) { - onNotify?.('Document type name cannot be empty.', 'error'); - return; - } - - setSaving(true); - try { - await onUpdate(editingId, { name: trimmed }); - cancelEdit(); - } catch (error) { - onNotify?.('Failed to update document type.', 'error'); - console.error('[document-types] update failed', error); - setSaving(false); - } - }, [editingId, draftName, onUpdate, cancelEdit, onNotify]); - - const handleDelete = useCallback( - async (entry) => { - if (!entry?.id) return; - setDeletingId(entry.id); - try { - await onDelete(entry.id); - if (editingId === entry.id) { - cancelEdit(); - } - } catch (error) { - onNotify?.('Failed to delete document type.', 'error'); - console.error('[document-types] delete failed', error); - } finally { - setDeletingId(null); - } - }, - [onDelete, editingId, cancelEdit, onNotify], - ); - - const handleCreate = useCallback( - async (event) => { - event.preventDefault(); - const trimmed = createName.trim(); - if (!trimmed) { - onNotify?.('Document type name cannot be empty.', 'error'); - return; - } - setCreating(true); - try { - await onCreate({ name: trimmed }); - setCreateName(''); - } catch (error) { - onNotify?.('Failed to create document type.', 'error'); - console.error('[document-types] create failed', error); - } finally { - setCreating(false); - } - }, - [createName, onCreate, onNotify], - ); - - const handleKeyDown = useCallback( - (event) => { - if (event.key === 'Enter') { - event.preventDefault(); - handleSave(); - } else if (event.key === 'Escape') { - event.preventDefault(); - cancelEdit(); - } - }, - [handleSave, cancelEdit], - ); - - const renderUsage = useCallback((usage) => { - if (!usage) { - return '0'; - } - const total = typeof usage.total === 'number' ? usage.total : 0; - return total.toString(); - }, []); - - return ( -
-
-
-

Document Types

-
{documentTypes.length} total
-
-
-
- setCreateName(event.target.value)} - disabled={creating} - /> - -
- -
-
-
- {documentTypes.length === 0 ? ( -
No document types created yet.
- ) : ( -
- - - - - - - - - - {documentTypes.map((entry) => { - const isEditing = editingId === entry.id; - return ( - - - - - - ); - })} - -
Name - Usage - - Actions -
- {isEditing ? ( - setDraftName(event.target.value)} - onKeyDown={handleKeyDown} - disabled={saving} - autoFocus - /> - ) : ( - {entry.name} - )} - {renderUsage(entry.usage)} - {isEditing ? ( -
- - -
- ) : ( -
- - -
- )} -
-
- )} -
-
- ); -} - -export default DocumentTypesPanel; diff --git a/frontend/src/documents/DocumentsTable.jsx b/frontend/src/documents/DocumentsTable.jsx index 7743b2f..7e5ac65 100644 --- a/frontend/src/documents/DocumentsTable.jsx +++ b/frontend/src/documents/DocumentsTable.jsx @@ -56,29 +56,6 @@ const resolveCorrespondents = (doc) => { return results; }; -const resolveDocumentType = (doc) => { - if (!doc) { - return null; - } - - const type = doc.document_type; - const fallbackId = doc.document_type_id ?? null; - - if (!type) { - return null; - } - - if (typeof type === 'object' && typeof type.name === 'string') { - const name = type.name.trim(); - if (!name) { - return null; - } - return { id: type.id ?? fallbackId, name }; - } - - return null; -}; - // Detects when an element becomes visible within a scroll container. const useLazyVisibility = (rootRef, resetKey) => { const targetRef = useRef(null); @@ -254,8 +231,6 @@ const DocumentsTable = ({ getDownloadHref, onTagClick, onCorrespondentClick, - activeDocumentTypeIds = [], - onDocumentTypeClick, isSearchLoading = false, onDocumentTagDrop, viewMode = 'list', @@ -282,10 +257,6 @@ const DocumentsTable = ({ () => new Set(activeCorrespondentIds || []), [activeCorrespondentIds], ); - const activeDocumentTypeIdSet = useMemo( - () => new Set(activeDocumentTypeIds || []), - [activeDocumentTypeIds], - ); const scrollRef = useRef(null); const suppressDocumentClickRef = useRef(false); const [, forceVisibilityTick] = useState(0); @@ -657,43 +628,10 @@ const DocumentsTable = ({ const visibleTags = tagList.slice(0, 3); const remainingTagCount = tagList.length > 3 ? tagList.length - 3 : 0; const correspondents = resolveCorrespondents(doc); - const documentType = resolveDocumentType(doc); - const isDocumentTypeActive = - documentType?.id != null && activeDocumentTypeIdSet.has(documentType.id); - const canToggleDocumentType = - documentType?.id != null && typeof onDocumentTypeClick === 'function'; const cardClasses = ['document-card', 'document']; if (isSelected) cardClasses.push('selected'); if (isDraggingDoc) cardClasses.push('is-dragging'); const titleText = doc.title || doc.original_name; - const documentTypeNode = documentType - ? ( - { - event.stopPropagation(); - if (canToggleDocumentType) { - onDocumentTypeClick?.(documentType.id, documentType); - } - }} - onKeyDown={(event) => { - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault(); - event.stopPropagation(); - if (canToggleDocumentType) { - onDocumentTypeClick?.(documentType.id, documentType); - } - } - }} - > - ({documentType.name}) - - ) - : null; return (
) : null} {titleText} - {documentTypeNode ? ( - <> - {' '} - {documentTypeNode} - - ) : null}
{visibleTags.length > 0 && (
@@ -924,38 +856,7 @@ const DocumentsTable = ({ if (isDraggingDoc) rowClasses.push('is-dragging'); const downloadHref = getDownloadHref?.(doc) || null; const correspondents = resolveCorrespondents(doc); - const documentType = resolveDocumentType(doc); - const isRowDocumentTypeActive = documentType?.id != null && activeDocumentTypeIdSet.has(documentType.id); - const canToggleRowDocumentType = documentType?.id != null && typeof onDocumentTypeClick === 'function'; const titleText = doc.title || doc.original_name; - const documentTypeNode = documentType - ? ( - { - event.stopPropagation(); - if (canToggleRowDocumentType) { - onDocumentTypeClick?.(documentType.id, documentType); - } - }} - onKeyDown={(event) => { - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault(); - event.stopPropagation(); - if (canToggleRowDocumentType) { - onDocumentTypeClick?.(documentType.id, documentType); - } - } - }} - > - ({documentType.name}) - - ) - : null; return ( ) : null} {titleText} - {documentTypeNode ? ( - <> - {' '} - {documentTypeNode} - - ) : null}
{(doc.tags || []).length > 0 && ( diff --git a/frontend/src/preview/PreviewWorkspace.jsx b/frontend/src/preview/PreviewWorkspace.jsx index cdf59ee..ab8e74a 100644 --- a/frontend/src/preview/PreviewWorkspace.jsx +++ b/frontend/src/preview/PreviewWorkspace.jsx @@ -3,8 +3,6 @@ import { DownloadIcon, TextScanIcon, AnalyzeIcon, CloseIcon } from '../ui/icons' import { describeDocumentSummary } from '../documents/documentSummary'; import { createDocumentActionState } from '../documents/documentActions'; -const resolveDocumentTypeName = (doc) => doc?.document_type?.name; - const PreviewWorkspace = ({ document, previewEntry, @@ -21,7 +19,6 @@ const PreviewWorkspace = ({ const tags = Array.isArray(document.tags) ? document.tags.map((tag) => tag?.label).filter(Boolean).join(', ') : ''; - const documentTypeName = resolveDocumentTypeName(document); const formatDateTime = (value) => { if (!value) { @@ -36,7 +33,6 @@ const PreviewWorkspace = ({ { label: 'Archive Reference', value: document.archive_serial || '—' }, { label: 'Issued On', value: formatDateTime(document.issued_at) }, { label: 'Correspondent', value: correspondents || '—' }, - { label: 'Document Type', value: documentTypeName || '—' }, { label: 'Filename', value: document.archive_path || document.filename || '—', diff --git a/frontend/src/sidebar/Sidebar.jsx b/frontend/src/sidebar/Sidebar.jsx index d113802..682ec60 100644 --- a/frontend/src/sidebar/Sidebar.jsx +++ b/frontend/src/sidebar/Sidebar.jsx @@ -151,15 +151,10 @@ const Sidebar = ({ correspondents = [], activeCorrespondentIds = [], onToggleCorrespondentFilter, - documentTypes = [], - activeDocumentTypeIds = [], - onToggleDocumentTypeFilter, onManageTags, onManageCorrespondents, - onManageDocumentTypes, onCreateTag, onCreateCorrespondent, - onCreateDocumentType, searchQuery = '', onSearchChange, onSearchSubmit, @@ -187,24 +182,10 @@ const Sidebar = ({ () => new Set(activeCorrespondentIds || []), [activeCorrespondentIds], ); - const sortedDocumentTypes = useMemo(() => { - if (!Array.isArray(documentTypes)) { - return []; - } - return documentTypes - .filter((entry) => entry?.name) - .slice() - .sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })); - }, [documentTypes]); - const activeDocumentTypeSet = useMemo( - () => new Set(activeDocumentTypeIds || []), - [activeDocumentTypeIds], - ); const handleToggleTag = onToggleTagFilter || (() => {}); const activeTagSet = new Set(activeTagIds); const handleManageTags = onManageTags || (() => {}); const handleManageCorrespondents = onManageCorrespondents || (() => {}); - const handleManageDocumentTypes = onManageDocumentTypes || (() => {}); const handleCreateTag = useCallback(async () => { const input = window.prompt('New tag name'); if (!input) { @@ -237,22 +218,6 @@ const Sidebar = ({ } }, [onCreateCorrespondent]); - const handleCreateDocumentType = useCallback(async () => { - const input = window.prompt('New document type name'); - if (!input) { - return; - } - const trimmed = input.trim(); - if (!trimmed) { - return; - } - try { - await onCreateDocumentType?.(trimmed); - } catch (error) { - console.error('[sidebar] failed to create document type', error); - } - }, [onCreateDocumentType]); - const handleCreateFolder = useCallback(() => { if (creatingFolder) { return; @@ -629,58 +594,6 @@ const Sidebar = ({ })} -
-
-

Document Types

-
- - - {documentTypes.length} -
-
-
    - {sortedDocumentTypes.map((entry) => { - const isActive = activeDocumentTypeSet.has(entry.id); - const className = `sidebar-correspondent-item${isActive ? ' active' : ''}`; - const label = entry.name || 'Untitled'; - const handleSelect = () => { - const nextId = isActive ? null : entry.id; - onToggleDocumentTypeFilter?.(nextId); - }; - return ( -
  • - { - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault(); - handleSelect(); - } - }} - > - {label} - -
  • - ); - })} -
-
{typeof neutralHue === 'number' || typeof neutralHue === 'string' ? (
diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 2aa4e88..7d66596 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -2016,32 +2016,6 @@ button.danger:hover:not([disabled]) { font-weight: 600; } -.doc-type-inline { - color: var(--muted); - font-size: 0.85em; - line-height: 1.2; - cursor: default; -} - -.doc-type-inline.active { - color: var(--accent-strong, var(--accent)); - font-weight: 600; -} - -.doc-type-inline[role='button'] { - cursor: pointer; -} - -.doc-type-inline[role='button']:hover, -.doc-type-inline[role='button']:focus-visible { - color: var(--accent-strong, var(--accent)); -} - -.doc-type-inline[role='button']:focus-visible { - outline: 2px solid currentColor; - outline-offset: 2px; -} - .doc-correspondent-link { background: none; background-color: transparent;