From e1556a8ee3c2a377fa47168876454c73e8f8f1f5 Mon Sep 17 00:00:00 2001 From: aecsocket <43144841+aecsocket@users.noreply.github.com> Date: Sun, 12 Jul 2026 10:04:54 +0100 Subject: [PATCH] fix: attribution scanning details (#6651) * key override file sources on file path, allow deleting groups * dont scan modrinth files * delete attr group button * return what override files already exist on platform * intl:extract * uppercase queries * fix dupe migration key --- ...c2a9044a3028f1cb34e8c9c565a670956d938.json | 22 +++ ...b53b6526332433faf66de8716bc5cd2b12afd.json | 22 --- ...f62a052715c7441952ee4ee182afde07c44d7.json | 14 ++ ...935c4ec289b1fc6417eb0bdb2feddd94f175e.json | 46 ++++++ ...bc7475308bde8b7f34a4ef3c7f3ef351f46a8.json | 14 ++ ...f141270b3acf96ec832a50b5f7c572e255947.json | 16 ++ ...dff1a076eb7269d5b22860db3e3d1a651f6eb.json | 15 -- ...120001_override_file_sources_file_path.sql | 4 + apps/labrinth/src/queue/file_scan.rs | 80 ++-------- .../src/routes/internal/attribution.rs | 147 +++++++++++++++++- apps/labrinth/src/routes/internal/mod.rs | 1 + .../modules/labrinth/attribution/internal.ts | 15 ++ .../api-client/src/modules/labrinth/types.ts | 9 ++ .../ExternalProjectPermissionsCard.vue | 41 +++++ packages/ui/src/locales/en-US/index.json | 6 + 15 files changed, 346 insertions(+), 106 deletions(-) create mode 100644 apps/labrinth/.sqlx/query-00ef039799c162e21a8fa5ba7aec2a9044a3028f1cb34e8c9c565a670956d938.json delete mode 100644 apps/labrinth/.sqlx/query-079846a1e6a6b080e0e7f2e3efbb53b6526332433faf66de8716bc5cd2b12afd.json create mode 100644 apps/labrinth/.sqlx/query-827534d191739cd2ea502142a65f62a052715c7441952ee4ee182afde07c44d7.json create mode 100644 apps/labrinth/.sqlx/query-9a0a10cd75d0c9317ea599d21e7935c4ec289b1fc6417eb0bdb2feddd94f175e.json create mode 100644 apps/labrinth/.sqlx/query-b69317d234e934aa6bdf8be4d35bc7475308bde8b7f34a4ef3c7f3ef351f46a8.json create mode 100644 apps/labrinth/.sqlx/query-c1482e5ded6536a24937a9396a3f141270b3acf96ec832a50b5f7c572e255947.json delete mode 100644 apps/labrinth/.sqlx/query-e7a0481729efa9cba6140effcdddff1a076eb7269d5b22860db3e3d1a651f6eb.json create mode 100644 apps/labrinth/migrations/20260707120001_override_file_sources_file_path.sql diff --git a/apps/labrinth/.sqlx/query-00ef039799c162e21a8fa5ba7aec2a9044a3028f1cb34e8c9c565a670956d938.json b/apps/labrinth/.sqlx/query-00ef039799c162e21a8fa5ba7aec2a9044a3028f1cb34e8c9c565a670956d938.json new file mode 100644 index 0000000000..473c606b00 --- /dev/null +++ b/apps/labrinth/.sqlx/query-00ef039799c162e21a8fa5ba7aec2a9044a3028f1cb34e8c9c565a670956d938.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n\t\tSELECT DISTINCT f.version_id AS \"version_id: DBVersionId\"\n\t\tFROM project_attribution_files paf\n\t\tINNER JOIN project_attribution_groups pag ON pag.id = paf.group_id\n\t\tINNER JOIN override_file_sources ofs ON ofs.sha1 = paf.sha1\n\t\tINNER JOIN files f ON f.id = ofs.file_id\n\t\tINNER JOIN versions v ON v.id = f.version_id\n\t\tWHERE paf.group_id = $1\n\t\t\tAND pag.project_id = v.mod_id\n\t\t", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "version_id: DBVersionId", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false + ] + }, + "hash": "00ef039799c162e21a8fa5ba7aec2a9044a3028f1cb34e8c9c565a670956d938" +} diff --git a/apps/labrinth/.sqlx/query-079846a1e6a6b080e0e7f2e3efbb53b6526332433faf66de8716bc5cd2b12afd.json b/apps/labrinth/.sqlx/query-079846a1e6a6b080e0e7f2e3efbb53b6526332433faf66de8716bc5cd2b12afd.json deleted file mode 100644 index 6872b92825..0000000000 --- a/apps/labrinth/.sqlx/query-079846a1e6a6b080e0e7f2e3efbb53b6526332433faf66de8716bc5cd2b12afd.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n select id\n from mods\n where id = any($1)\n and status not in ('rejected', 'draft', 'withheld', 'withdrawn')\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Int8Array" - ] - }, - "nullable": [ - false - ] - }, - "hash": "079846a1e6a6b080e0e7f2e3efbb53b6526332433faf66de8716bc5cd2b12afd" -} diff --git a/apps/labrinth/.sqlx/query-827534d191739cd2ea502142a65f62a052715c7441952ee4ee182afde07c44d7.json b/apps/labrinth/.sqlx/query-827534d191739cd2ea502142a65f62a052715c7441952ee4ee182afde07c44d7.json new file mode 100644 index 0000000000..f58787791b --- /dev/null +++ b/apps/labrinth/.sqlx/query-827534d191739cd2ea502142a65f62a052715c7441952ee4ee182afde07c44d7.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "\n\t\tDELETE FROM project_attribution_groups\n\t\tWHERE id = $1\n\t\t", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + }, + "hash": "827534d191739cd2ea502142a65f62a052715c7441952ee4ee182afde07c44d7" +} diff --git a/apps/labrinth/.sqlx/query-9a0a10cd75d0c9317ea599d21e7935c4ec289b1fc6417eb0bdb2feddd94f175e.json b/apps/labrinth/.sqlx/query-9a0a10cd75d0c9317ea599d21e7935c4ec289b1fc6417eb0bdb2feddd94f175e.json new file mode 100644 index 0000000000..833096cb7f --- /dev/null +++ b/apps/labrinth/.sqlx/query-9a0a10cd75d0c9317ea599d21e7935c4ec289b1fc6417eb0bdb2feddd94f175e.json @@ -0,0 +1,46 @@ +{ + "db_name": "PostgreSQL", + "query": "\n\t\t\tSELECT\n\t\t\t\tofs.file_path AS \"file_path!\",\n\t\t\t\tCONVERT_FROM(ofs.sha1, 'UTF8') AS \"sha1!\",\n\t\t\t\tsource_file.version_id AS \"version_id: DBVersionId\",\n\t\t\t\tplatform_file.version_id AS \"platform_version_id: DBVersionId\",\n\t\t\t\tplatform_version.mod_id AS \"platform_project_id: DBProjectId\"\n\t\t\tFROM files source_file\n\t\t\tINNER JOIN versions source_version\n\t\t\t\tON source_version.id = source_file.version_id\n\t\t\tINNER JOIN override_file_sources ofs\n\t\t\t\tON ofs.file_id = source_file.id\n\t\t\tINNER JOIN hashes h\n\t\t\t\tON h.algorithm = 'sha1' AND h.hash = ofs.sha1\n\t\t\tINNER JOIN files platform_file\n\t\t\t\tON platform_file.id = h.file_id\n\t\t\tINNER JOIN versions platform_version\n\t\t\t\tON platform_version.id = platform_file.version_id\n\t\t\tWHERE source_version.mod_id = $1\n\t\t\t", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "file_path!", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "sha1!", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "version_id: DBVersionId", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "platform_version_id: DBVersionId", + "type_info": "Int8" + }, + { + "ordinal": 4, + "name": "platform_project_id: DBProjectId", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + null, + false, + false, + false + ] + }, + "hash": "9a0a10cd75d0c9317ea599d21e7935c4ec289b1fc6417eb0bdb2feddd94f175e" +} diff --git a/apps/labrinth/.sqlx/query-b69317d234e934aa6bdf8be4d35bc7475308bde8b7f34a4ef3c7f3ef351f46a8.json b/apps/labrinth/.sqlx/query-b69317d234e934aa6bdf8be4d35bc7475308bde8b7f34a4ef3c7f3ef351f46a8.json new file mode 100644 index 0000000000..cba2e4ff28 --- /dev/null +++ b/apps/labrinth/.sqlx/query-b69317d234e934aa6bdf8be4d35bc7475308bde8b7f34a4ef3c7f3ef351f46a8.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "\n\t\tDELETE FROM project_attribution_files\n\t\tWHERE group_id = $1\n\t\t", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + }, + "hash": "b69317d234e934aa6bdf8be4d35bc7475308bde8b7f34a4ef3c7f3ef351f46a8" +} diff --git a/apps/labrinth/.sqlx/query-c1482e5ded6536a24937a9396a3f141270b3acf96ec832a50b5f7c572e255947.json b/apps/labrinth/.sqlx/query-c1482e5ded6536a24937a9396a3f141270b3acf96ec832a50b5f7c572e255947.json new file mode 100644 index 0000000000..45aa8d29be --- /dev/null +++ b/apps/labrinth/.sqlx/query-c1482e5ded6536a24937a9396a3f141270b3acf96ec832a50b5f7c572e255947.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO override_file_sources (sha1, file_id, file_path)\n SELECT source.sha1, $2, source.file_path\n FROM UNNEST($1::BYTEA[], $3::TEXT[]) AS source(sha1, file_path)\n ON CONFLICT DO NOTHING\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "ByteaArray", + "Int8", + "TextArray" + ] + }, + "nullable": [] + }, + "hash": "c1482e5ded6536a24937a9396a3f141270b3acf96ec832a50b5f7c572e255947" +} diff --git a/apps/labrinth/.sqlx/query-e7a0481729efa9cba6140effcdddff1a076eb7269d5b22860db3e3d1a651f6eb.json b/apps/labrinth/.sqlx/query-e7a0481729efa9cba6140effcdddff1a076eb7269d5b22860db3e3d1a651f6eb.json deleted file mode 100644 index 0b7cf69e9c..0000000000 --- a/apps/labrinth/.sqlx/query-e7a0481729efa9cba6140effcdddff1a076eb7269d5b22860db3e3d1a651f6eb.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n insert into override_file_sources (sha1, file_id)\n select unnest($1::bytea[]), $2\n on conflict do nothing\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "ByteaArray", - "Int8" - ] - }, - "nullable": [] - }, - "hash": "e7a0481729efa9cba6140effcdddff1a076eb7269d5b22860db3e3d1a651f6eb" -} diff --git a/apps/labrinth/migrations/20260707120001_override_file_sources_file_path.sql b/apps/labrinth/migrations/20260707120001_override_file_sources_file_path.sql new file mode 100644 index 0000000000..c0481941df --- /dev/null +++ b/apps/labrinth/migrations/20260707120001_override_file_sources_file_path.sql @@ -0,0 +1,4 @@ +alter table override_file_sources + add column file_path text not null default '', + drop constraint override_file_sources_pkey, + add primary key (sha1, file_id, file_path); diff --git a/apps/labrinth/src/queue/file_scan.rs b/apps/labrinth/src/queue/file_scan.rs index b655b64967..b054c4579e 100644 --- a/apps/labrinth/src/queue/file_scan.rs +++ b/apps/labrinth/src/queue/file_scan.rs @@ -231,7 +231,7 @@ async fn scan_pending_files_chunk( .await .wrap_err("beginning file scan transaction")?; - let resolved = resolve_overrides(&overrides, redis, &mut txn) + let resolved = resolve_overrides(&overrides, &mut txn) .await .wrap_err_with(|| { eyre!("resolving overrides for file {file_id:?}") @@ -325,9 +325,8 @@ async fn scan_file_inner( if !overrides.is_empty() { let before = count_project_attributions(project_id, txn).await?; - let resolved = resolve_overrides(&overrides, redis, txn) - .await - .wrap_err_with(|| { + let resolved = + resolve_overrides(&overrides, txn).await.wrap_err_with(|| { eyre!("resolving overrides for file {file_id:?}") })?; @@ -494,7 +493,6 @@ pub struct OverrideFile { #[derive(Debug)] pub enum OverrideResolution { - OnModrinth, ExternalLicense { id: i64, status: ApprovalType, @@ -518,8 +516,7 @@ fn log_marked_override_projects( | OverrideResolution::Flame(flame_project) => { Some(format!("{} ({})", flame_project.title, flame_project.id)) } - OverrideResolution::OnModrinth - | OverrideResolution::ExternalLicense { .. } + OverrideResolution::ExternalLicense { .. } | OverrideResolution::Unknown => None, }) .collect::>(); @@ -671,7 +668,6 @@ async fn persist_attribution_results( } match resolved.get(&file.sha1) { - Some(OverrideResolution::OnModrinth) => continue, Some(OverrideResolution::ExternalLicense { id, status, @@ -864,14 +860,19 @@ async fn persist_attribution_results( } if !all_sha1s.is_empty() { + let file_paths: Vec = + overrides.iter().map(|f| f.path.clone()).collect(); + sqlx::query!( " - insert into override_file_sources (sha1, file_id) - select unnest($1::bytea[]), $2 - on conflict do nothing + INSERT INTO override_file_sources (sha1, file_id, file_path) + SELECT source.sha1, $2, source.file_path + FROM UNNEST($1::BYTEA[], $3::TEXT[]) AS source(sha1, file_path) + ON CONFLICT DO NOTHING ", &all_sha1s, file_id as DBFileId, + &file_paths, ) .execute(&mut *txn) .await @@ -937,7 +938,6 @@ fn default_external_license_attribution( async fn resolve_overrides( overrides: &[OverrideFile], - redis: &RedisPool, txn: &mut PgTransaction<'_>, ) -> Result> { let mut results: HashMap = HashMap::new(); @@ -947,62 +947,6 @@ async fn resolve_overrides( return Ok(results); } - let hashes: Vec = - overrides.iter().map(|x| x.sha1.clone()).collect(); - let files = DBVersion::get_files_from_hash( - "sha1".to_string(), - &hashes, - &mut *txn, - redis, - ) - .await - .wrap_err("fetching files on platform by hash")?; - - let matching_project_ids: Vec<_> = - files.iter().map(|file| file.project_id.0).collect(); - let valid_project_ids = sqlx::query_scalar!( - r#" - select id - from mods - where id = any($1) - and status not in ('rejected', 'draft', 'withheld', 'withdrawn') - "#, - &matching_project_ids, - ) - .fetch_all(&mut *txn) - .await - .wrap_err("fetching matched file project statuses")?; - - let version_ids: Vec<_> = files.iter().map(|x| x.version_id).collect(); - let versions_data = DBVersion::get_many(&version_ids, &mut *txn, redis) - .await - .wrap_err("fetching versions")?; - - for file in &files { - if !valid_project_ids.contains(&file.project_id.0) { - continue; - } - - if !versions_data.iter().any(|v| v.inner.id == file.version_id) { - continue; - } - - if let Some(hash) = file.hashes.get("sha1") - && let Some(pos) = - remaining.iter().position(|i| overrides[*i].sha1 == *hash) - { - let idx = remaining.remove(pos); - results.insert( - overrides[idx].sha1.clone(), - OverrideResolution::OnModrinth, - ); - } - } - - if remaining.is_empty() { - return Ok(results); - } - let rows = sqlx::query!( " SELECT encode(mef.sha1, 'escape') sha1, mel.id, mel.status status, mel.link diff --git a/apps/labrinth/src/routes/internal/attribution.rs b/apps/labrinth/src/routes/internal/attribution.rs index bf783fbbeb..33ecc0e314 100644 --- a/apps/labrinth/src/routes/internal/attribution.rs +++ b/apps/labrinth/src/routes/internal/attribution.rs @@ -1,4 +1,4 @@ -use actix_web::{HttpRequest, get, patch, post, web}; +use actix_web::{HttpRequest, delete, get, patch, post, web}; use chrono::{DateTime, Utc}; use eyre::eyre; use serde::{Deserialize, Serialize}; @@ -31,6 +31,7 @@ use crate::util::error::Context; pub fn config(cfg: &mut actix_web::web::ServiceConfig) { cfg.service(list) .service(update_group) + .service(delete_group) .service(scan) .service(force_scan_file) .service(assign) @@ -46,6 +47,7 @@ struct AttributionGroupResponse { attributed_by: Option, files: Vec, versions: Vec, + override_files_on_platform: Vec, } #[derive(Clone, Serialize, utoipa::ToSchema)] @@ -67,6 +69,15 @@ struct AttributionFileResponse { moderation_external_license: Option, } +#[derive(Clone, Serialize, utoipa::ToSchema)] +struct OverrideFileOnPlatformResponse { + file_path: String, + sha1: String, + version_id: VersionId, + platform_version_id: VersionId, + platform_project_id: ProjectId, +} + #[derive(Clone, Serialize, utoipa::ToSchema)] struct ModerationExternalLicenseResponse { id: i64, @@ -358,6 +369,48 @@ pub async fn list( .wrap_internal_err("failed to fetch attribution group files")? }; + let override_files_on_platform = if requester_is_mod { + sqlx::query!( + r#" + SELECT + ofs.file_path AS "file_path!", + CONVERT_FROM(ofs.sha1, 'UTF8') AS "sha1!", + source_file.version_id AS "version_id: DBVersionId", + platform_file.version_id AS "platform_version_id: DBVersionId", + platform_version.mod_id AS "platform_project_id: DBProjectId" + FROM files source_file + INNER JOIN versions source_version + ON source_version.id = source_file.version_id + INNER JOIN override_file_sources ofs + ON ofs.file_id = source_file.id + INNER JOIN hashes h + ON h.algorithm = 'sha1' AND h.hash = ofs.sha1 + INNER JOIN files platform_file + ON platform_file.id = h.file_id + INNER JOIN versions platform_version + ON platform_version.id = platform_file.version_id + WHERE source_version.mod_id = $1 + "#, + project_id as DBProjectId, + ) + .fetch_all(pool.as_ref()) + .await + .wrap_internal_err( + "failed to fetch override files already on platform", + )? + .into_iter() + .map(|row| OverrideFileOnPlatformResponse { + file_path: row.file_path, + sha1: row.sha1, + version_id: row.version_id.into(), + platform_version_id: row.platform_version_id.into(), + platform_project_id: row.platform_project_id.into(), + }) + .collect::>() + } else { + Vec::new() + }; + let moderation_external_licenses = if requester_is_mod { let mut ids: Vec = files .iter() @@ -497,6 +550,15 @@ pub async fn list( .filter(|version| group_version_ids.contains(&version.id)) .cloned() .collect(); + let group_file_sha1s = group_files + .iter() + .map(|file| file.sha1.as_str()) + .collect::>(); + let group_override_files_on_platform = override_files_on_platform + .iter() + .filter(|file| group_file_sha1s.contains(file.sha1.as_str())) + .cloned() + .collect(); let mut attribution = group.attribution.and_then(|v| { serde_json::from_value::(v).ok() @@ -530,6 +592,7 @@ pub async fn list( attributed_by, files: group_files, versions: group_versions, + override_files_on_platform: group_override_files_on_platform, }); } @@ -620,6 +683,88 @@ pub async fn update_group( Ok(()) } +/// Delete an attribution group and all files inside it. +#[utoipa::path( + context_path = "/attribution", + tag = "attribution", + responses((status = NO_CONTENT)) +)] +#[delete("/group/{group_id}")] +pub async fn delete_group( + req: HttpRequest, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, + path: web::Path, +) -> Result<(), ApiError> { + check_is_moderator_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Scopes::PROJECT_READ, + ) + .await?; + + let mut txn = pool.begin().await.wrap_internal_err( + "failed to begin attribution group deletion transaction", + )?; + + let group_id = path.into_inner(); + let version_ids = sqlx::query_scalar!( + r#" + SELECT DISTINCT f.version_id AS "version_id: DBVersionId" + FROM project_attribution_files paf + INNER JOIN project_attribution_groups pag ON pag.id = paf.group_id + INNER JOIN override_file_sources ofs ON ofs.sha1 = paf.sha1 + INNER JOIN files f ON f.id = ofs.file_id + INNER JOIN versions v ON v.id = f.version_id + WHERE paf.group_id = $1 + AND pag.project_id = v.mod_id + "#, + group_id, + ) + .fetch_all(&mut txn) + .await + .wrap_internal_err("failed to fetch attribution group versions")?; + + sqlx::query!( + " + DELETE FROM project_attribution_files + WHERE group_id = $1 + ", + group_id, + ) + .execute(&mut txn) + .await + .wrap_internal_err("failed to delete attribution group files")?; + + let result = sqlx::query!( + " + DELETE FROM project_attribution_groups + WHERE id = $1 + ", + group_id, + ) + .execute(&mut txn) + .await + .wrap_internal_err("failed to delete attribution group")?; + + if result.rows_affected() == 0 { + return Err(ApiError::NotFound); + } + + txn.commit().await.wrap_internal_err( + "failed to commit attribution group deletion transaction", + )?; + + DBVersion::clear_cache_ids(&version_ids, redis.as_ref()) + .await + .wrap_internal_err("failed to clear version attribution cache")?; + + Ok(()) +} + #[derive(Deserialize, utoipa::ToSchema)] struct AssignBody { sha1: String, diff --git a/apps/labrinth/src/routes/internal/mod.rs b/apps/labrinth/src/routes/internal/mod.rs index c1061ead40..8ab03e1d12 100644 --- a/apps/labrinth/src/routes/internal/mod.rs +++ b/apps/labrinth/src/routes/internal/mod.rs @@ -142,6 +142,7 @@ pub fn config(cfg: &mut web::ServiceConfig) { attribution::scan, attribution::list, attribution::update_group, + attribution::delete_group, attribution::assign, attribution::split, billing::products, diff --git a/packages/api-client/src/modules/labrinth/attribution/internal.ts b/packages/api-client/src/modules/labrinth/attribution/internal.ts index 1ee316ecf6..d431d2144c 100644 --- a/packages/api-client/src/modules/labrinth/attribution/internal.ts +++ b/packages/api-client/src/modules/labrinth/attribution/internal.ts @@ -64,6 +64,21 @@ export class LabrinthAttributionInternalModule extends AbstractModule { }) } + /** + * Delete an attribution group and all files inside it. + * DELETE /_internal/attribution/group/{group_id} + * + * @param groupId - The base62 attribution group id (as returned from listProjectAttribution). + */ + public async deleteGroup(groupId: string): Promise { + const numericId = decodeBase62Id(groupId) + return this.client.request(`/attribution/group/${numericId}`, { + api: 'labrinth', + version: 'internal', + method: 'DELETE', + }) + } + /** * Reassign a file (by sha1) to another attribution group within the same project. * POST /_internal/attribution/assign diff --git a/packages/api-client/src/modules/labrinth/types.ts b/packages/api-client/src/modules/labrinth/types.ts index 4826422e56..b4c0f120ad 100644 --- a/packages/api-client/src/modules/labrinth/types.ts +++ b/packages/api-client/src/modules/labrinth/types.ts @@ -373,6 +373,14 @@ export namespace Labrinth { date_created: string } + export type OverrideFileOnPlatform = { + file_path: string + sha1: string + version_id: string + platform_version_id: string + platform_project_id: string + } + export type AttributionGroup = { id: string flame_project: FlameProject | null @@ -381,6 +389,7 @@ export namespace Labrinth { attributed_by: string | null files: AttributionFile[] versions: AttributionVersionInfo[] + override_files_on_platform: OverrideFileOnPlatform[] } export type UpdateGroupRequest = { diff --git a/packages/ui/src/components/external_files/ExternalProjectPermissionsCard.vue b/packages/ui/src/components/external_files/ExternalProjectPermissionsCard.vue index a2bd339065..aa413d115e 100644 --- a/packages/ui/src/components/external_files/ExternalProjectPermissionsCard.vue +++ b/packages/ui/src/components/external_files/ExternalProjectPermissionsCard.vue @@ -9,6 +9,7 @@ import { ReportIcon, ScaleIcon, SpinnerIcon, + TrashIcon, VersionIcon, XCircleIcon, XIcon, @@ -135,6 +136,10 @@ const messages = defineMessages({ id: 'external-files.permissions-card.add-files-to-group', defaultMessage: 'Add files...', }, + removeGroup: { + id: 'external-files.permissions-card.remove-group', + defaultMessage: 'Remove group', + }, moderationReasonLabel: { id: 'external-files.permissions-card.moderation-reason', defaultMessage: 'Reason', @@ -248,6 +253,26 @@ const splitFileMutation = useMutation({ }, }) +const deleteGroupMutation = useMutation({ + mutationFn: () => client.labrinth.attribution_internal.deleteGroup(props.group.id), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ['project-attribution', props.projectId] }) + emit('updated') + }, + onError: (error: Error) => { + addNotification({ + type: 'error', + title: formatMessage( + defineMessage({ + id: 'external-files.permissions-card.remove-group-error.title', + defaultMessage: 'Could not remove group', + }), + ), + text: error.message, + }) + }, +}) + function startEditingAttribution() { editingMode.value = 'attribution' collapsed.value = false @@ -280,6 +305,10 @@ function handleConfirmAddFiles(sha1s: string[]) { assignFilesMutation.mutate(sha1s) } +function handleDeleteGroup() { + deleteGroupMutation.mutate() +} + async function handleAddFilesToGroup(event: MouseEvent) { try { const groups = await queryClient.ensureQueryData({ @@ -782,6 +811,18 @@ const visibleQuickReplies = computed(() => { @cancel="stopEditing" /> +
+ + + +