From 6862cf5ab227e9e2776cdcce9669b445af21c1e4 Mon Sep 17 00:00:00 2001 From: Sychic <47618543+Sychic@users.noreply.github.com> Date: Thu, 23 Apr 2026 13:32:19 -0400 Subject: [PATCH] Show orgs in project card when a project is owned by an org (#5892) * fix: link to user using id instead of username * feat: show org in project card * fix: account for outdated documents * refactor: use struct to store owner information * fix: default new fields * fix lint --- ...250baa3a5f200fcf9ae990575073d0a1043e0.json | 34 ++++++++ ...bac035931a0bab8c0d0cf63888c8e5616f847.json | 28 ------- ...f4f5aa006944c05980a5b5da366135d17c912.json | 46 +++++++++++ ...120b4ae4068bd086dc08f572b33cfc2476354.json | 28 ------- apps/labrinth/src/models/v2/search.rs | 9 +++ apps/labrinth/src/search/indexing.rs | 79 ++++++++++++------- apps/labrinth/src/search/mod.rs | 12 +++ .../api-client/src/modules/labrinth/types.ts | 6 ++ .../src/layouts/shared/browse-tab/layout.vue | 12 ++- 9 files changed, 167 insertions(+), 87 deletions(-) create mode 100644 apps/labrinth/.sqlx/query-654ca85d28573de9b532d0768e5250baa3a5f200fcf9ae990575073d0a1043e0.json delete mode 100644 apps/labrinth/.sqlx/query-80734c33c16aeacca980cf40070bac035931a0bab8c0d0cf63888c8e5616f847.json create mode 100644 apps/labrinth/.sqlx/query-81b65298ddf4c58b884b15e64f1f4f5aa006944c05980a5b5da366135d17c912.json delete mode 100644 apps/labrinth/.sqlx/query-e50e308826d1e7fa54cade7daf8120b4ae4068bd086dc08f572b33cfc2476354.json diff --git a/apps/labrinth/.sqlx/query-654ca85d28573de9b532d0768e5250baa3a5f200fcf9ae990575073d0a1043e0.json b/apps/labrinth/.sqlx/query-654ca85d28573de9b532d0768e5250baa3a5f200fcf9ae990575073d0a1043e0.json new file mode 100644 index 000000000..83c41fbc4 --- /dev/null +++ b/apps/labrinth/.sqlx/query-654ca85d28573de9b532d0768e5250baa3a5f200fcf9ae990575073d0a1043e0.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT m.id mod_id, u.username, u.id uid\n FROM mods m\n INNER JOIN team_members tm ON tm.is_owner = TRUE and tm.team_id = m.team_id\n INNER JOIN users u ON u.id = tm.user_id\n WHERE m.id = ANY($1)\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "mod_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "username", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "uid", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "654ca85d28573de9b532d0768e5250baa3a5f200fcf9ae990575073d0a1043e0" +} diff --git a/apps/labrinth/.sqlx/query-80734c33c16aeacca980cf40070bac035931a0bab8c0d0cf63888c8e5616f847.json b/apps/labrinth/.sqlx/query-80734c33c16aeacca980cf40070bac035931a0bab8c0d0cf63888c8e5616f847.json deleted file mode 100644 index b874da66c..000000000 --- a/apps/labrinth/.sqlx/query-80734c33c16aeacca980cf40070bac035931a0bab8c0d0cf63888c8e5616f847.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT m.id mod_id, u.username\n FROM mods m\n INNER JOIN team_members tm ON tm.is_owner = TRUE and tm.team_id = m.team_id\n INNER JOIN users u ON u.id = tm.user_id\n WHERE m.id = ANY($1)\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "mod_id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "username", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Int8Array" - ] - }, - "nullable": [ - false, - false - ] - }, - "hash": "80734c33c16aeacca980cf40070bac035931a0bab8c0d0cf63888c8e5616f847" -} diff --git a/apps/labrinth/.sqlx/query-81b65298ddf4c58b884b15e64f1f4f5aa006944c05980a5b5da366135d17c912.json b/apps/labrinth/.sqlx/query-81b65298ddf4c58b884b15e64f1f4f5aa006944c05980a5b5da366135d17c912.json new file mode 100644 index 000000000..e91fb46af --- /dev/null +++ b/apps/labrinth/.sqlx/query-81b65298ddf4c58b884b15e64f1f4f5aa006944c05980a5b5da366135d17c912.json @@ -0,0 +1,46 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT m.id mod_id, u.username, u.id uid, o.name orgname, o.id oid\n FROM mods m\n INNER JOIN organizations o ON o.id = m.organization_id\n INNER JOIN team_members tm ON tm.is_owner = TRUE and tm.team_id = o.team_id\n INNER JOIN users u ON u.id = tm.user_id\n WHERE m.id = ANY($1)\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "mod_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "username", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "uid", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "orgname", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "oid", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [ + false, + false, + false, + false, + false + ] + }, + "hash": "81b65298ddf4c58b884b15e64f1f4f5aa006944c05980a5b5da366135d17c912" +} diff --git a/apps/labrinth/.sqlx/query-e50e308826d1e7fa54cade7daf8120b4ae4068bd086dc08f572b33cfc2476354.json b/apps/labrinth/.sqlx/query-e50e308826d1e7fa54cade7daf8120b4ae4068bd086dc08f572b33cfc2476354.json deleted file mode 100644 index dadf62968..000000000 --- a/apps/labrinth/.sqlx/query-e50e308826d1e7fa54cade7daf8120b4ae4068bd086dc08f572b33cfc2476354.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT m.id mod_id, u.username\n FROM mods m\n INNER JOIN organizations o ON o.id = m.organization_id\n INNER JOIN team_members tm ON tm.is_owner = TRUE and tm.team_id = o.team_id\n INNER JOIN users u ON u.id = tm.user_id\n WHERE m.id = ANY($1)\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "mod_id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "username", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Int8Array" - ] - }, - "nullable": [ - false, - false - ] - }, - "hash": "e50e308826d1e7fa54cade7daf8120b4ae4068bd086dc08f572b33cfc2476354" -} diff --git a/apps/labrinth/src/models/v2/search.rs b/apps/labrinth/src/models/v2/search.rs index 8b90197e7..392dac7d1 100644 --- a/apps/labrinth/src/models/v2/search.rs +++ b/apps/labrinth/src/models/v2/search.rs @@ -17,6 +17,12 @@ pub struct LegacyResultSearchProject { pub project_type: String, pub slug: Option, pub author: String, + #[serde(default)] + pub author_id: Option, + #[serde(default)] + pub organization: Option, + #[serde(default)] + pub organization_id: Option, pub title: String, pub description: String, pub categories: Vec, @@ -139,6 +145,9 @@ impl LegacyResultSearchProject { project_id: result_search_project.project_id, slug: result_search_project.slug, author: result_search_project.author, + author_id: result_search_project.author_id, + organization: result_search_project.organization, + organization_id: result_search_project.organization_id, title: result_search_project.name, description: result_search_project.summary, display_categories, diff --git a/apps/labrinth/src/search/indexing.rs b/apps/labrinth/src/search/indexing.rs index b081ce9aa..85c6408ec 100644 --- a/apps/labrinth/src/search/indexing.rs +++ b/apps/labrinth/src/search/indexing.rs @@ -15,8 +15,8 @@ use crate::database::models::loader_fields::{ VersionField, }; use crate::database::models::{ - DBProjectId, DBVersionId, LoaderFieldEnumId, LoaderFieldEnumValueId, - LoaderFieldId, + DBOrganizationId, DBProjectId, DBUserId, DBVersionId, LoaderFieldEnumId, + LoaderFieldEnumValueId, LoaderFieldId, }; use crate::database::redis::RedisPool; use crate::models::exp; @@ -34,6 +34,13 @@ fn normalize_for_search(s: &str) -> String { SPECIAL_CHARS_RE.replace_all(s, "").to_kebab_case() } +struct ProjectOwner { + username: String, + user_id: DBUserId, + org_name: Option, + org_id: Option, +} + pub async fn index_local( pool: &PgPool, redis: &RedisPool, @@ -171,9 +178,9 @@ pub async fn index_local( info!("Indexing local org owners!"); - let mods_org_owners: DashMap = sqlx::query!( + let mods_org_owners: DashMap = sqlx::query!( " - SELECT m.id mod_id, u.username + SELECT m.id mod_id, u.username, u.id uid, o.name orgname, o.id oid FROM mods m INNER JOIN organizations o ON o.id = m.organization_id INNER JOIN team_members tm ON tm.is_owner = TRUE and tm.team_id = o.team_id @@ -183,17 +190,22 @@ pub async fn index_local( &*project_ids, ) .fetch(pool) - .try_fold(DashMap::new(), |acc: DashMap, m| { - acc.insert(DBProjectId(m.mod_id), m.username); + .try_fold(DashMap::new(), |acc: DashMap, m| { + acc.insert(DBProjectId(m.mod_id), ProjectOwner { + username: m.username, + user_id: DBUserId(m.uid), + org_name: Some(m.orgname), + org_id: Some(DBOrganizationId(m.oid)), + }); async move { Ok(acc) } }) .await?; info!("Indexing local team owners!"); - let mods_team_owners: DashMap = sqlx::query!( + let mods_team_owners: DashMap = sqlx::query!( " - SELECT m.id mod_id, u.username + SELECT m.id mod_id, u.username, u.id uid FROM mods m INNER JOIN team_members tm ON tm.is_owner = TRUE and tm.team_id = m.team_id INNER JOIN users u ON u.id = tm.user_id @@ -202,8 +214,13 @@ pub async fn index_local( &project_ids, ) .fetch(pool) - .try_fold(DashMap::new(), |acc: DashMap, m| { - acc.insert(DBProjectId(m.mod_id), m.username); + .try_fold(DashMap::new(), |acc: DashMap, m| { + acc.insert(DBProjectId(m.mod_id), ProjectOwner { + username: m.username, + user_id: DBUserId(m.uid), + org_name: None, + org_id: None, + }); async move { Ok(acc) } }) .await?; @@ -262,21 +279,24 @@ pub async fn index_local( if count % 1000 == 0 { info!("projects index prog: {count}/{total_len}"); } - - let owner = - if let Some((_, org_owner)) = mods_org_owners.remove(&project.id) { - org_owner - } else if let Some((_, team_owner)) = - mods_team_owners.remove(&project.id) - { - team_owner - } else { - warn!( - "org owner not found for project {} id: {}!", - project.name, project.id.0 - ); - continue; - }; + let Some(( + _, + ProjectOwner { + username, + user_id, + org_name, + org_id, + }, + )) = mods_org_owners + .remove(&project.id) + .or_else(|| mods_team_owners.remove(&project.id)) + else { + warn!( + "org owner not found for project {} id: {}!", + project.name, project.id.0 + ); + continue; + }; let license = match project.license.split(' ').next() { Some(license) => license.to_string(), @@ -444,8 +464,13 @@ pub async fn index_local( downloads: project.downloads, log_downloads: (project.downloads.max(1) as f64).ln(), icon_url: project.icon_url.clone(), - author: owner.clone(), - indexed_author: normalize_for_search(&owner), + author: username.clone(), + author_id: ariadne::ids::UserId::from(user_id).to_string(), + organization: org_name.clone(), + organization_id: org_id.map(|e| { + crate::models::ids::OrganizationId::from(e).to_string() + }), + indexed_author: normalize_for_search(&username), date_created: project.approved, created_timestamp: project.approved.timestamp(), date_modified: project.updated, diff --git a/apps/labrinth/src/search/mod.rs b/apps/labrinth/src/search/mod.rs index 843a6519a..d5be4fc59 100644 --- a/apps/labrinth/src/search/mod.rs +++ b/apps/labrinth/src/search/mod.rs @@ -228,6 +228,9 @@ pub struct UploadSearchProject { pub project_types: Vec, pub slug: Option, pub author: String, + pub author_id: String, + pub organization: Option, + pub organization_id: Option, pub indexed_author: String, pub name: String, pub indexed_name: String, @@ -279,6 +282,12 @@ pub struct ResultSearchProject { pub project_types: Vec, pub slug: Option, pub author: String, + #[serde(default)] + pub author_id: Option, + #[serde(default)] + pub organization: Option, + #[serde(default)] + pub organization_id: Option, pub name: String, pub summary: String, pub categories: Vec, @@ -315,6 +324,9 @@ impl From for ResultSearchProject { project_types: source.project_types, slug: source.slug, author: source.author, + author_id: Some(source.author_id), + organization: source.organization, + organization_id: source.organization_id, name: source.name, summary: source.summary, categories: source.categories, diff --git a/packages/api-client/src/modules/labrinth/types.ts b/packages/api-client/src/modules/labrinth/types.ts index 2426d700d..d87a26a5f 100644 --- a/packages/api-client/src/modules/labrinth/types.ts +++ b/packages/api-client/src/modules/labrinth/types.ts @@ -1133,6 +1133,9 @@ export namespace Labrinth { project_type: string slug: string | null author: string + author_id: string | null + organization: string | null + organization_id: string | null title: string description: string categories: string[] @@ -1167,6 +1170,9 @@ export namespace Labrinth { project_types: string[] slug: string | null author: string + author_id: string | null + organization: string | null + organization_id: string | null name: string summary: string categories: string[] diff --git a/packages/ui/src/layouts/shared/browse-tab/layout.vue b/packages/ui/src/layouts/shared/browse-tab/layout.vue index 9da45a202..63e3ce4f9 100644 --- a/packages/ui/src/layouts/shared/browse-tab/layout.vue +++ b/packages/ui/src/layouts/shared/browse-tab/layout.vue @@ -190,11 +190,15 @@ const maxResultsOptions = computed[]>(() => :title="result.title" :icon-url="result.icon_url" :author="{ - name: result.author, + name: result.organization == null ? result.author : result.organization, link: - ctx.variant === 'web' - ? `/user/${result.author}` - : `https://modrinth.com/user/${result.author}`, + result.organization_id == null + ? ctx.variant === 'web' + ? `/user/${result.author_id ?? result.author}` + : `https://modrinth.com/user/${result.author_id ?? result.author}` + : ctx.variant === 'web' + ? `/organization/${result.organization_id}` + : `https://modrinth.com/organization/${result.organization_id}`, }" :date-updated="result.date_modified" :date-published="result.date_created"