mirror of
https://github.com/modrinth/code.git
synced 2026-08-25 00:55:25 +00:00
feat: allow bulk deleting attribution groups for moderators (#6839)
* feat: allow bulk deleting attribution groups for moderators * prepr
This commit is contained in:
@@ -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">
|
||||||
|
|||||||
+3
-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"
|
||||||
}
|
}
|
||||||
+3
-3
@@ -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"
|
||||||
}
|
}
|
||||||
Generated
+22
@@ -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"
|
||||||
|
}
|
||||||
+3
-3
@@ -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")?;
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user