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(
props.project_id,
)
for (const group of groups) {
await client.labrinth.attribution_internal.deleteGroup(group.id)
}
await client.labrinth.attribution_internal.deleteGroups(groups.map((group) => group.id))
await queryClient.invalidateQueries({ queryKey: ['project-attribution', props.project_id] })
} catch (error) {
@@ -3899,6 +3899,21 @@
"project.settings.permissions.completed.title": {
"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": {
"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,
ScaleIcon,
SearchIcon,
SpinnerIcon,
TrashIcon,
UnfoldVerticalIcon,
XCircleIcon,
} from '@modrinth/assets'
@@ -18,20 +20,22 @@ import {
Combobox,
type ComboboxOption,
commonMessages,
ConfirmModal,
createAttributionGroupTitle,
defineMessage,
defineMessages,
EmptyState,
ExternalProjectPermissionsCard,
injectModrinthClient,
injectNotificationManager,
injectProjectPageContext,
IntlFormatted,
StyledInput,
useVIntl,
} from '@modrinth/ui'
import { isStaff } from '@modrinth/utils'
import { useQuery } from '@tanstack/vue-query'
import { computed, ref, watch } from 'vue'
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, ref, useTemplateRef, watch } from 'vue'
import { setupAttributionModerationProvider } from '~/providers/setup/attribution-moderation'
@@ -47,6 +51,10 @@ const isModerator = computed(() => {
const { formatMessage } = useVIntl()
const { projectV2: project } = injectProjectPageContext()
const { labrinth } = injectModrinthClient()
const { addNotification } = injectNotificationManager()
const queryClient = useQueryClient()
const deleteAllGroupsModalRef =
useTemplateRef<InstanceType<typeof ConfirmModal>>('deleteAllGroupsModalRef')
type SortType = 'status' | 'most_files' | 'recently_edited' | 'rejected'
@@ -369,6 +377,46 @@ const messages = defineMessages({
defaultMessage:
'{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 {
@@ -412,12 +460,26 @@ function toggleAllCardsCollapsed() {
cardCollapsedById.value = next
}
function showDeleteAllGroupsConfirmation() {
deleteAllGroupsModalRef.value?.show()
}
function dismissInfoBanner() {
flags.value.dismissedExternalProjectsInfo = true
saveFeatureFlags()
}
</script>
<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
v-if="!flags.dismissedExternalProjectsInfo && !isModerator"
type="info"
@@ -500,7 +562,7 @@ function dismissInfoBanner() {
/>
</template>
<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
v-model="searchQuery"
type="text"
@@ -554,6 +616,21 @@ function dismissInfoBanner() {
{{ expandCollapseAllLabel }}
</button>
</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 v-if="isModerator" class="mt-4 flex flex-wrap items-center gap-3">