linting, tenant slug -> name

This commit is contained in:
2025-10-29 18:26:59 +01:00
parent b6bcb72165
commit abc0116945
23 changed files with 324 additions and 487 deletions
@@ -0,0 +1,5 @@
ALTER TABLE tenants
RENAME COLUMN name TO slug;
ALTER TABLE tenants
RENAME CONSTRAINT tenants_name_key TO tenants_slug_key;
@@ -0,0 +1,5 @@
ALTER TABLE tenants
RENAME COLUMN slug TO name;
ALTER TABLE tenants
RENAME CONSTRAINT tenants_slug_key TO tenants_name_key;
+76 -76
View File
@@ -39,23 +39,23 @@ enum Command {
username: String, username: String,
}, },
CreateTenant { CreateTenant {
slug: String, name: String,
storage_root: Option<String>, storage_root: Option<String>,
quickwit_index: Option<String>, quickwit_index: Option<String>,
}, },
DeleteTenant { DeleteTenant {
slug: String, name: String,
}, },
AddUserToTenant { AddUserToTenant {
username: String, username: String,
slug: String, name: String,
}, },
RemoveUserFromTenant { RemoveUserFromTenant {
username: String, username: String,
slug: String, name: String,
}, },
ReanalyzeDocuments { ReanalyzeDocuments {
slug: String, name: String,
}, },
ListTenants, ListTenants,
DeleteAssets(String), DeleteAssets(String),
@@ -70,15 +70,15 @@ impl Command {
set-password <username> <password>\n\ set-password <username> <password>\n\
list-users\n\ list-users\n\
delete-user <username>\n\ delete-user <username>\n\
create-tenant <slug> [storage_root] [quickwit_index]\n\ create-tenant <name> [storage_root] [quickwit_index]\n\
delete-tenant <slug>\n\ delete-tenant <name>\n\
add-user-to-tenant <username> <slug>\n\ add-user-to-tenant <username> <name>\n\
remove-user-from-tenant <username> <slug>\n\ remove-user-from-tenant <username> <name>\n\
reanalyze-documents <slug>\n\ reanalyze-documents <name>\n\
list-tenants\n\ list-tenants\n\
delete-assets <slug>\n\ delete-assets <name>\n\
quickwit-create-index <slug>\n\ quickwit-create-index <name>\n\
quickwit-delete-index <slug>" quickwit-delete-index <name>"
} }
fn parse() -> Result<Self> { fn parse() -> Result<Self> {
@@ -97,33 +97,33 @@ impl Command {
username: args.next().ok_or_else(|| anyhow!("username required"))?, username: args.next().ok_or_else(|| anyhow!("username required"))?,
}), }),
Some("create-tenant") => Ok(Self::CreateTenant { Some("create-tenant") => Ok(Self::CreateTenant {
slug: args.next().ok_or_else(|| anyhow!("tenant slug required"))?, name: args.next().ok_or_else(|| anyhow!("tenant name required"))?,
storage_root: args.next(), storage_root: args.next(),
quickwit_index: args.next(), quickwit_index: args.next(),
}), }),
Some("delete-tenant") => Ok(Self::DeleteTenant { Some("delete-tenant") => Ok(Self::DeleteTenant {
slug: args.next().ok_or_else(|| anyhow!("tenant slug required"))?, name: args.next().ok_or_else(|| anyhow!("tenant name required"))?,
}), }),
Some("add-user-to-tenant") => Ok(Self::AddUserToTenant { Some("add-user-to-tenant") => Ok(Self::AddUserToTenant {
username: args.next().ok_or_else(|| anyhow!("username required"))?, username: args.next().ok_or_else(|| anyhow!("username required"))?,
slug: args.next().ok_or_else(|| anyhow!("tenant slug required"))?, name: args.next().ok_or_else(|| anyhow!("tenant name required"))?,
}), }),
Some("remove-user-from-tenant") => Ok(Self::RemoveUserFromTenant { Some("remove-user-from-tenant") => Ok(Self::RemoveUserFromTenant {
username: args.next().ok_or_else(|| anyhow!("username required"))?, username: args.next().ok_or_else(|| anyhow!("username required"))?,
slug: args.next().ok_or_else(|| anyhow!("tenant slug required"))?, name: args.next().ok_or_else(|| anyhow!("tenant name required"))?,
}), }),
Some("reanalyze-documents") => Ok(Self::ReanalyzeDocuments { Some("reanalyze-documents") => Ok(Self::ReanalyzeDocuments {
slug: args.next().ok_or_else(|| anyhow!("tenant slug required"))?, name: args.next().ok_or_else(|| anyhow!("tenant name required"))?,
}), }),
Some("list-tenants") => Ok(Self::ListTenants), Some("list-tenants") => Ok(Self::ListTenants),
Some("delete-assets") => Ok(Self::DeleteAssets( Some("delete-assets") => Ok(Self::DeleteAssets(
args.next().ok_or_else(|| anyhow!("tenant slug required"))?, args.next().ok_or_else(|| anyhow!("tenant name required"))?,
)), )),
Some("quickwit-create-index") => Ok(Self::QuickwitCreate( Some("quickwit-create-index") => Ok(Self::QuickwitCreate(
args.next().ok_or_else(|| anyhow!("tenant slug required"))?, args.next().ok_or_else(|| anyhow!("tenant name required"))?,
)), )),
Some("quickwit-delete-index") => Ok(Self::QuickwitDelete( Some("quickwit-delete-index") => Ok(Self::QuickwitDelete(
args.next().ok_or_else(|| anyhow!("tenant slug required"))?, args.next().ok_or_else(|| anyhow!("tenant name required"))?,
)), )),
_ => Err(anyhow!(Self::usage())), _ => Err(anyhow!(Self::usage())),
} }
@@ -143,23 +143,23 @@ async fn main() -> Result<()> {
Command::ListUsers => list_users(&pool)?, Command::ListUsers => list_users(&pool)?,
Command::DeleteUser { username } => delete_user(&pool, &username)?, Command::DeleteUser { username } => delete_user(&pool, &username)?,
Command::CreateTenant { Command::CreateTenant {
slug, name,
storage_root, storage_root,
quickwit_index, quickwit_index,
} => create_tenant(&pool, &slug, storage_root, quickwit_index)?, } => create_tenant(&pool, &name, storage_root, quickwit_index)?,
Command::DeleteTenant { slug } => delete_tenant(&pool, &slug)?, Command::DeleteTenant { name } => delete_tenant(&pool, &name)?,
Command::AddUserToTenant { username, slug } => add_user_to_tenant(&pool, &username, &slug)?, Command::AddUserToTenant { username, name } => add_user_to_tenant(&pool, &username, &name)?,
Command::RemoveUserFromTenant { username, slug } => { Command::RemoveUserFromTenant { username, name } => {
remove_user_from_tenant(&pool, &username, &slug)? remove_user_from_tenant(&pool, &username, &name)?
} }
Command::ReanalyzeDocuments { slug } => reanalyze_documents(&pool, &slug)?, Command::ReanalyzeDocuments { name } => reanalyze_documents(&pool, &name)?,
Command::ListTenants => list_tenants(&pool)?, Command::ListTenants => list_tenants(&pool)?,
Command::DeleteAssets(slug) => delete_assets_for_tenant(&config, &pool, &slug).await?, Command::DeleteAssets(name) => delete_assets_for_tenant(&config, &pool, &name).await?,
Command::QuickwitCreate(slug) => { Command::QuickwitCreate(name) => {
quickwit_index(&config, &pool, &slug, Method::POST).await? quickwit_index(&config, &pool, &name, Method::POST).await?
} }
Command::QuickwitDelete(slug) => { Command::QuickwitDelete(name) => {
quickwit_index(&config, &pool, &slug, Method::DELETE).await? quickwit_index(&config, &pool, &name, Method::DELETE).await?
} }
} }
@@ -229,8 +229,8 @@ fn list_users(pool: &PgPool) -> Result<()> {
let memberships: Vec<String> = user_memberships::table let memberships: Vec<String> = user_memberships::table
.inner_join(tenants::table) .inner_join(tenants::table)
.filter(user_memberships::user_id.eq(user.id)) .filter(user_memberships::user_id.eq(user.id))
.select(tenants::slug) .select(tenants::name)
.order(tenants::slug.asc()) .order(tenants::name.asc())
.load(&mut conn)?; .load(&mut conn)?;
if memberships.is_empty() { if memberships.is_empty() {
@@ -267,14 +267,14 @@ fn delete_user(pool: &PgPool, username: &str) -> Result<()> {
fn create_tenant( fn create_tenant(
pool: &PgPool, pool: &PgPool,
slug: &str, name: &str,
storage_root_arg: Option<String>, storage_root_arg: Option<String>,
quickwit_index_arg: Option<String>, quickwit_index_arg: Option<String>,
) -> Result<()> { ) -> Result<()> {
let service = TenantService::new(pool.clone()); let service = TenantService::new(pool.clone());
let tenant = service let tenant = service
.create_tenant( .create_tenant(
slug, name,
storage_root_arg.as_deref(), storage_root_arg.as_deref(),
quickwit_index_arg.as_deref(), quickwit_index_arg.as_deref(),
TenantStatus::Creating, TenantStatus::Creating,
@@ -288,7 +288,7 @@ fn create_tenant(
println!( println!(
"created tenant '{}' with id {}, storage_root '{}', quickwit_index '{}', status '{}'", "created tenant '{}' with id {}, storage_root '{}', quickwit_index '{}', status '{}'",
tenant.slug, tenant.name,
tenant.id, tenant.id,
storage_root, storage_root,
quickwit_index, quickwit_index,
@@ -297,29 +297,29 @@ fn create_tenant(
Ok(()) Ok(())
} }
fn delete_tenant(pool: &PgPool, slug: &str) -> Result<()> { fn delete_tenant(pool: &PgPool, name: &str) -> Result<()> {
let mut conn = pool.get().context("failed to get database connection")?; let mut conn = pool.get().context("failed to get database connection")?;
let tenant: Tenant = tenants::table let tenant: Tenant = tenants::table
.filter(tenants::slug.eq(slug)) .filter(tenants::name.eq(name))
.first(&mut conn) .first(&mut conn)
.optional()? .optional()?
.ok_or_else(|| anyhow!("tenant '{}' not found", slug))?; .ok_or_else(|| anyhow!("tenant '{}' not found", name))?;
let member_exists: bool = select(exists( let member_exists: bool = select(exists(
user_memberships::table.filter(user_memberships::tenant_id.eq(tenant.id)), user_memberships::table.filter(user_memberships::tenant_id.eq(tenant.id)),
)) ))
.get_result(&mut conn)?; .get_result(&mut conn)?;
if member_exists { if member_exists {
bail!("tenant '{}' still has user memberships", slug); bail!("tenant '{}' still has user memberships", name);
} }
diesel::delete(tenants::table.filter(tenants::id.eq(tenant.id))).execute(&mut conn)?; diesel::delete(tenants::table.filter(tenants::id.eq(tenant.id))).execute(&mut conn)?;
println!("deleted tenant '{}'", slug); println!("deleted tenant '{}'", name);
Ok(()) Ok(())
} }
fn add_user_to_tenant(pool: &PgPool, username: &str, slug: &str) -> Result<()> { fn add_user_to_tenant(pool: &PgPool, username: &str, name: &str) -> Result<()> {
let mut conn = pool.get().context("failed to get database connection")?; let mut conn = pool.get().context("failed to get database connection")?;
let user: User = users::table let user: User = users::table
@@ -329,10 +329,10 @@ fn add_user_to_tenant(pool: &PgPool, username: &str, slug: &str) -> Result<()> {
.ok_or_else(|| anyhow!("user '{}' not found", username))?; .ok_or_else(|| anyhow!("user '{}' not found", username))?;
let tenant: Tenant = tenants::table let tenant: Tenant = tenants::table
.filter(tenants::slug.eq(slug)) .filter(tenants::name.eq(name))
.first(&mut conn) .first(&mut conn)
.optional()? .optional()?
.ok_or_else(|| anyhow!("tenant '{}' not found", slug))?; .ok_or_else(|| anyhow!("tenant '{}' not found", name))?;
let membership = NewUserMembership { let membership = NewUserMembership {
id: Uuid::new_v4(), id: Uuid::new_v4(),
@@ -346,11 +346,11 @@ fn add_user_to_tenant(pool: &PgPool, username: &str, slug: &str) -> Result<()> {
.do_nothing() .do_nothing()
.execute(&mut conn)?; .execute(&mut conn)?;
println!("added user '{}' to tenant '{}'", username, slug); println!("added user '{}' to tenant '{}'", username, name);
Ok(()) Ok(())
} }
fn remove_user_from_tenant(pool: &PgPool, username: &str, slug: &str) -> Result<()> { fn remove_user_from_tenant(pool: &PgPool, username: &str, name: &str) -> Result<()> {
let mut conn = pool.get().context("failed to get database connection")?; let mut conn = pool.get().context("failed to get database connection")?;
let user: User = users::table let user: User = users::table
@@ -360,10 +360,10 @@ fn remove_user_from_tenant(pool: &PgPool, username: &str, slug: &str) -> Result<
.ok_or_else(|| anyhow!("user '{}' not found", username))?; .ok_or_else(|| anyhow!("user '{}' not found", username))?;
let tenant: Tenant = tenants::table let tenant: Tenant = tenants::table
.filter(tenants::slug.eq(slug)) .filter(tenants::name.eq(name))
.first(&mut conn) .first(&mut conn)
.optional()? .optional()?
.ok_or_else(|| anyhow!("tenant '{}' not found", slug))?; .ok_or_else(|| anyhow!("tenant '{}' not found", name))?;
let removed = diesel::delete( let removed = diesel::delete(
user_memberships::table user_memberships::table
@@ -373,21 +373,21 @@ fn remove_user_from_tenant(pool: &PgPool, username: &str, slug: &str) -> Result<
.execute(&mut conn)?; .execute(&mut conn)?;
if removed == 0 { if removed == 0 {
println!("user '{}' was not a member of tenant '{}'", username, slug); println!("user '{}' was not a member of tenant '{}'", username, name);
} else { } else {
println!("removed user '{}' from tenant '{}'", username, slug); println!("removed user '{}' from tenant '{}'", username, name);
} }
Ok(()) Ok(())
} }
fn reanalyze_documents(pool: &PgPool, slug: &str) -> Result<()> { fn reanalyze_documents(pool: &PgPool, name: &str) -> Result<()> {
let mut conn = pool.get().context("failed to get database connection")?; let mut conn = pool.get().context("failed to get database connection")?;
let tenant: Tenant = tenants::table let tenant: Tenant = tenants::table
.filter(tenants::slug.eq(slug)) .filter(tenants::name.eq(name))
.first(&mut conn) .first(&mut conn)
.optional()? .optional()?
.ok_or_else(|| anyhow!("tenant '{}' not found", slug))?; .ok_or_else(|| anyhow!("tenant '{}' not found", name))?;
let targets: Vec<(Uuid, Uuid)> = documents::table let targets: Vec<(Uuid, Uuid)> = documents::table
.filter(documents::tenant_id.eq(tenant.id)) .filter(documents::tenant_id.eq(tenant.id))
@@ -396,7 +396,7 @@ fn reanalyze_documents(pool: &PgPool, slug: &str) -> Result<()> {
.load(&mut conn)?; .load(&mut conn)?;
if targets.is_empty() { if targets.is_empty() {
println!("tenant '{}' has no active documents", slug); println!("tenant '{}' has no active documents", name);
return Ok(()); return Ok(());
} }
@@ -419,7 +419,7 @@ fn reanalyze_documents(pool: &PgPool, slug: &str) -> Result<()> {
println!( println!(
"queued {} documents for re-analysis in tenant '{}'", "queued {} documents for re-analysis in tenant '{}'",
queued, slug queued, name
); );
Ok(()) Ok(())
} }
@@ -427,7 +427,7 @@ fn reanalyze_documents(pool: &PgPool, slug: &str) -> Result<()> {
fn list_tenants(pool: &PgPool) -> Result<()> { fn list_tenants(pool: &PgPool) -> Result<()> {
let mut conn = pool.get().context("failed to get database connection")?; let mut conn = pool.get().context("failed to get database connection")?;
let tenants: Vec<Tenant> = tenants::table let tenants: Vec<Tenant> = tenants::table
.order(tenants::slug.asc()) .order(tenants::name.asc())
.load(&mut conn) .load(&mut conn)
.context("failed to load tenants")?; .context("failed to load tenants")?;
@@ -437,7 +437,7 @@ fn list_tenants(pool: &PgPool) -> Result<()> {
} }
for tenant in tenants { for tenant in tenants {
println!("{} ({})", tenant.slug, tenant.id); println!("{} ({})", tenant.name, tenant.id);
} }
Ok(()) Ok(())
@@ -446,7 +446,7 @@ fn list_tenants(pool: &PgPool) -> Result<()> {
async fn delete_assets_for_tenant( async fn delete_assets_for_tenant(
config: &AppConfig, config: &AppConfig,
pool: &PgPool, pool: &PgPool,
tenant_slug: &str, tenant_name: &str,
) -> Result<()> { ) -> Result<()> {
let s3_client = s3::build_client(config).await?; let s3_client = s3::build_client(config).await?;
let storage: Arc<dyn ObjectStorage> = let storage: Arc<dyn ObjectStorage> =
@@ -454,28 +454,28 @@ async fn delete_assets_for_tenant(
let mut conn = pool.get().context("failed to get database connection")?; let mut conn = pool.get().context("failed to get database connection")?;
let tenant: Tenant = tenants::table let tenant: Tenant = tenants::table
.filter(tenants::slug.eq(tenant_slug)) .filter(tenants::name.eq(tenant_name))
.first(&mut conn) .first(&mut conn)
.optional() .optional()
.context("failed to load tenant")? .context("failed to load tenant")?
.ok_or_else(|| anyhow!("tenant '{}' not found", tenant_slug))?; .ok_or_else(|| anyhow!("tenant '{}' not found", tenant_name))?;
let tenant_storage = TenantStorage::new(Arc::clone(&storage), &tenant) let tenant_storage = TenantStorage::new(Arc::clone(&storage), &tenant)
.with_context(|| format!("missing storage root for tenant {}", tenant.slug))?; .with_context(|| format!("missing storage root for tenant {}", tenant.name))?;
let assets: Vec<DocumentAsset> = document_assets::table let assets: Vec<DocumentAsset> = document_assets::table
.filter(document_assets::tenant_id.eq(tenant.id)) .filter(document_assets::tenant_id.eq(tenant.id))
.load(&mut conn) .load(&mut conn)
.with_context(|| format!("failed to load assets for tenant {}", tenant.slug))?; .with_context(|| format!("failed to load assets for tenant {}", tenant.name))?;
if assets.is_empty() { if assets.is_empty() {
println!("Tenant {}: no assets", tenant.slug); println!("Tenant {}: no assets", tenant.name);
return Ok(()); return Ok(());
} }
println!( println!(
"Tenant {} ({}): deleting {} assets…", "Tenant {} ({}): deleting {} assets…",
tenant.slug, tenant.name,
tenant.id, tenant.id,
assets.len() assets.len()
); );
@@ -486,13 +486,13 @@ async fn delete_assets_for_tenant(
.filter(document_asset_objects::tenant_id.eq(tenant.id)) .filter(document_asset_objects::tenant_id.eq(tenant.id))
.filter(document_asset_objects::asset_id.eq_any(&asset_ids)) .filter(document_asset_objects::asset_id.eq_any(&asset_ids))
.load(&mut conn) .load(&mut conn)
.with_context(|| format!("failed to load asset objects for tenant {}", tenant.slug))?; .with_context(|| format!("failed to load asset objects for tenant {}", tenant.name))?;
for object in &objects { for object in &objects {
if let Err(err) = tenant_storage.delete_object(&object.s3_key).await { if let Err(err) = tenant_storage.delete_object(&object.s3_key).await {
eprintln!( eprintln!(
"Failed to delete object {} (tenant {}): {err}", "Failed to delete object {} (tenant {}): {err}",
object.s3_key, tenant.slug object.s3_key, tenant.name
); );
} }
} }
@@ -503,20 +503,20 @@ async fn delete_assets_for_tenant(
.filter(document_asset_objects::asset_id.eq_any(&asset_ids)), .filter(document_asset_objects::asset_id.eq_any(&asset_ids)),
) )
.execute(&mut conn) .execute(&mut conn)
.with_context(|| format!("failed to remove asset objects for tenant {}", tenant.slug))?; .with_context(|| format!("failed to remove asset objects for tenant {}", tenant.name))?;
diesel::delete(document_assets::table.filter(document_assets::tenant_id.eq(tenant.id))) diesel::delete(document_assets::table.filter(document_assets::tenant_id.eq(tenant.id)))
.execute(&mut conn) .execute(&mut conn)
.with_context(|| format!("failed to remove asset records for tenant {}", tenant.slug))?; .with_context(|| format!("failed to remove asset records for tenant {}", tenant.name))?;
println!("Tenant {}: asset records deleted.", tenant.slug); println!("Tenant {}: asset records deleted.", tenant.name);
Ok(()) Ok(())
} }
async fn quickwit_index( async fn quickwit_index(
config: &AppConfig, config: &AppConfig,
pool: &PgPool, pool: &PgPool,
slug: &str, name: &str,
method: Method, method: Method,
) -> Result<()> { ) -> Result<()> {
let endpoint = config let endpoint = config
@@ -526,11 +526,11 @@ async fn quickwit_index(
let mut conn = pool.get().context("failed to get database connection")?; let mut conn = pool.get().context("failed to get database connection")?;
let tenant: Tenant = tenants::table let tenant: Tenant = tenants::table
.filter(tenants::slug.eq(slug)) .filter(tenants::name.eq(name))
.first(&mut conn) .first(&mut conn)
.optional() .optional()
.context("failed to query tenants")? .context("failed to query tenants")?
.ok_or_else(|| anyhow!("tenant '{}' not found", slug))?; .ok_or_else(|| anyhow!("tenant '{}' not found", name))?;
let client = Client::new(); let client = Client::new();
let index_id = format!("documents-{}", tenant.id); let index_id = format!("documents-{}", tenant.id);
@@ -549,7 +549,7 @@ async fn quickwit_index(
println!( println!(
"Tenant '{}' quickwit index set to '{}'.", "Tenant '{}' quickwit index set to '{}'.",
tenant.slug, index_id tenant.name, index_id
); );
} }
Method::DELETE => { Method::DELETE => {
@@ -566,7 +566,7 @@ async fn quickwit_index(
.execute(&mut conn) .execute(&mut conn)
.context("failed to clear tenant quickwit_index")?; .context("failed to clear tenant quickwit_index")?;
println!("Tenant '{}' quickwit index cleared.", tenant.slug); println!("Tenant '{}' quickwit index cleared.", tenant.name);
} }
status => { status => {
let body = response.text().await.unwrap_or_default(); let body = response.text().await.unwrap_or_default();
+3 -3
View File
@@ -54,8 +54,8 @@ pub struct AppConfig {
pub quickwit_endpoint: Option<String>, pub quickwit_endpoint: Option<String>,
#[serde(default)] #[serde(default)]
pub quickwit_index: Option<String>, pub quickwit_index: Option<String>,
#[serde(default = "default_tenant_slug")] #[serde(default = "default_tenant_name")]
pub default_tenant_slug: String, pub default_tenant_name: String,
#[serde(default = "default_worker_max_document_bytes")] #[serde(default = "default_worker_max_document_bytes")]
pub worker_max_document_bytes: u64, pub worker_max_document_bytes: u64,
#[serde(default)] #[serde(default)]
@@ -171,7 +171,7 @@ fn default_aws_region() -> String {
"us-east-1".to_string() "us-east-1".to_string()
} }
fn default_tenant_slug() -> String { fn default_tenant_name() -> String {
"admin".to_string() "admin".to_string()
} }
+1 -1
View File
@@ -97,7 +97,7 @@ impl FromSql<TenantStatusSql, Pg> for TenantStatus {
#[diesel(primary_key(id))] #[diesel(primary_key(id))]
pub struct Tenant { pub struct Tenant {
pub id: Uuid, pub id: Uuid,
pub slug: String, pub name: String,
pub storage_root: Option<String>, pub storage_root: Option<String>,
pub quickwit_index: Option<String>, pub quickwit_index: Option<String>,
pub config: Value, pub config: Value,
+2 -2
View File
@@ -684,7 +684,7 @@ pub mod schemas {
pub username: String, pub username: String,
pub password: String, pub password: String,
#[schema(nullable)] #[schema(nullable)]
pub preferred_tenant_slug: Option<String>, pub preferred_tenant_id: Option<Uuid>,
} }
#[derive(Serialize, Deserialize, ToSchema)] #[derive(Serialize, Deserialize, ToSchema)]
@@ -704,7 +704,7 @@ pub mod schemas {
#[derive(Serialize, Deserialize, ToSchema)] #[derive(Serialize, Deserialize, ToSchema)]
pub struct TenantSnippet { pub struct TenantSnippet {
pub id: Uuid, pub id: Uuid,
pub slug: String, pub name: String,
} }
#[derive(Serialize, Deserialize, ToSchema)] #[derive(Serialize, Deserialize, ToSchema)]
+14 -20
View File
@@ -41,7 +41,7 @@ pub struct LoginRequest {
pub username: String, pub username: String,
pub password: String, pub password: String,
#[serde(default)] #[serde(default)]
pub preferred_tenant_slug: Option<String>, pub preferred_tenant_id: Option<Uuid>,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@@ -61,7 +61,7 @@ pub struct LoginResponse {
#[derive(Serialize)] #[derive(Serialize)]
pub struct TenantSnippet { pub struct TenantSnippet {
pub id: Uuid, pub id: Uuid,
pub slug: String, pub name: String,
} }
#[derive(Serialize)] #[derive(Serialize)]
@@ -109,9 +109,9 @@ pub async fn signup(
insert_user(&mut conn, user_id, username, &password_hash)?; insert_user(&mut conn, user_id, username, &password_hash)?;
let tenant_slug = username.to_lowercase(); let tenant_name = username.to_string();
let tenant = state.tenants.create_tenant( let tenant = state.tenants.create_tenant(
&tenant_slug, &tenant_name,
None, None,
None, None,
TenantStatus::Creating, TenantStatus::Creating,
@@ -147,13 +147,7 @@ pub async fn login(
return Err(AppError::unauthorized()); return Err(AppError::unauthorized());
} }
let preferred_slug = payload complete_login(&state, &mut conn, &user, payload.preferred_tenant_id)
.preferred_tenant_slug
.as_ref()
.map(|slug| slug.trim().to_string())
.filter(|slug| !slug.is_empty());
complete_login(&state, &mut conn, &user, preferred_slug)
} }
pub async fn refresh( pub async fn refresh(
@@ -323,10 +317,10 @@ pub async fn list_tenants(
let tenants = memberships_dsl::user_memberships let tenants = memberships_dsl::user_memberships
.inner_join(tenant_dsl::tenants) .inner_join(tenant_dsl::tenants)
.filter(memberships_dsl::user_id.eq(user_id)) .filter(memberships_dsl::user_id.eq(user_id))
.select((tenant_dsl::id, tenant_dsl::slug)) .select((tenant_dsl::id, tenant_dsl::name))
.load::<(Uuid, String)>(&mut conn)? .load::<(Uuid, String)>(&mut conn)?
.into_iter() .into_iter()
.map(|(id, slug)| TenantSnippet { id, slug }) .map(|(id, name)| TenantSnippet { id, name })
.collect(); .collect();
Ok(Json(TenantListResponse { tenants })) Ok(Json(TenantListResponse { tenants }))
@@ -424,7 +418,7 @@ fn complete_login(
state: &AppState, state: &AppState,
conn: &mut PgConnection, conn: &mut PgConnection,
user: &User, user: &User,
preferred_slug: Option<String>, preferred_tenant_id: Option<Uuid>,
) -> AppResult<Response> { ) -> AppResult<Response> {
let memberships: Vec<(UserMembership, Tenant)> = memberships_dsl::user_memberships let memberships: Vec<(UserMembership, Tenant)> = memberships_dsl::user_memberships
.inner_join(tenant_dsl::tenants) .inner_join(tenant_dsl::tenants)
@@ -435,10 +429,10 @@ fn complete_login(
return Err(AppError::unauthorized()); return Err(AppError::unauthorized());
} }
if let Some(ref slug) = preferred_slug { if let Some(preferred_id) = preferred_tenant_id {
if let Some((_, tenant)) = memberships if let Some((_, tenant)) = memberships
.iter() .iter()
.find(|(_, tenant)| tenant.slug.eq_ignore_ascii_case(slug)) .find(|(_, tenant)| tenant.id == preferred_id)
{ {
return issue_session(state, conn, user, tenant.id); return issue_session(state, conn, user, tenant.id);
} }
@@ -457,7 +451,7 @@ fn complete_login(
.into_iter() .into_iter()
.map(|(_, tenant)| TenantSnippet { .map(|(_, tenant)| TenantSnippet {
id: tenant.id, id: tenant.id,
slug: tenant.slug, name: tenant.name,
}) })
.collect(); .collect();
@@ -480,9 +474,9 @@ fn issue_session(
.generate_token(user.id, tenant_id, &user.username) .generate_token(user.id, tenant_id, &user.username)
.map_err(AppError::from)?; .map_err(AppError::from)?;
let tenant_slug: String = tenant_dsl::tenants let tenant_name: String = tenant_dsl::tenants
.find(tenant_id) .find(tenant_id)
.select(tenant_dsl::slug) .select(tenant_dsl::name)
.first(conn) .first(conn)
.map_err(AppError::from)?; .map_err(AppError::from)?;
@@ -509,7 +503,7 @@ fn issue_session(
expires_in: state.config.jwt_expiry_minutes * 60, expires_in: state.config.jwt_expiry_minutes * 60,
tenant: TenantSnippet { tenant: TenantSnippet {
id: tenant_id, id: tenant_id,
slug: tenant_slug, name: tenant_name,
}, },
}) })
.into_response(); .into_response();
+2 -2
View File
@@ -457,11 +457,11 @@ fn authenticate(state: &AppState, headers: &HeaderMap) -> Result<Option<WebDavCo
.inner_join(tenant_dsl::tenants) .inner_join(tenant_dsl::tenants)
.filter(memberships_dsl::user_id.eq(user.id)) .filter(memberships_dsl::user_id.eq(user.id))
.filter(memberships_dsl::tenant_id.eq(token.tenant_id)) .filter(memberships_dsl::tenant_id.eq(token.tenant_id))
.select((tenant_dsl::id, tenant_dsl::slug)) .select((tenant_dsl::id, tenant_dsl::name))
.first::<(Uuid, String)>(&mut conn) .first::<(Uuid, String)>(&mut conn)
.optional()?; .optional()?;
let (tenant_id, _slug) = match tenant_row { let (tenant_id, _name) = match tenant_row {
Some(row) => row, Some(row) => row,
None => { None => {
tracing::warn!( tracing::warn!(
+1 -1
View File
@@ -159,7 +159,7 @@ diesel::table! {
tenants (id) { tenants (id) {
id -> Uuid, id -> Uuid,
slug -> Text, name -> Text,
storage_root -> Nullable<Text>, storage_root -> Nullable<Text>,
quickwit_index -> Nullable<Text>, quickwit_index -> Nullable<Text>,
config -> Jsonb, config -> Jsonb,
+15 -15
View File
@@ -24,9 +24,9 @@ impl TenantRepository {
dsl::tenants.find(tenant_id).first(conn).map_err(Into::into) dsl::tenants.find(tenant_id).first(conn).map_err(Into::into)
} }
pub fn get_by_slug(conn: &mut PgConnection, slug: &str) -> AppResult<Tenant> { pub fn get_by_name(conn: &mut PgConnection, name: &str) -> AppResult<Tenant> {
dsl::tenants dsl::tenants
.filter(dsl::slug.eq(slug)) .filter(dsl::name.eq(name))
.first(conn) .first(conn)
.map_err(Into::into) .map_err(Into::into)
} }
@@ -47,28 +47,28 @@ impl TenantService {
Ok(tenant) Ok(tenant)
} }
pub fn get_by_slug(&self, slug: &str) -> AppResult<Tenant> { pub fn get_by_name(&self, name: &str) -> AppResult<Tenant> {
let slug_owned = slug.to_owned(); let name_owned = name.to_owned();
let tenant = self.load(|conn| TenantRepository::get_by_slug(conn, &slug_owned))?; let tenant = self.load(|conn| TenantRepository::get_by_name(conn, &name_owned))?;
Ok(tenant) Ok(tenant)
} }
pub fn tenant_id_for_slug(&self, slug: &str) -> AppResult<Uuid> { pub fn tenant_id_for_name(&self, name: &str) -> AppResult<Uuid> {
Ok(self.get_by_slug(slug)?.id) Ok(self.get_by_name(name)?.id)
} }
pub fn create_tenant( pub fn create_tenant(
&self, &self,
slug: &str, name: &str,
storage_root: Option<&str>, storage_root: Option<&str>,
quickwit_index: Option<&str>, quickwit_index: Option<&str>,
status: TenantStatus, status: TenantStatus,
initial_members: &[Uuid], initial_members: &[Uuid],
created_by: Option<Uuid>, created_by: Option<Uuid>,
) -> AppResult<Tenant> { ) -> AppResult<Tenant> {
let slug = slug.trim(); let name = name.trim();
if slug.is_empty() { if name.is_empty() {
return Err(AppError::bad_request("tenant slug must not be empty")); return Err(AppError::bad_request("tenant name must not be empty"));
} }
let mut conn = self.pool.get().map_err(|err| { let mut conn = self.pool.get().map_err(|err| {
@@ -77,11 +77,11 @@ impl TenantService {
})?; })?;
let exists: bool = let exists: bool =
select(exists(dsl::tenants.filter(dsl::slug.eq(slug)))).get_result(&mut conn)?; select(exists(dsl::tenants.filter(dsl::name.eq(name)))).get_result(&mut conn)?;
if exists { if exists {
return Err(AppError::conflict(format!( return Err(AppError::conflict(format!(
"tenant '{}' already exists", "tenant '{}' already exists",
slug name
))); )));
} }
@@ -92,7 +92,7 @@ impl TenantService {
diesel::insert_into(dsl::tenants) diesel::insert_into(dsl::tenants)
.values(( .values((
dsl::id.eq(id), dsl::id.eq(id),
dsl::slug.eq(slug), dsl::name.eq(name),
dsl::storage_root.eq(Some(storage_root.clone())), dsl::storage_root.eq(Some(storage_root.clone())),
dsl::quickwit_index.eq(Some(quickwit_index.clone())), dsl::quickwit_index.eq(Some(quickwit_index.clone())),
dsl::config.eq(json!({})), dsl::config.eq(json!({})),
@@ -150,7 +150,7 @@ impl FromRequestParts<AppState> for TenantContext {
) -> Result<Self, Self::Rejection> { ) -> Result<Self, Self::Rejection> {
let tenant = state let tenant = state
.tenants .tenants
.get_by_slug(&state.config.default_tenant_slug)?; .get_by_name(&state.config.default_tenant_name)?;
Ok(Self { tenant }) Ok(Self { tenant })
} }
} }
+10 -9
View File
@@ -33,7 +33,8 @@ struct ErrorResponse {
#[derive(Deserialize)] #[derive(Deserialize)]
struct LoginTenant { struct LoginTenant {
slug: String, id: Uuid,
name: String,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@@ -51,7 +52,7 @@ struct TenantSelectionResponse {
#[derive(Deserialize)] #[derive(Deserialize)]
struct TenantSummary { struct TenantSummary {
id: Uuid, id: Uuid,
slug: String, name: String,
} }
#[tokio::test] #[tokio::test]
@@ -116,7 +117,7 @@ async fn signup_creates_user_tenant_and_membership() -> Result<()> {
.first(conn)?; .first(conn)?;
let tenant: backend::models::Tenant = tenants::table let tenant: backend::models::Tenant = tenants::table
.filter(tenants::slug.eq("signup-user")) .filter(tenants::name.eq("signup-user"))
.first(conn)?; .first(conn)?;
assert_eq!(tenant.status, TenantStatus::Creating); assert_eq!(tenant.status, TenantStatus::Creating);
@@ -419,7 +420,7 @@ async fn refresh_rotates_refresh_token() -> Result<()> {
let new_cookie = extract_refresh_cookie(response.headers())?; let new_cookie = extract_refresh_cookie(response.headers())?;
let body = body_to_vec(response.into_body()).await?; let body = body_to_vec(response.into_body()).await?;
let refreshed: LoginResponse = serde_json::from_slice(&body)?; let refreshed: LoginResponse = serde_json::from_slice(&body)?;
assert_eq!(refreshed.tenant.slug, login.tenant.slug); assert_eq!(refreshed.tenant.name, login.tenant.name);
let me_response = app let me_response = app
.get("/api/auth/me", Some(&refreshed.access_token)) .get("/api/auth/me", Some(&refreshed.access_token))
@@ -492,14 +493,14 @@ async fn login_returns_tenant_selection_when_multiple_memberships() -> Result<()
let password = "multipass"; let password = "multipass";
let user_id = app.insert_user("multipass", password, "admin").await?; let user_id = app.insert_user("multipass", password, "admin").await?;
let secondary_slug = "secondary".to_string(); let secondary_name = "secondary".to_string();
let slug_for_insert = secondary_slug.clone(); let name_for_insert = secondary_name.clone();
let secondary_id = Uuid::new_v4(); let secondary_id = Uuid::new_v4();
app.with_conn(move |conn| { app.with_conn(move |conn| {
diesel::insert_into(tenants::table) diesel::insert_into(tenants::table)
.values(( .values((
tenants::id.eq(secondary_id), tenants::id.eq(secondary_id),
tenants::slug.eq(&slug_for_insert), tenants::name.eq(&name_for_insert),
tenants::status.eq(TenantStatus::Active), tenants::status.eq(TenantStatus::Active),
)) ))
.execute(conn)?; .execute(conn)?;
@@ -526,7 +527,7 @@ async fn login_returns_tenant_selection_when_multiple_memberships() -> Result<()
let secondary = selection let secondary = selection
.tenants .tenants
.iter() .iter()
.find(|tenant| tenant.slug == secondary_slug) .find(|tenant| tenant.name == secondary_name)
.map(|t| t.id) .map(|t| t.id)
.context("secondary tenant missing from selection")?; .context("secondary tenant missing from selection")?;
@@ -541,7 +542,7 @@ async fn login_returns_tenant_selection_when_multiple_memberships() -> Result<()
let session_cookie = extract_refresh_cookie(select_response.headers())?; let session_cookie = extract_refresh_cookie(select_response.headers())?;
let select_body = body_to_vec(select_response.into_body()).await?; let select_body = body_to_vec(select_response.into_body()).await?;
let login: LoginResponse = serde_json::from_slice(&select_body)?; let login: LoginResponse = serde_json::from_slice(&select_body)?;
assert_eq!(login.tenant.slug, secondary_slug); assert_eq!(login.tenant.name, secondary_name);
let me_response = app.get("/api/auth/me", Some(&login.access_token)).await?; let me_response = app.get("/api/auth/me", Some(&login.access_token)).await?;
assert_eq!(me_response.status(), StatusCode::OK); assert_eq!(me_response.status(), StatusCode::OK);
+7 -7
View File
@@ -140,7 +140,7 @@ impl TestApp {
s3_bucket: "test-bucket".to_string(), s3_bucket: "test-bucket".to_string(),
quickwit_endpoint: None, quickwit_endpoint: None,
quickwit_index: None, quickwit_index: None,
default_tenant_slug: "admin".to_string(), default_tenant_name: "admin".to_string(),
worker_max_document_bytes: 200 * 1024 * 1024, worker_max_document_bytes: 200 * 1024 * 1024,
webauthn_rp_id: Some("localhost".to_string()), webauthn_rp_id: Some("localhost".to_string()),
webauthn_origin: Some("http://localhost".to_string()), webauthn_origin: Some("http://localhost".to_string()),
@@ -193,7 +193,7 @@ impl TestApp {
let tenant = self let tenant = self
.state .state
.tenants .tenants
.get_by_slug(&self.state.config.default_tenant_slug) .get_by_name(&self.state.config.default_tenant_name)
.map_err(|err| anyhow!("default tenant not found: {:?}", err))?; .map_err(|err| anyhow!("default tenant not found: {:?}", err))?;
let root = tenant let root = tenant
.storage_root .storage_root
@@ -208,7 +208,7 @@ impl TestApp {
let tenant_id = self let tenant_id = self
.state .state
.tenants .tenants
.tenant_id_for_slug(&self.state.config.default_tenant_slug) .tenant_id_for_name(&self.state.config.default_tenant_name)
.map_err(|err| anyhow!("default tenant not found: {:?}", err))?; .map_err(|err| anyhow!("default tenant not found: {:?}", err))?;
self.with_conn(move |conn| { self.with_conn(move |conn| {
let password_hash = hash_password(&password)?; let password_hash = hash_password(&password)?;
@@ -264,13 +264,13 @@ impl TestApp {
} }
async fn ensure_default_tenant(&self) -> Result<Uuid> { async fn ensure_default_tenant(&self) -> Result<Uuid> {
let slug_value = self.state.config.default_tenant_slug.clone(); let name_value = self.state.config.default_tenant_name.clone();
let quickwit_enabled = self.state.config.quickwit_endpoint.is_some(); let quickwit_enabled = self.state.config.quickwit_endpoint.is_some();
self.with_conn(move |conn| { self.with_conn(move |conn| {
use backend::schema::tenants::dsl as tenants_dsl; use backend::schema::tenants::dsl as tenants_dsl;
let existing = tenants_dsl::tenants let existing = tenants_dsl::tenants
.filter(tenants_dsl::slug.eq(&slug_value)) .filter(tenants_dsl::name.eq(&name_value))
.first::<Tenant>(conn) .first::<Tenant>(conn)
.optional() .optional()
.context("failed to load default tenant")?; .context("failed to load default tenant")?;
@@ -302,7 +302,7 @@ impl TestApp {
diesel::insert_into(tenants_dsl::tenants) diesel::insert_into(tenants_dsl::tenants)
.values(( .values((
tenants_dsl::id.eq(new_id), tenants_dsl::id.eq(new_id),
tenants_dsl::slug.eq(&slug_value), tenants_dsl::name.eq(&name_value),
tenants_dsl::storage_root.eq(Some(root)), tenants_dsl::storage_root.eq(Some(root)),
tenants_dsl::quickwit_index.eq(quickwit_value), tenants_dsl::quickwit_index.eq(quickwit_value),
tenants_dsl::status.eq(TenantStatus::Active), tenants_dsl::status.eq(TenantStatus::Active),
@@ -352,7 +352,7 @@ impl TestApp {
#[derive(Deserialize)] #[derive(Deserialize)]
struct TenantSummary { struct TenantSummary {
id: Uuid, id: Uuid,
_slug: String, name: String,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
+1 -1
View File
@@ -211,7 +211,7 @@ async fn tags_are_isolated_between_tenants() -> Result<()> {
diesel::insert_into(tenants_dsl::tenants) diesel::insert_into(tenants_dsl::tenants)
.values(( .values((
tenants_dsl::id.eq(tenant_b_id), tenants_dsl::id.eq(tenant_b_id),
tenants_dsl::slug.eq("tenant-b"), tenants_dsl::name.eq("tenant-b"),
tenants_dsl::storage_root.eq(Some(storage_root)), tenants_dsl::storage_root.eq(Some(storage_root)),
tenants_dsl::status.eq(TenantStatus::Active), tenants_dsl::status.eq(TenantStatus::Active),
)) ))
+2 -2
View File
@@ -5,8 +5,8 @@ Unless noted otherwise, endpoints below require a valid `Authorization: Bearer <
Authentication Authentication
-------------- --------------
- POST /api/auth/login - Exchange username/password for an access token and refresh cookie (public). Returns the active tenant as `{ tenant: { id, slug } }`. When multiple tenants are available, the response contains an `access_token` (tenant-selector token) and tenant list instead. - POST /api/auth/login - Exchange username/password for an access token and refresh cookie (public). Returns the active tenant as `{ tenant: { id, name } }`. When multiple tenants are available, the response contains an `access_token` (tenant-selector token) and tenant list instead.
- POST /api/auth/refresh - Rotate the refresh cookie and return a new access token (public, requires refresh cookie). Response also includes the current tenant `{ tenant: { id, slug } }`. - POST /api/auth/refresh - Rotate the refresh cookie and return a new access token (public, requires refresh cookie). Response also includes the current tenant `{ tenant: { id, name } }`.
- POST /api/auth/logout - Revoke the caller's refresh tokens and clear the cookie. - POST /api/auth/logout - Revoke the caller's refresh tokens and clear the cookie.
- GET /api/auth/me - Return the authenticated principal payload. - GET /api/auth/me - Return the authenticated principal payload.
+32 -33
View File
@@ -123,10 +123,11 @@ const readTransferData = (dataTransfer, mimeTypes) => {
if (raw) { if (raw) {
return raw; return raw;
} }
} catch ( } catch (error) {
// eslint-disable-next-line no-empty if (DEBUG_DROP) {
error console.warn('[skeuo] readTransferData failed for type', type, error);
) {} }
}
} }
return null; return null;
}; };
@@ -141,10 +142,9 @@ const parseTagTransferPayload = (event) => {
} }
try { try {
return JSON.parse(raw); return JSON.parse(raw);
} catch ( } catch (error) {
// eslint-disable-next-line no-empty console.warn('[skeuo] parseTagTransferPayload failed', error);
error }
) {}
return null; return null;
}; };
@@ -465,10 +465,9 @@ const usePreviewMetadata = (documents, getDocumentAsset, ensureAssetUrl) => {
view = createAssetView(asset); view = createAssetView(asset);
metadata = view.getPrimaryMetadata(); metadata = view.getPrimaryMetadata();
} }
} catch ( } catch (error) {
// eslint-disable-next-line no-empty console.warn('[skeuo] ensureDocumentSize metadata fetch failed', error);
error }
) {}
} }
if (!hasDimensions(metadata)) { if (!hasDimensions(metadata)) {
@@ -509,7 +508,7 @@ const usePreviewMetadata = (documents, getDocumentAsset, ensureAssetUrl) => {
return metadataMap; return metadataMap;
}; };
const seededRandom = (input) => { function seededRandom(input) {
const text = String(input); const text = String(input);
let hash = 2166136261; let hash = 2166136261;
for (let index = 0; index < text.length; index += 1) { for (let index = 0; index < text.length; index += 1) {
@@ -517,16 +516,18 @@ const seededRandom = (input) => {
hash = Math.imul(hash, 16777619); hash = Math.imul(hash, 16777619);
} }
return (hash >>> 0) / 4294967295; return (hash >>> 0) / 4294967295;
}; }
const randomRangeFromSeed = (seedKey, min, max) => { function randomRangeFromSeed(seedKey, min, max) {
const span = max - min; const span = max - min;
if (span <= 0) return min; if (span <= 0) return min;
const seed = seededRandom(seedKey); const seed = seededRandom(seedKey);
return min + seed * span; return min + seed * span;
}; }
const buildKey = (docId, suffix) => `${docId}::${suffix}`; function buildKey(docId, suffix) {
return `${docId}::${suffix}`;
}
const clampCardDimensions = (width, height) => { const clampCardDimensions = (width, height) => {
const w = Number(width); const w = Number(width);
@@ -758,10 +759,11 @@ const DesktopWorkspace = ({
if (DEBUG_FOCUS) { if (DEBUG_FOCUS) {
console.log('[skeuo] focusCanvas: applied focus. activeElement:', document?.activeElement); console.log('[skeuo] focusCanvas: applied focus. activeElement:', document?.activeElement);
} }
} catch ( } catch (error) {
// eslint-disable-next-line no-empty if (DEBUG_FOCUS) {
error console.warn('[skeuo] focusTarget failed to focus canvas', error);
) {} }
}
}; };
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
@@ -1551,20 +1553,18 @@ const syncLayoutSnapshot = useCallback(() => {
if (event.dataTransfer) { if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'copyMove'; event.dataTransfer.effectAllowed = 'copyMove';
} }
} catch ( } catch (error) {
// eslint-disable-next-line no-empty console.warn('[skeuo] Failed to set drag effect', error);
error }
) {}
const payload = JSON.stringify({ id: tag.id, label: tag.label, sourceDocId: doc.id }); const payload = JSON.stringify({ id: tag.id, label: tag.label, sourceDocId: doc.id });
try { try {
event.dataTransfer?.setData('application/x-papercrate-tag', payload); event.dataTransfer?.setData('application/x-papercrate-tag', payload);
event.dataTransfer?.setData('text/papercrate-tag', payload); event.dataTransfer?.setData('text/papercrate-tag', payload);
event.dataTransfer?.setData('text/plain', tag.label || 'Tag'); event.dataTransfer?.setData('text/plain', tag.label || 'Tag');
} catch ( } catch (error) {
// eslint-disable-next-line no-empty console.warn('[skeuo] Failed to populate drag data for tag', error);
error }
) {}
const pending = pendingDocTagDragRef.current; const pending = pendingDocTagDragRef.current;
const node = event.currentTarget; const node = event.currentTarget;
@@ -1593,10 +1593,9 @@ const syncLayoutSnapshot = useCallback(() => {
if (preview && event.dataTransfer) { if (preview && event.dataTransfer) {
try { try {
event.dataTransfer.setDragImage(preview.clone, preview.offsetX, preview.offsetY); event.dataTransfer.setDragImage(preview.clone, preview.offsetX, preview.offsetY);
} catch ( } catch (error) {
// eslint-disable-next-line no-empty console.warn('[skeuo] Failed to set drag image', error);
error }
) {}
} }
} }
draggingTagRef.current = { draggingTagRef.current = {
@@ -38,10 +38,11 @@ function CorrespondentsPanel({
try { try {
await onUpdate(editingId, { name: trimmed }); await onUpdate(editingId, { name: trimmed });
cancelEdit(); cancelEdit();
} catch ( } catch (error) {
// eslint-disable-next-line no-empty onNotify?.('Failed to update correspondent.', 'error');
error console.error('[correspondents] update failed', error);
) {} setSaving(false);
}
}, [editingId, draftName, onUpdate, cancelEdit, onNotify]); }, [editingId, draftName, onUpdate, cancelEdit, onNotify]);
const handleDelete = useCallback( const handleDelete = useCallback(
@@ -53,16 +54,14 @@ function CorrespondentsPanel({
if (editingId === correspondent.id) { if (editingId === correspondent.id) {
cancelEdit(); cancelEdit();
} }
} catch ( } catch (error) {
// eslint-disable-next-line no-empty onNotify?.('Failed to delete correspondent.', 'error');
error console.error('[correspondents] delete failed', error);
) {
return;
} finally { } finally {
setDeletingId(null); setDeletingId(null);
} }
}, },
[onDelete, editingId, cancelEdit], [onDelete, editingId, cancelEdit, onNotify],
); );
const handleCreate = useCallback( const handleCreate = useCallback(
@@ -77,11 +76,9 @@ function CorrespondentsPanel({
try { try {
await onCreate({ name: trimmed }); await onCreate({ name: trimmed });
setCreateName(''); setCreateName('');
} catch ( } catch (error) {
// eslint-disable-next-line no-empty onNotify?.('Failed to create correspondent.', 'error');
error console.error('[correspondents] create failed', error);
) {
return;
} finally { } finally {
setCreating(false); setCreating(false);
} }
+6 -8
View File
@@ -4,14 +4,12 @@ export const preventAll = (event) => {
} }
try { try {
event.preventDefault(); event.preventDefault();
} catch ( } catch (error) {
// eslint-disable-next-line no-empty console.warn('[events] preventDefault failed', error);
error }
) {}
try { try {
event.stopPropagation(); event.stopPropagation();
} catch ( } catch (error) {
// eslint-disable-next-line no-empty console.warn('[events] stopPropagation failed', error);
error }
) {}
}; };
+11 -9
View File
@@ -35,16 +35,17 @@ const useDocumentDrag = () => {
if (capturedTarget && typeof capturedTarget.releasePointerCapture === 'function') { if (capturedTarget && typeof capturedTarget.releasePointerCapture === 'function') {
try { try {
capturedTarget.releasePointerCapture(pointerId); capturedTarget.releasePointerCapture(pointerId);
} catch ( } catch (error) {
// eslint-disable-next-line no-empty if (debugDrag) {
error console.warn('[skeuo] releasePointerCapture failed', error);
) {} }
}
} }
dragStateRef.current = null; dragStateRef.current = null;
setDraggingId((current) => (current === state.docId ? null : current)); setDraggingId((current) => (current === state.docId ? null : current));
syncLayoutSnapshot(); syncLayoutSnapshot();
}, },
[setDraggingId, syncLayoutSnapshot], [debugDrag, setDraggingId, syncLayoutSnapshot],
); );
const handlePointerDown = useCallback( const handlePointerDown = useCallback(
@@ -83,10 +84,11 @@ const useDocumentDrag = () => {
if (capturedTarget && typeof capturedTarget.setPointerCapture === 'function') { if (capturedTarget && typeof capturedTarget.setPointerCapture === 'function') {
try { try {
capturedTarget.setPointerCapture(event.pointerId); capturedTarget.setPointerCapture(event.pointerId);
} catch ( } catch (error) {
// eslint-disable-next-line no-empty if (debugDrag) {
error console.warn('[skeuo] setPointerCapture failed', error);
) {} }
}
} }
dragStateRef.current = { dragStateRef.current = {
docId, docId,
+12 -7
View File
@@ -200,11 +200,7 @@ const PreviewStack = ({
onOpenPreview, onOpenPreview,
onZoomPreview, onZoomPreview,
}) => { }) => {
if (!items.length) { const limited = useMemo(() => items.slice(0, maxItems), [items, maxItems]);
return <span className="meta">{emptyMessage}</span>;
}
const limited = items.slice(0, maxItems);
const hasMultiple = limited.length > 1; const hasMultiple = limited.length > 1;
const preparedItems = useMemo( const preparedItems = useMemo(
() => () =>
@@ -215,6 +211,10 @@ const PreviewStack = ({
[limited], [limited],
); );
if (!limited.length) {
return <span className="meta">{emptyMessage}</span>;
}
return ( return (
<div className="preview-stack preview-stack--stacked"> <div className="preview-stack preview-stack--stacked">
{preparedItems.map(({ entry, angle }, index) => { {preparedItems.map(({ entry, angle }, index) => {
@@ -329,6 +329,11 @@ const DetailPanel = ({
const [titleError, setTitleError] = useState(null); const [titleError, setTitleError] = useState(null);
const [zoomedPreview, setZoomedPreview] = useState(null); const [zoomedPreview, setZoomedPreview] = useState(null);
const bulkDocumentIds = useMemo(
() => selectedDocuments.map((doc) => doc?.id).filter(Boolean),
[selectedDocuments],
);
useEffect(() => { useEffect(() => {
if (!singleDoc) { if (!singleDoc) {
setTitleEditDocId(null); setTitleEditDocId(null);
@@ -1097,7 +1102,7 @@ const DetailPanel = ({
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
}; };
const documentIds = Array.from(new Set(selectedDocuments.map((doc) => doc?.id).filter(Boolean))); const documentIds = bulkDocumentIds;
return ( return (
<> <>
@@ -1224,7 +1229,7 @@ const DetailPanel = ({
className="icon-button ghost" className="icon-button ghost"
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
onBulkReanalyze(documentIds); onBulkReanalyze(bulkDocumentIds);
}} }}
aria-label="Re-run analysis for selection" aria-label="Re-run analysis for selection"
title="Re-run analysis for selection" title="Re-run analysis for selection"
+20 -23
View File
@@ -392,10 +392,9 @@ const DocumentsTable = ({
if (parsed?.id && onDocumentTagDrop) { if (parsed?.id && onDocumentTagDrop) {
onDocumentTagDrop(documentId, parsed); onDocumentTagDrop(documentId, parsed);
} }
} catch ( } catch (error) {
// eslint-disable-next-line no-empty console.warn('[documents] Failed to parse tag drop payload', error);
error }
) {}
}, },
[isTagDragEvent, onDocumentTagDrop], [isTagDragEvent, onDocumentTagDrop],
); );
@@ -714,10 +713,9 @@ const DocumentsTable = ({
event.dataTransfer?.setData('application/x-papercrate-tag', payload); event.dataTransfer?.setData('application/x-papercrate-tag', payload);
event.dataTransfer?.setData('text/papercrate-tag', payload); event.dataTransfer?.setData('text/papercrate-tag', payload);
event.dataTransfer?.setData('text/plain', tag.label || 'Tag'); event.dataTransfer?.setData('text/plain', tag.label || 'Tag');
} catch ( } catch (error) {
// eslint-disable-next-line no-empty console.warn('[documents] Failed to configure tag drag payload', error);
error }
) {}
}} }}
onDragEnd={(event) => { onDragEnd={(event) => {
event.stopPropagation(); event.stopPropagation();
@@ -925,21 +923,20 @@ const DocumentsTable = ({
onDragStart={(event) => { onDragStart={(event) => {
event.stopPropagation(); event.stopPropagation();
try { try {
if (event.dataTransfer) { if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'copyMove'; event.dataTransfer.effectAllowed = 'copyMove';
} }
const payload = JSON.stringify({ const payload = JSON.stringify({
id: tag.id, id: tag.id,
label: tag.label, label: tag.label,
sourceDocId: doc.id, sourceDocId: doc.id,
}); });
event.dataTransfer?.setData('application/x-papercrate-tag', payload); event.dataTransfer?.setData('application/x-papercrate-tag', payload);
event.dataTransfer?.setData('text/papercrate-tag', payload); event.dataTransfer?.setData('text/papercrate-tag', payload);
event.dataTransfer?.setData('text/plain', tag.label || 'Tag'); event.dataTransfer?.setData('text/plain', tag.label || 'Tag');
} catch ( } catch (error) {
// eslint-disable-next-line no-empty console.warn('[documents] Failed to configure tag drag payload', error);
error }
) {}
}} }}
onDragEnd={(event) => { onDragEnd={(event) => {
event.stopPropagation(); event.stopPropagation();
+59 -219
View File
@@ -53,10 +53,9 @@ if (storage) {
if (rawTenant) { if (rawTenant) {
STORED_TENANT = JSON.parse(rawTenant); STORED_TENANT = JSON.parse(rawTenant);
} }
} catch ( } catch (error) {
// eslint-disable-next-line no-empty console.warn('[app] Failed to parse stored tenant metadata', error);
error }
) {}
} }
if (STORED_TOKEN) { if (STORED_TOKEN) {
api.defaults.headers.common.Authorization = `Bearer ${STORED_TOKEN}`; api.defaults.headers.common.Authorization = `Bearer ${STORED_TOKEN}`;
@@ -428,7 +427,7 @@ const LoginView = ({
: 'login-card__tenant-button' : 'login-card__tenant-button'
} }
> >
{tenant.slug} {tenant.name}
</button> </button>
))} ))}
</div> </div>
@@ -492,7 +491,7 @@ const AppLayout = () => {
const routeDocumentId = docMatch?.params?.documentId || null; const routeDocumentId = docMatch?.params?.documentId || null;
const previewDocumentId = routeDocumentId; const previewDocumentId = routeDocumentId;
const { status: appStatus, token, tenant, tenants: tenantOptions = [] } = appState; const { status: appStatus, token, tenant, tenants: tenantOptions = [] } = appState;
const tenantSlug = tenant?.slug || null; const tenantName = tenant?.name || tenant?.slug || null;
const currentTenantId = tenant?.id || null; const currentTenantId = tenant?.id || null;
const [status, setStatus] = useState(null); const [status, setStatus] = useState(null);
const setStatusMessage = useCallback((message, variant = 'info') => { const setStatusMessage = useCallback((message, variant = 'info') => {
@@ -586,6 +585,8 @@ const AppLayout = () => {
} }
}, [appDispatch]); }, [appDispatch]);
const [searchResults, setSearchResults] = useState(null); const [searchResults, setSearchResults] = useState(null);
const [previewEntries, setPreviewEntries] = useState(() => new Map());
const previewInflightRef = useRef(new Map());
const [tags, setTags] = useState([]); const [tags, setTags] = useState([]);
const [correspondents, setCorrespondents] = useState([]); const [correspondents, setCorrespondents] = useState([]);
const [webdavTokens, setWebdavTokens] = useState([]); const [webdavTokens, setWebdavTokens] = useState([]);
@@ -596,6 +597,7 @@ const AppLayout = () => {
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [activeTagFilters, setActiveTagFilters] = useState([]); const [activeTagFilters, setActiveTagFilters] = useState([]);
const [activeCorrespondentFilters, setActiveCorrespondentFilters] = useState([]); const [activeCorrespondentFilters, setActiveCorrespondentFilters] = useState([]);
const [searchLoading, setSearchLoading] = useState(false);
const documentsRouteMatch = useMatch('/documents'); const documentsRouteMatch = useMatch('/documents');
const documentsFolderRouteMatch = useMatch('/documents/folder/:folderId'); const documentsFolderRouteMatch = useMatch('/documents/folder/:folderId');
const documentsDetailRouteMatch = useMatch('/documents/:documentId'); const documentsDetailRouteMatch = useMatch('/documents/:documentId');
@@ -656,7 +658,6 @@ const AppLayout = () => {
folderName: DEFAULT_FOLDER_NAME, folderName: DEFAULT_FOLDER_NAME,
}); });
const [activePreviewId, setActivePreviewId] = useState(routeDocumentId || null); const [activePreviewId, setActivePreviewId] = useState(routeDocumentId || null);
const [searchLoading, setSearchLoading] = useState(false);
const shellRef = useRef(null); const shellRef = useRef(null);
const assetManagerRef = useRef(null); const assetManagerRef = useRef(null);
if (!assetManagerRef.current) { if (!assetManagerRef.current) {
@@ -1951,6 +1952,21 @@ const AppLayout = () => {
[refreshCorrespondents, notifyApiError, setStatusMessage, mapDocumentCaches], [refreshCorrespondents, notifyApiError, setStatusMessage, mapDocumentCaches],
); );
const refreshCurrentFolder = useCallback(async () => {
setLoading(true);
try {
const contents = await ensureFolderData(selectedFolder, {
force: true,
prefetchDepth: 1,
});
applySelectedFolder(selectedFolder, contents);
} catch (error) {
notifyApiError(error, 'Failed to refresh folder.');
} finally {
setLoading(false);
}
}, [selectedFolder, ensureFolderData, applySelectedFolder, notifyApiError]);
const handleDocumentCorrespondentAttach = useCallback( const handleDocumentCorrespondentAttach = useCallback(
async ({ documentId, correspondentId }, { notify = true, refresh = true } = {}) => { async ({ documentId, correspondentId }, { notify = true, refresh = true } = {}) => {
if (!documentId || !correspondentId) { if (!documentId || !correspondentId) {
@@ -2033,10 +2049,10 @@ const AppLayout = () => {
if (input) { if (input) {
input.value = ''; input.value = '';
} }
} catch ( } catch (error) {
// eslint-disable-next-line no-empty setStatusMessage('Failed to assign correspondent.', 'error');
error console.error('[documents] assign correspondent failed', error);
) {} }
}, },
[ [
handleCorrespondentCreate, handleCorrespondentCreate,
@@ -2286,21 +2302,6 @@ const AppLayout = () => {
selectFolder, selectFolder,
]); ]);
const refreshCurrentFolder = useCallback(async () => {
setLoading(true);
try {
const contents = await ensureFolderData(selectedFolder, {
force: true,
prefetchDepth: 1,
});
applySelectedFolder(selectedFolder, contents);
} catch (error) {
notifyApiError(error, 'Failed to refresh folder.');
} finally {
setLoading(false);
}
}, [selectedFolder, ensureFolderData, applySelectedFolder, notifyApiError]);
const resolveTargetDocumentIds = useCallback( const resolveTargetDocumentIds = useCallback(
(candidateIds) => { (candidateIds) => {
const normalized = Array.isArray(candidateIds) const normalized = Array.isArray(candidateIds)
@@ -2671,8 +2672,6 @@ const AppLayout = () => {
); );
const folderPathCacheRef = useRef(new Map()); const folderPathCacheRef = useRef(new Map());
const [previewEntries, setPreviewEntries] = useState(() => new Map());
const previewInflightRef = useRef(new Map());
const ensureFolderPathOnServer = useCallback( const ensureFolderPathOnServer = useCallback(
async (baseFolderId, segments) => { async (baseFolderId, segments) => {
@@ -2919,10 +2918,9 @@ const AppLayout = () => {
event.dataTransfer.setData('application/x-papercrate-folder', folderSelection[0]); event.dataTransfer.setData('application/x-papercrate-folder', folderSelection[0]);
} }
} }
} catch ( } catch (error) {
// eslint-disable-next-line no-empty console.warn('[documents] Failed to populate drag payload', error);
error }
) {}
if (previewNode) { if (previewNode) {
const width = previewNode.offsetWidth || 96; const width = previewNode.offsetWidth || 96;
const height = previewNode.offsetHeight || 96; const height = previewNode.offsetHeight || 96;
@@ -2993,10 +2991,9 @@ const AppLayout = () => {
JSON.stringify(effectiveDocumentSelection), JSON.stringify(effectiveDocumentSelection),
); );
} }
} catch ( } catch (error) {
// eslint-disable-next-line no-empty console.warn('[folders] Failed to set drag payload', error);
error }
) {}
const previewDocs = effectiveDocumentSelection const previewDocs = effectiveDocumentSelection
.map((id) => documentLookup.get(id) || null) .map((id) => documentLookup.get(id) || null)
@@ -3112,14 +3109,14 @@ const AppLayout = () => {
const readAllEntries = async (reader) => { const readAllEntries = async (reader) => {
const entries = []; const entries = [];
while (true) { let batch = [];
do {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
const batch = await new Promise((resolve, reject) => reader.readEntries(resolve, reject)); batch = await new Promise((resolve, reject) => reader.readEntries(resolve, reject));
if (!batch.length) { if (batch.length) {
break; entries.push(...batch);
} }
entries.push(...batch); } while (batch.length);
}
return entries; return entries;
}; };
@@ -4424,10 +4421,9 @@ const AppLayout = () => {
folderIds = parsed.filter(Boolean); folderIds = parsed.filter(Boolean);
} }
} }
} catch ( } catch (error) {
// eslint-disable-next-line no-empty console.warn('[folders] Failed to parse folder list drag payload', error);
error }
) {}
if (!folderIds.length) { if (!folderIds.length) {
let folderSourceId = draggedFolderId; let folderSourceId = draggedFolderId;
@@ -4436,10 +4432,9 @@ const AppLayout = () => {
if (event.dataTransfer.types?.includes('application/x-papercrate-folder')) { if (event.dataTransfer.types?.includes('application/x-papercrate-folder')) {
folderSourceId = event.dataTransfer.getData('application/x-papercrate-folder'); folderSourceId = event.dataTransfer.getData('application/x-papercrate-folder');
} }
} catch ( } catch (error) {
// eslint-disable-next-line no-empty console.warn('[folders] Failed to read folder id from drag payload', error);
error }
) {}
} }
if (folderSourceId) { if (folderSourceId) {
@@ -4480,21 +4475,19 @@ const AppLayout = () => {
docIds = parsed.filter(Boolean); docIds = parsed.filter(Boolean);
} }
} }
} catch ( } catch (error) {
// eslint-disable-next-line no-empty console.warn('[documents] Failed to parse document list drag payload', error);
error }
) {}
if (!docIds.length) { if (!docIds.length) {
try { try {
const single = event.dataTransfer.getData('application/x-papercrate-doc'); const single = event.dataTransfer.getData('application/x-papercrate-doc');
if (single) { if (single) {
docIds = [single]; docIds = [single];
} }
} catch ( } catch (error) {
// eslint-disable-next-line no-empty console.warn('[documents] Failed to read single document drag payload', error);
error }
) {}
} }
if (!docIds.length && draggedDocumentIds.length) { if (!docIds.length && draggedDocumentIds.length) {
@@ -4551,159 +4544,6 @@ const AppLayout = () => {
setStatusMessage, setStatusMessage,
], ],
); );
const node = folderNodes.get(folderId);
const nextExpanded = !(node?.expanded ?? false);
if (nextExpanded) {
try {
await ensureFolderData(folderId, {
includeDocuments: false,
prefetchDepth: 1,
});
} catch (error) {
notifyApiError(error, 'Failed to load folder.');
}
} else if (node && !node.loaded) {
try {
await ensureFolderData(folderId, {
includeDocuments: false,
prefetchDepth: 1,
});
} catch (error) {
notifyApiError(error, 'Failed to load folder.');
}
}
setFolderNodes((prev) => {
const next = new Map(prev);
const current = next.get(folderId);
if (!current) return prev;
next.set(folderId, { ...current, expanded: nextExpanded });
return next;
});
},
onSelect: selectFolder,
onDrop: async (event, folderId) => {
event.preventDefault();
event.stopPropagation();
event.currentTarget.classList.remove('is-drop-target');
let folderIds = [];
try {
const rawFolderList = event.dataTransfer.getData('application/x-papercrate-folder-list');
if (rawFolderList) {
const parsed = JSON.parse(rawFolderList);
if (Array.isArray(parsed)) {
folderIds = parsed.filter(Boolean);
}
}
} catch (
// eslint-disable-next-line no-empty
error
) {}
if (!folderIds.length) {
let folderSourceId = draggedFolderId;
if (!folderSourceId) {
try {
if (event.dataTransfer.types?.includes('application/x-papercrate-folder')) {
folderSourceId = event.dataTransfer.getData('application/x-papercrate-folder');
}
} catch (
// eslint-disable-next-line no-empty
error
) {}
}
if (folderSourceId) {
folderIds = [folderSourceId];
}
}
folderIds = Array.from(new Set(folderIds.filter(Boolean)));
if (folderIds.length) {
setDraggedFolderId(null);
const invalidMove = folderIds.some((sourceId) => isInvalidFolderDrop(sourceId, folderId));
if (invalidMove) {
setStatusMessage(
'Cannot move a folder into itself or one of its descendants.',
'error',
);
return;
}
for (const sourceId of folderIds) {
// eslint-disable-next-line no-await-in-loop
await moveFolder(sourceId, folderId);
}
}
if (hasFiles(event)) {
await handleFileDrop(event.dataTransfer, folderId);
return;
}
let docIds = [];
try {
const raw = event.dataTransfer.getData('application/x-papercrate-doc-list');
if (raw) {
const parsed = JSON.parse(raw);
if (Array.isArray(parsed)) {
docIds = parsed.filter(Boolean);
}
}
} catch (
// eslint-disable-next-line no-empty
error
) {}
if (!docIds.length) {
try {
const single = event.dataTransfer.getData('application/x-papercrate-doc');
if (single) {
docIds = [single];
}
} catch (
// eslint-disable-next-line no-empty
error
) {}
}
if (!docIds.length && draggedDocumentIds.length) {
docIds = draggedDocumentIds;
}
docIds = Array.from(new Set(docIds));
if (!docIds.length || folderId === selectedFolder) {
return;
}
setDraggedDocumentIds([]);
await moveDocumentsToFolder(docIds, folderId);
},
onDragOver: (event, folderId) => {
const folderDragActive = Boolean(draggedFolderId);
if (folderDragActive && isInvalidFolderDrop(draggedFolderId, folderId)) {
return;
}
if (hasFiles(event)) {
event.preventDefault();
event.dataTransfer.dropEffect = 'copy';
event.currentTarget.classList.add('is-drop-target');
return;
}
if (draggedDocumentIds.length || folderDragActive) {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
event.currentTarget.classList.add('is-drop-target');
}
},
onDragLeave: (event) => {
event.currentTarget.classList.remove('is-drop-target');
},
};
const selectedDocument = useMemo(() => { const selectedDocument = useMemo(() => {
if (!focusedDocumentId) { if (!focusedDocumentId) {
@@ -5163,7 +5003,7 @@ const AppLayout = () => {
await Promise.all([refreshTags(), refreshCorrespondents()]); await Promise.all([refreshTags(), refreshCorrespondents()]);
await loadFolder('root', { showLoading: false, preserveSearch: false }); await loadFolder('root', { showLoading: false, preserveSearch: false });
const tenantLabel = data?.tenant?.slug || data?.tenant?.id || 'tenant'; const tenantLabel = data?.tenant?.name || data?.tenant?.id || 'tenant';
setStatusMessage(`Switched to ${tenantLabel}.`, 'info'); setStatusMessage(`Switched to ${tenantLabel}.`, 'info');
} catch (error) { } catch (error) {
notifyApiError(error, 'Failed to switch tenant.'); notifyApiError(error, 'Failed to switch tenant.');
@@ -5314,7 +5154,7 @@ const AppLayout = () => {
isFilterActive, isFilterActive,
onLogout: handleLogout, onLogout: handleLogout,
status, status,
tenantSlug, tenantName,
tenants: tenantOptions, tenants: tenantOptions,
activeTenantId: currentTenantId, activeTenantId: currentTenantId,
onSelectTenant: handleTenantSelect, onSelectTenant: handleTenantSelect,
@@ -5347,7 +5187,7 @@ const AppLayout = () => {
status, status,
tags, tags,
tenantOptions, tenantOptions,
tenantSlug, tenantName,
toggleCorrespondentFilter, toggleCorrespondentFilter,
toggleTagFilter, toggleTagFilter,
], ],
+24 -26
View File
@@ -154,7 +154,7 @@ const Sidebar = ({
onLogout, onLogout,
status, status,
onCollapse, onCollapse,
tenantSlug, tenantName,
tenants = [], tenants = [],
activeTenantId = null, activeTenantId = null,
onSelectTenant, onSelectTenant,
@@ -308,7 +308,7 @@ const Sidebar = ({
> >
<span className="sidebar__title"> <span className="sidebar__title">
Papercrate Papercrate
{tenantSlug ? <span className="sidebar__tenant"> / {tenantSlug}</span> : null} {tenantName ? <span className="sidebar__tenant"> / {tenantName}</span> : null}
</span> </span>
<ChevronDownIcon <ChevronDownIcon
className={`sidebar__title-chevron${tenantMenuOpen ? ' is-open' : ''}`} className={`sidebar__title-chevron${tenantMenuOpen ? ' is-open' : ''}`}
@@ -324,9 +324,10 @@ const Sidebar = ({
tenants.map((tenant) => { tenants.map((tenant) => {
const tenantId = tenant?.id || null; const tenantId = tenant?.id || null;
const isActive = tenantId === activeTenantId; const isActive = tenantId === activeTenantId;
const tenantLabel = tenant?.name || tenant?.slug || tenantId || 'Tenant';
return ( return (
<button <button
key={tenantId || tenant?.slug || tenant?.name} key={tenantId || tenantLabel}
type="button" type="button"
className={`menu__item${isActive ? ' active' : ''}`} className={`menu__item${isActive ? ' active' : ''}`}
onClick={() => handleTenantSelect(tenant)} onClick={() => handleTenantSelect(tenant)}
@@ -335,9 +336,7 @@ const Sidebar = ({
<span className="menu__check-slot"> <span className="menu__check-slot">
{isActive ? <CheckIcon size={16} /> : null} {isActive ? <CheckIcon size={16} /> : null}
</span> </span>
<span className="menu__label"> <span className="menu__label">{tenantLabel}</span>
{tenant?.slug || tenant?.name || tenantId || 'Tenant'}
</span>
</button> </button>
); );
}) })
@@ -437,26 +436,25 @@ const Sidebar = ({
style={style || undefined} style={style || undefined}
onClick={() => handleToggleTag(tag.id)} onClick={() => handleToggleTag(tag.id)}
aria-pressed={isActive} aria-pressed={isActive}
draggable draggable
onDragStart={(event) => { onDragStart={(event) => {
try { try {
const payload = JSON.stringify({ const payload = JSON.stringify({
id: tag.id, id: tag.id,
label: tag.label, label: tag.label,
color: tag.color || null, color: tag.color || null,
}); });
event.dataTransfer.effectAllowed = 'copy'; event.dataTransfer.effectAllowed = 'copy';
event.dataTransfer.setData('application/x-papercrate-tag', payload); event.dataTransfer.setData('application/x-papercrate-tag', payload);
event.dataTransfer.setData('text/papercrate-tag', payload); event.dataTransfer.setData('text/papercrate-tag', payload);
} catch ( } catch (error) {
// eslint-disable-next-line no-empty console.warn('[sidebar] Failed to set tag drag payload', error);
error }
) {} }}
}} >
> {tag.label}
{tag.label} </button>
</button> );
);
})} })}
</div> </div>
</div> </div>
+4 -8
View File
@@ -23,15 +23,11 @@ export async function resolveOcrTextUrl({
let asset = pickAsset(docRef, getDocumentAsset); let asset = pickAsset(docRef, getDocumentAsset);
if (!asset && typeof ensurePreviewData === 'function') { if (!asset && typeof ensurePreviewData === 'function') {
try { const refreshed = await ensurePreviewData(docRef.id);
const refreshed = await ensurePreviewData(docRef.id); if (refreshed) {
if (refreshed) { docRef = refreshed;
docRef = refreshed;
}
asset = pickAsset(docRef, getDocumentAsset);
} catch (error) {
throw error;
} }
asset = pickAsset(docRef, getDocumentAsset);
} }
if (!asset) { if (!asset) {