feat: allow bulk deleting attribution groups for moderators (#6839)

* feat: allow bulk deleting attribution groups for moderators

* prepr
This commit is contained in:
aecsocket
2026-07-24 13:42:36 +00:00
committed by GitHub
parent e58af98f21
commit a6d9bb7b3e
13 changed files with 302 additions and 46 deletions
@@ -235,10 +235,7 @@ async function clearAllGroups() {
const groups = await client.labrinth.attribution_internal.listProjectAttribution( const groups = await client.labrinth.attribution_internal.listProjectAttribution(
props.project_id, props.project_id,
) )
await client.labrinth.attribution_internal.deleteGroups(groups.map((group) => group.id))
for (const group of groups) {
await client.labrinth.attribution_internal.deleteGroup(group.id)
}
await queryClient.invalidateQueries({ queryKey: ['project-attribution', props.project_id] }) await queryClient.invalidateQueries({ queryKey: ['project-attribution', props.project_id] })
} catch (error) { } catch (error) {
@@ -3899,6 +3899,21 @@
"project.settings.permissions.completed.title": { "project.settings.permissions.completed.title": {
"message": "Permissions completed!" "message": "Permissions completed!"
}, },
"project.settings.permissions.delete-all-groups": {
"message": "Delete all groups"
},
"project.settings.permissions.delete-all-groups-confirmation.description": {
"message": "This will permanently delete all {count, plural, one {# attribution group} other {# attribution groups}} for this project and all files inside them. This action cannot be undone."
},
"project.settings.permissions.delete-all-groups-confirmation.title": {
"message": "Delete all attribution groups?"
},
"project.settings.permissions.delete-all-groups-error": {
"message": "Could not delete all attribution groups"
},
"project.settings.permissions.delete-all-groups-success": {
"message": "All attribution groups were deleted."
},
"project.settings.permissions.empty-state.description": { "project.settings.permissions.empty-state.description": {
"message": "None of your project's versions contain external content, so you don't need to worry about obtaining permissions." "message": "None of your project's versions contain external content, so you don't need to worry about obtaining permissions."
}, },
@@ -9,6 +9,8 @@ import {
RightArrowIcon, RightArrowIcon,
ScaleIcon, ScaleIcon,
SearchIcon, SearchIcon,
SpinnerIcon,
TrashIcon,
UnfoldVerticalIcon, UnfoldVerticalIcon,
XCircleIcon, XCircleIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
@@ -18,20 +20,22 @@ import {
Combobox, Combobox,
type ComboboxOption, type ComboboxOption,
commonMessages, commonMessages,
ConfirmModal,
createAttributionGroupTitle, createAttributionGroupTitle,
defineMessage, defineMessage,
defineMessages, defineMessages,
EmptyState, EmptyState,
ExternalProjectPermissionsCard, ExternalProjectPermissionsCard,
injectModrinthClient, injectModrinthClient,
injectNotificationManager,
injectProjectPageContext, injectProjectPageContext,
IntlFormatted, IntlFormatted,
StyledInput, StyledInput,
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
import { isStaff } from '@modrinth/utils' import { isStaff } from '@modrinth/utils'
import { useQuery } from '@tanstack/vue-query' import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, ref, watch } from 'vue' import { computed, ref, useTemplateRef, watch } from 'vue'
import { setupAttributionModerationProvider } from '~/providers/setup/attribution-moderation' import { setupAttributionModerationProvider } from '~/providers/setup/attribution-moderation'
@@ -47,6 +51,10 @@ const isModerator = computed(() => {
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const { projectV2: project } = injectProjectPageContext() const { projectV2: project } = injectProjectPageContext()
const { labrinth } = injectModrinthClient() const { labrinth } = injectModrinthClient()
const { addNotification } = injectNotificationManager()
const queryClient = useQueryClient()
const deleteAllGroupsModalRef =
useTemplateRef<InstanceType<typeof ConfirmModal>>('deleteAllGroupsModalRef')
type SortType = 'status' | 'most_files' | 'recently_edited' | 'rejected' type SortType = 'status' | 'most_files' | 'recently_edited' | 'rejected'
@@ -369,6 +377,46 @@ const messages = defineMessages({
defaultMessage: defaultMessage:
'{count, plural, =0 {No attributions need approval} one {# attribution needs approval} other {# attributions need approval}}', '{count, plural, =0 {No attributions need approval} one {# attribution needs approval} other {# attributions need approval}}',
}, },
deleteAllGroups: {
id: 'project.settings.permissions.delete-all-groups',
defaultMessage: 'Delete all groups',
},
deleteAllGroupsConfirmationTitle: {
id: 'project.settings.permissions.delete-all-groups-confirmation.title',
defaultMessage: 'Delete all attribution groups?',
},
deleteAllGroupsConfirmationDescription: {
id: 'project.settings.permissions.delete-all-groups-confirmation.description',
defaultMessage:
'This will permanently delete all {count, plural, one {# attribution group} other {# attribution groups}} for this project and all files inside them. This action cannot be undone.',
},
deleteAllGroupsSuccess: {
id: 'project.settings.permissions.delete-all-groups-success',
defaultMessage: 'All attribution groups were deleted.',
},
deleteAllGroupsError: {
id: 'project.settings.permissions.delete-all-groups-error',
defaultMessage: 'Could not delete all attribution groups',
},
})
const deleteAllGroupsMutation = useMutation({
mutationFn: () => labrinth.attribution_internal.deleteAllGroups(project.value.id),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ['project-attribution', project.value.id] })
addNotification({
type: 'success',
title: formatMessage(messages.deleteAllGroups),
text: formatMessage(messages.deleteAllGroupsSuccess),
})
},
onError: (error: Error) => {
addNotification({
type: 'error',
title: formatMessage(messages.deleteAllGroupsError),
text: error.message,
})
},
}) })
function defaultCardCollapsed(group: Labrinth.Attribution.Internal.AttributionGroup): boolean { function defaultCardCollapsed(group: Labrinth.Attribution.Internal.AttributionGroup): boolean {
@@ -412,12 +460,26 @@ function toggleAllCardsCollapsed() {
cardCollapsedById.value = next cardCollapsedById.value = next
} }
function showDeleteAllGroupsConfirmation() {
deleteAllGroupsModalRef.value?.show()
}
function dismissInfoBanner() { function dismissInfoBanner() {
flags.value.dismissedExternalProjectsInfo = true flags.value.dismissedExternalProjectsInfo = true
saveFeatureFlags() saveFeatureFlags()
} }
</script> </script>
<template> <template>
<ConfirmModal
v-if="isModerator"
ref="deleteAllGroupsModalRef"
:title="formatMessage(messages.deleteAllGroupsConfirmationTitle)"
:description="
formatMessage(messages.deleteAllGroupsConfirmationDescription, { count: totalGroups })
"
:proceed-label="formatMessage(messages.deleteAllGroups)"
@proceed="deleteAllGroupsMutation.mutate()"
/>
<Admonition <Admonition
v-if="!flags.dismissedExternalProjectsInfo && !isModerator" v-if="!flags.dismissedExternalProjectsInfo && !isModerator"
type="info" type="info"
@@ -500,7 +562,7 @@ function dismissInfoBanner() {
/> />
</template> </template>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="grid grid-cols-[1fr_auto_auto] gap-2"> <div class="grid grid-cols-1 gap-2 sm:grid-cols-[minmax(0,1fr)_auto_auto_auto]">
<StyledInput <StyledInput
v-model="searchQuery" v-model="searchQuery"
type="text" type="text"
@@ -554,6 +616,21 @@ function dismissInfoBanner() {
{{ expandCollapseAllLabel }} {{ expandCollapseAllLabel }}
</button> </button>
</ButtonStyled> </ButtonStyled>
<ButtonStyled v-if="isModerator" color="red" type="outlined">
<button
type="button"
class="!h-[40px]"
:disabled="deleteAllGroupsMutation.isPending.value"
@click="showDeleteAllGroupsConfirmation"
>
<SpinnerIcon
v-if="deleteAllGroupsMutation.isPending.value"
class="size-5 flex-shrink-0 animate-spin"
/>
<TrashIcon v-else class="size-5 flex-shrink-0" />
{{ formatMessage(messages.deleteAllGroups) }}
</button>
</ButtonStyled>
</div> </div>
</div> </div>
<div v-if="isModerator" class="mt-4 flex flex-wrap items-center gap-3"> <div v-if="isModerator" class="mt-4 flex flex-wrap items-center gap-3">
@@ -1,14 +1,14 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "\n\t\tDELETE FROM project_attribution_files\n\t\tWHERE group_id = $1\n\t\t", "query": "\n\t\tDELETE FROM project_attribution_files\n\t\tWHERE group_id = ANY($1)\n\t\t",
"describe": { "describe": {
"columns": [], "columns": [],
"parameters": { "parameters": {
"Left": [ "Left": [
"Int8" "Int8Array"
] ]
}, },
"nullable": [] "nullable": []
}, },
"hash": "b69317d234e934aa6bdf8be4d35bc7475308bde8b7f34a4ef3c7f3ef351f46a8" "hash": "14779c6105c15c6a117332caf850c7939af5d70562eb309680047da3c76f74fd"
} }
@@ -1,14 +1,14 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "\n\t\tDELETE FROM project_attribution_groups\n\t\tWHERE id = $1\n\t\t", "query": "\n\t\tDELETE FROM project_attribution_groups\n\t\tWHERE id = ANY($1)\n\t\t",
"describe": { "describe": {
"columns": [], "columns": [],
"parameters": { "parameters": {
"Left": [ "Left": [
"Int8" "Int8Array"
] ]
}, },
"nullable": [] "nullable": []
}, },
"hash": "827534d191739cd2ea502142a65f62a052715c7441952ee4ee182afde07c44d7" "hash": "41ec69f790366c741cea01aa7e1bfac7912b4ed17b7d19b5ba10221fa93bc3f8"
} }
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "\n\t\tSELECT id AS \"id: DBAttributionGroupId\"\n\t\tFROM project_attribution_groups\n\t\tWHERE project_id = $1\n\t\t",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id: DBAttributionGroupId",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false
]
},
"hash": "78f66d65d665210fd8334d2e1e8a793ce8128f7d6b53b3b408b030bfee4b3841"
}
@@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "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", "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 = ANY($1)\n\t\t\tAND pag.project_id = v.mod_id\n\t\t",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -11,12 +11,12 @@
], ],
"parameters": { "parameters": {
"Left": [ "Left": [
"Int8" "Int8Array"
] ]
}, },
"nullable": [ "nullable": [
false false
] ]
}, },
"hash": "00ef039799c162e21a8fa5ba7aec2a9044a3028f1cb34e8c9c565a670956d938" "hash": "bfc67441eb246ac6b155db36919a7a62e19e0dd0c87916e761175ec909add15b"
} }
@@ -13,7 +13,7 @@ use crate::database::models::{
}, },
}; };
use crate::file_hosting::FileHost; use crate::file_hosting::FileHost;
use crate::models::ids::{FileId, ProjectId, VersionId}; use crate::models::ids::{AttributionGroupId, FileId, ProjectId, VersionId};
use crate::models::pats::Scopes; use crate::models::pats::Scopes;
use crate::models::projects::{ use crate::models::projects::{
AttributionModerationStatusKind, AttributionResolution, AttributionModerationStatusKind, AttributionResolution,
@@ -31,7 +31,8 @@ use xredis::RedisPool;
pub fn config(cfg: &mut actix_web::web::ServiceConfig) { pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(list) cfg.service(list)
.service(update_group) .service(update_group)
.service(delete_group) .service(delete_groups)
.service(delete_all_groups)
.service(scan) .service(scan)
.service(force_scan_file) .service(force_scan_file)
.service(assign) .service(assign)
@@ -706,19 +707,25 @@ pub async fn update_group(
Ok(()) Ok(())
} }
/// Delete an attribution group and all files inside it. #[derive(Deserialize, utoipa::ToSchema)]
struct DeleteGroupsBody {
groups: Vec<AttributionGroupId>,
}
/// Delete attribution groups and all files inside them.
#[utoipa::path( #[utoipa::path(
context_path = "/attribution", context_path = "/attribution",
tag = "attribution", tag = "attribution",
request_body = DeleteGroupsBody,
responses((status = NO_CONTENT)) responses((status = NO_CONTENT))
)] )]
#[delete("/group/{group_id}")] #[delete("/group")]
pub async fn delete_group( pub async fn delete_groups(
req: HttpRequest, req: HttpRequest,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
redis: web::Data<RedisPool>, redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>, session_queue: web::Data<AuthQueue>,
path: web::Path<i64>, web::Json(body): web::Json<DeleteGroupsBody>,
) -> Result<(), ApiError> { ) -> Result<(), ApiError> {
check_is_moderator_from_headers( check_is_moderator_from_headers(
&req, &req,
@@ -729,11 +736,79 @@ pub async fn delete_group(
) )
.await?; .await?;
let group_ids = body
.groups
.into_iter()
.map(|id| DBAttributionGroupId::from(id).0)
.collect::<Vec<_>>();
delete_attribution_groups(pool.as_ref(), redis.as_ref(), group_ids).await
}
#[derive(Deserialize, utoipa::ToSchema)]
struct DeleteAllGroupsBody {
project_id: ProjectId,
}
/// Delete all attribution groups and files for a project.
#[utoipa::path(
context_path = "/attribution",
tag = "attribution",
request_body = DeleteAllGroupsBody,
responses((status = NO_CONTENT))
)]
#[delete("/all-groups")]
pub async fn delete_all_groups(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
web::Json(body): web::Json<DeleteAllGroupsBody>,
) -> Result<(), ApiError> {
check_is_moderator_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::PROJECT_READ,
)
.await?;
let project_id = DBProjectId::from(body.project_id).0;
let group_ids = sqlx::query_scalar!(
r#"
SELECT id AS "id: DBAttributionGroupId"
FROM project_attribution_groups
WHERE project_id = $1
"#,
project_id,
)
.fetch_all(pool.as_ref())
.await
.wrap_internal_err("failed to fetch project attribution groups")?
.into_iter()
.map(|id| id.0)
.collect::<Vec<_>>();
delete_attribution_groups(pool.as_ref(), redis.as_ref(), group_ids).await
}
async fn delete_attribution_groups(
pool: &PgPool,
redis: &RedisPool,
mut group_ids: Vec<i64>,
) -> Result<(), ApiError> {
group_ids.sort_unstable();
group_ids.dedup();
if group_ids.is_empty() {
return Ok(());
}
let mut txn = pool.begin().await.wrap_internal_err( let mut txn = pool.begin().await.wrap_internal_err(
"failed to begin attribution group deletion transaction", "failed to begin attribution group deletion transaction",
)?; )?;
let group_id = path.into_inner();
let version_ids = sqlx::query_scalar!( let version_ids = sqlx::query_scalar!(
r#" r#"
SELECT DISTINCT f.version_id AS "version_id: DBVersionId" SELECT DISTINCT f.version_id AS "version_id: DBVersionId"
@@ -742,10 +817,10 @@ pub async fn delete_group(
INNER JOIN override_file_sources ofs ON ofs.sha1 = paf.sha1 INNER JOIN override_file_sources ofs ON ofs.sha1 = paf.sha1
INNER JOIN files f ON f.id = ofs.file_id INNER JOIN files f ON f.id = ofs.file_id
INNER JOIN versions v ON v.id = f.version_id INNER JOIN versions v ON v.id = f.version_id
WHERE paf.group_id = $1 WHERE paf.group_id = ANY($1)
AND pag.project_id = v.mod_id AND pag.project_id = v.mod_id
"#, "#,
group_id, &group_ids,
) )
.fetch_all(&mut txn) .fetch_all(&mut txn)
.await .await
@@ -754,9 +829,9 @@ pub async fn delete_group(
sqlx::query!( sqlx::query!(
" "
DELETE FROM project_attribution_files DELETE FROM project_attribution_files
WHERE group_id = $1 WHERE group_id = ANY($1)
", ",
group_id, &group_ids,
) )
.execute(&mut txn) .execute(&mut txn)
.await .await
@@ -765,15 +840,15 @@ pub async fn delete_group(
let result = sqlx::query!( let result = sqlx::query!(
" "
DELETE FROM project_attribution_groups DELETE FROM project_attribution_groups
WHERE id = $1 WHERE id = ANY($1)
", ",
group_id, &group_ids,
) )
.execute(&mut txn) .execute(&mut txn)
.await .await
.wrap_internal_err("failed to delete attribution group")?; .wrap_internal_err("failed to delete attribution groups")?;
if result.rows_affected() == 0 { if result.rows_affected() != group_ids.len() as u64 {
return Err(ApiError::NotFound); return Err(ApiError::NotFound);
} }
@@ -781,7 +856,7 @@ pub async fn delete_group(
"failed to commit attribution group deletion transaction", "failed to commit attribution group deletion transaction",
)?; )?;
DBVersion::clear_cache_ids(&version_ids, redis.as_ref()) DBVersion::clear_cache_ids(&version_ids, redis)
.await .await
.wrap_internal_err("failed to clear version attribution cache")?; .wrap_internal_err("failed to clear version attribution cache")?;
+2 -1
View File
@@ -142,7 +142,8 @@ pub fn config(cfg: &mut web::ServiceConfig) {
attribution::scan, attribution::scan,
attribution::list, attribution::list,
attribution::update_group, attribution::update_group,
attribution::delete_group, attribution::delete_groups,
attribution::delete_all_groups,
attribution::assign, attribution::assign,
attribution::split, attribution::split,
billing::products, billing::products,
@@ -65,17 +65,34 @@ export class LabrinthAttributionInternalModule extends AbstractModule {
} }
/** /**
* Delete an attribution group and all files inside it. * Delete attribution groups and all files inside them.
* DELETE /_internal/attribution/group/{group_id} * DELETE /_internal/attribution/group
* *
* @param groupId - The base62 attribution group id (as returned from listProjectAttribution). * @param groupIds - The base62 attribution group ids (as returned from listProjectAttribution).
*/ */
public async deleteGroup(groupId: string): Promise<void> { public async deleteGroups(groupIds: string[]): Promise<void> {
const numericId = decodeBase62Id(groupId) const body: Labrinth.Attribution.Internal.DeleteGroupsRequest = { groups: groupIds }
return this.client.request<void>(`/attribution/group/${numericId}`, { return this.client.request<void>('/attribution/group', {
api: 'labrinth', api: 'labrinth',
version: 'internal', version: 'internal',
method: 'DELETE', method: 'DELETE',
body,
})
}
/**
* Delete all attribution groups and files for a project.
* DELETE /_internal/attribution/all-groups
*/
public async deleteAllGroups(projectId: string): Promise<void> {
const body: Labrinth.Attribution.Internal.DeleteAllGroupsRequest = {
project_id: projectId,
}
return this.client.request<void>('/attribution/all-groups', {
api: 'labrinth',
version: 'internal',
method: 'DELETE',
body,
}) })
} }
@@ -397,6 +397,14 @@ export namespace Labrinth {
attribution: AttributionResolution attribution: AttributionResolution
} }
export type DeleteGroupsRequest = {
groups: string[]
}
export type DeleteAllGroupsRequest = {
project_id: string
}
export type AssignRequest = { export type AssignRequest = {
sha1: string sha1: string
target_group_id: number target_group_id: number
@@ -18,7 +18,7 @@ import { renderString } from '@modrinth/utils'
import { useMutation, useQueryClient } from '@tanstack/vue-query' import { useMutation, useQueryClient } from '@tanstack/vue-query'
import { computed, ref, useTemplateRef, watch } from 'vue' import { computed, ref, useTemplateRef, watch } from 'vue'
import { ButtonStyled, Collapsible, OverflowMenu } from '#ui/components' import { ButtonStyled, Collapsible, ConfirmModal, OverflowMenu } from '#ui/components'
import type { OverflowMenuOption } from '#ui/components/base' import type { OverflowMenuOption } from '#ui/components/base'
import { commonMessages } from '#ui/utils' import { commonMessages } from '#ui/utils'
@@ -79,6 +79,7 @@ const addToGlobalModalRef =
useTemplateRef<typeof AddToGlobalPermissionsDatabaseModal>('addToGlobalModalRef') useTemplateRef<typeof AddToGlobalPermissionsDatabaseModal>('addToGlobalModalRef')
const addToExistingModalRef = const addToExistingModalRef =
useTemplateRef<typeof AddToExistingExternalProjectModal>('addToExistingModalRef') useTemplateRef<typeof AddToExistingExternalProjectModal>('addToExistingModalRef')
const deleteGroupModalRef = useTemplateRef<InstanceType<typeof ConfirmModal>>('deleteGroupModalRef')
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const client = injectModrinthClient() const client = injectModrinthClient()
@@ -138,7 +139,20 @@ const messages = defineMessages({
}, },
removeGroup: { removeGroup: {
id: 'external-files.permissions-card.remove-group', id: 'external-files.permissions-card.remove-group',
defaultMessage: 'Remove group', defaultMessage: 'Delete group',
},
removeGroupConfirmationTitle: {
id: 'external-files.permissions-card.remove-group-confirmation.title',
defaultMessage: 'Delete {title}?',
},
removeGroupConfirmationDescription: {
id: 'external-files.permissions-card.remove-group-confirmation.description',
defaultMessage:
'This will permanently delete this attribution group and all files inside it. This action cannot be undone.',
},
removeGroupShiftHint: {
id: 'external-files.permissions-card.remove-group-shift-hint',
defaultMessage: 'Hold Shift while clicking to skip confirmation.',
}, },
moderationReasonLabel: { moderationReasonLabel: {
id: 'external-files.permissions-card.moderation-reason', id: 'external-files.permissions-card.moderation-reason',
@@ -254,7 +268,7 @@ const splitFileMutation = useMutation({
}) })
const deleteGroupMutation = useMutation({ const deleteGroupMutation = useMutation({
mutationFn: () => client.labrinth.attribution_internal.deleteGroup(props.group.id), mutationFn: () => client.labrinth.attribution_internal.deleteGroups([props.group.id]),
onSuccess: async () => { onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ['project-attribution', props.projectId] }) await queryClient.invalidateQueries({ queryKey: ['project-attribution', props.projectId] })
emit('updated') emit('updated')
@@ -265,7 +279,7 @@ const deleteGroupMutation = useMutation({
title: formatMessage( title: formatMessage(
defineMessage({ defineMessage({
id: 'external-files.permissions-card.remove-group-error.title', id: 'external-files.permissions-card.remove-group-error.title',
defaultMessage: 'Could not remove group', defaultMessage: 'Could not delete group',
}), }),
), ),
text: error.message, text: error.message,
@@ -305,10 +319,19 @@ function handleConfirmAddFiles(sha1s: string[]) {
assignFilesMutation.mutate(sha1s) assignFilesMutation.mutate(sha1s)
} }
function handleDeleteGroup() { function deleteGroup() {
deleteGroupMutation.mutate() deleteGroupMutation.mutate()
} }
function handleDeleteGroup(event: MouseEvent) {
if (event.shiftKey) {
deleteGroup()
return
}
deleteGroupModalRef.value?.show()
}
async function handleAddFilesToGroup(event: MouseEvent) { async function handleAddFilesToGroup(event: MouseEvent) {
try { try {
const groups = await queryClient.ensureQueryData({ const groups = await queryClient.ensureQueryData({
@@ -825,6 +848,18 @@ const visibleQuickReplies = computed<OverflowMenuOption[]>(() => {
</div> </div>
</div> </div>
</Collapsible> </Collapsible>
<ConfirmModal
v-if="isModerator"
ref="deleteGroupModalRef"
:title="formatMessage(messages.removeGroupConfirmationTitle, { title })"
:description="formatMessage(messages.removeGroupConfirmationDescription)"
:proceed-label="formatMessage(messages.removeGroup)"
@proceed="deleteGroup"
>
<p class="m-0 text-xs text-secondary">
{{ formatMessage(messages.removeGroupShiftHint) }}
</p>
</ConfirmModal>
<AddFilesToAttributionGroupModal <AddFilesToAttributionGroupModal
ref="addFilesModalRef" ref="addFilesModalRef"
:group-id="group.id" :group-id="group.id"
+11 -2
View File
@@ -1194,10 +1194,19 @@
"defaultMessage": "You have obtained special permission to redistribute this work in your modpack." "defaultMessage": "You have obtained special permission to redistribute this work in your modpack."
}, },
"external-files.permissions-card.remove-group": { "external-files.permissions-card.remove-group": {
"defaultMessage": "Remove group" "defaultMessage": "Delete group"
},
"external-files.permissions-card.remove-group-confirmation.description": {
"defaultMessage": "This will permanently delete this attribution group and all files inside it. This action cannot be undone."
},
"external-files.permissions-card.remove-group-confirmation.title": {
"defaultMessage": "Delete {title}?"
}, },
"external-files.permissions-card.remove-group-error.title": { "external-files.permissions-card.remove-group-error.title": {
"defaultMessage": "Could not remove group" "defaultMessage": "Could not delete group"
},
"external-files.permissions-card.remove-group-shift-hint": {
"defaultMessage": "Hold Shift while clicking to skip confirmation."
}, },
"external-files.permissions-card.split-file": { "external-files.permissions-card.split-file": {
"defaultMessage": "Remove from group" "defaultMessage": "Remove from group"