linting, tenant slug -> name
This commit is contained in:
@@ -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
@@ -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();
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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();
|
||||||
|
|||||||
@@ -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!(
|
||||||
|
|||||||
@@ -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
@@ -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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
}
|
||||||
) {}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user