You've already forked AstralRinth
feat: new proj moderation page (#6044)
* feat: new proj moderation page * make requested changes * add boolean for showing delay message * fix server icon + shortened code * fix server icon * refactor admonitions * msg correction. * correction + change spam-notice * Separate status info from instruction details * Tweak timing delay msg, thread activity warning, and refer to moderation with consistent terms. * Whoops, actually updated msgs correctly now. * prepr + margin * split out strings, simplify code again * fix: a few more moderation fixes (#6048) * fix: move tooltip to button * fix: lock status buttons after pressing * fix: unlisted/withheld icon on legacy badge * prepprrr * fix banners, add some extra dev mode stuff * fix thread id copy padding * tweak: adjust some of the status change messages (#6041) * update messages & bunch of other stuff * rename toggle * change hover to 2.5, fix error size * private msg overlay --------- Co-authored-by: coolbot100s <76798835+coolbot100s@users.noreply.github.com>
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { BlueskyIcon, DiscordIcon, GithubIcon, MastodonIcon, TwitterIcon } from '@modrinth/assets'
|
||||
import {
|
||||
BlueskyIcon,
|
||||
DiscordIcon,
|
||||
GithubIcon,
|
||||
MastodonIcon,
|
||||
ToggleRightIcon,
|
||||
TwitterIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
AutoLink,
|
||||
ButtonStyled,
|
||||
@@ -10,6 +17,7 @@ import {
|
||||
type MessageDescriptor,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { commonSettingsMessages } from '@modrinth/ui/src/utils/common-messages.js'
|
||||
|
||||
import TextLogo from '~/components/brand/TextLogo.vue'
|
||||
|
||||
@@ -233,11 +241,21 @@ function developerModeIncrement() {
|
||||
role="region"
|
||||
:aria-label="formatMessage(messages.modrinthInformation)"
|
||||
>
|
||||
<TextLogo
|
||||
aria-hidden="true"
|
||||
class="text-logo button-base h-6 w-auto text-contrast lg:h-8"
|
||||
@click="developerModeIncrement()"
|
||||
/>
|
||||
<div class="flex items-center gap-2">
|
||||
<TextLogo
|
||||
aria-hidden="true"
|
||||
class="text-logo button-base h-6 w-auto text-contrast lg:h-8"
|
||||
@click="developerModeIncrement()"
|
||||
/>
|
||||
<ButtonStyled v-if="flags.developerMode" circular type="transparent" color="brand">
|
||||
<nuxt-link
|
||||
v-tooltip="formatMessage(commonSettingsMessages.featureFlags)"
|
||||
to="/settings/flags"
|
||||
>
|
||||
<ToggleRightIcon />
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div class="flex flex-wrap justify-center gap-px sm:-mx-2">
|
||||
<ButtonStyled
|
||||
v-for="(social, index) in socialLinks"
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { defineMessages, PagewideBanner, useVIntl } from '@modrinth/ui'
|
||||
import { XCircleIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, defineMessages, PagewideBanner, useVIntl } from '@modrinth/ui'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const flags = useFeatureFlags()
|
||||
|
||||
const tempIgnored = ref(false)
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
@@ -13,16 +17,34 @@ const messages = defineMessages({
|
||||
defaultMessage:
|
||||
"This deploy of Modrinth's frontend failed to generate state from the API. This may be due to an outage or an error in configuration. Rebuild when the API is available. Error codes: {errors}; Current API URL is: {url}",
|
||||
},
|
||||
ignoreErrors: {
|
||||
id: 'layout.banner.build-fail.ignore',
|
||||
defaultMessage: 'Ignore',
|
||||
},
|
||||
alwaysIgnore: {
|
||||
id: 'layout.banner.build-fail.always-ignore',
|
||||
defaultMessage: 'Always ignore',
|
||||
},
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
errors: any[] | undefined
|
||||
apiUrl: string
|
||||
}>()
|
||||
|
||||
function alwaysIgnoreBanner() {
|
||||
flags.value.alwaysIgnoreErrorBanner = true
|
||||
saveFeatureFlags()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PagewideBanner v-if="errors?.length" variant="error">
|
||||
<PagewideBanner
|
||||
v-if="
|
||||
flags.showAllBanners || (errors?.length && !tempIgnored && !flags.alwaysIgnoreErrorBanner)
|
||||
"
|
||||
variant="error"
|
||||
>
|
||||
<template #title>
|
||||
<span>{{ formatMessage(messages.title) }}</span>
|
||||
</template>
|
||||
@@ -34,5 +56,19 @@ defineProps<{
|
||||
})
|
||||
}}
|
||||
</template>
|
||||
<template #actions_right>
|
||||
<ButtonStyled color="red" type="transparent" hover-color-fill="background">
|
||||
<button @click="alwaysIgnoreBanner">
|
||||
<XCircleIcon />
|
||||
{{ formatMessage(messages.alwaysIgnore) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="red">
|
||||
<button @click="tempIgnored = true">
|
||||
<XIcon />
|
||||
{{ formatMessage(messages.ignoreErrors) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</PagewideBanner>
|
||||
</template>
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
const { formatMessage } = useVIntl()
|
||||
const flags = useFeatureFlags()
|
||||
const config = useRuntimeConfig()
|
||||
const route = useRoute()
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
@@ -21,7 +22,7 @@ const messages = defineMessages({
|
||||
},
|
||||
description: {
|
||||
id: 'layout.banner.preview.description',
|
||||
defaultMessage: `If you meant to access the official Modrinth website, visit <link>https://modrinth.com</link>. This preview deploy is used by Modrinth staff for testing purposes. It was built using <branch-link>{owner}/{branch}</branch-link> @ {commit}.`,
|
||||
defaultMessage: `If you meant to access the official Modrinth website, visit {url}. This preview deploy is used by Modrinth staff for testing purposes. It was built using <branch-link>{owner}/{branch}</branch-link> @ {commit}.`,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -29,10 +30,12 @@ function hidePreviewBanner() {
|
||||
flags.value.hidePreviewBanner = true
|
||||
saveFeatureFlags()
|
||||
}
|
||||
|
||||
const url = computed(() => `https://modrinth.com${route.fullPath}`)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PagewideBanner v-if="!flags.hidePreviewBanner" variant="info">
|
||||
<PagewideBanner v-if="!flags.hidePreviewBanner || flags.showAllBanners" variant="info">
|
||||
<template #title>
|
||||
<span>{{ formatMessage(messages.title) }}</span>
|
||||
</template>
|
||||
@@ -45,9 +48,9 @@ function hidePreviewBanner() {
|
||||
branch: config.public.branch,
|
||||
}"
|
||||
>
|
||||
<template #link="{ children }">
|
||||
<a href="https://modrinth.com" target="_blank" rel="noopener" class="text-link">
|
||||
<component :is="() => normalizeChildren(children)" />
|
||||
<template #url>
|
||||
<a :href="url" target="_blank" rel="noopener" class="text-link">
|
||||
{{ url }}
|
||||
</a>
|
||||
</template>
|
||||
<template #branch-link="{ children }">
|
||||
@@ -75,7 +78,7 @@ function hidePreviewBanner() {
|
||||
</IntlFormatted>
|
||||
</span>
|
||||
</template>
|
||||
<template #actions_right>
|
||||
<template #actions_top_right>
|
||||
<ButtonStyled type="transparent" circular>
|
||||
<button :aria-label="formatMessage(commonMessages.closeButton)" @click="hidePreviewBanner">
|
||||
<XIcon aria-hidden="true" />
|
||||
|
||||
@@ -47,7 +47,7 @@ function hideRussiaCensorshipBanner() {
|
||||
<span class="text-xs font-medium">(Перевод на русский)</span>
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<ButtonStyled type="transparent" hover-color-fill="background">
|
||||
<nuxt-link to="/news/article/standing-by-our-values">
|
||||
<BookTextIcon /> Read our full statement
|
||||
<span class="text-xs font-medium">(English)</span>
|
||||
@@ -55,7 +55,7 @@ function hideRussiaCensorshipBanner() {
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions_right>
|
||||
<template #actions_top_right>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<button
|
||||
v-tooltip="formatMessage(commonMessages.closeButton)"
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const cosmetics = useCosmetics()
|
||||
const flags = useFeatureFlags()
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
@@ -29,14 +30,14 @@ function hideStagingBanner() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PagewideBanner v-if="!cosmetics.hideStagingBanner" variant="warning">
|
||||
<PagewideBanner v-if="flags.showAllBanners || !cosmetics.hideStagingBanner" variant="warning">
|
||||
<template #title>
|
||||
<span>{{ formatMessage(messages.title) }}</span>
|
||||
</template>
|
||||
<template #description>
|
||||
{{ formatMessage(messages.description) }}
|
||||
</template>
|
||||
<template #actions_right>
|
||||
<template #actions_top_right>
|
||||
<ButtonStyled type="transparent" circular>
|
||||
<button :aria-label="formatMessage(commonMessages.closeButton)" @click="hideStagingBanner">
|
||||
<XIcon aria-hidden="true" />
|
||||
|
||||
@@ -29,8 +29,8 @@ const messages = defineMessages({
|
||||
<template #description>
|
||||
<span>{{ formatMessage(messages.description) }}</span>
|
||||
</template>
|
||||
<template #actions>
|
||||
<ButtonStyled>
|
||||
<template #actions_right>
|
||||
<ButtonStyled color="red">
|
||||
<nuxt-link to="/settings/billing">
|
||||
<SettingsIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.action) }}
|
||||
|
||||
@@ -55,7 +55,7 @@ function openTaxForm(e: MouseEvent) {
|
||||
formatMessage(messages.description, { threshold: formatMoney(taxThreshold) })
|
||||
}}</span>
|
||||
</template>
|
||||
<template #actions>
|
||||
<template #actions_right>
|
||||
<ButtonStyled color="orange">
|
||||
<button @click="openTaxForm"><FileTextIcon /> {{ formatMessage(messages.action) }}</button>
|
||||
</ButtonStyled>
|
||||
|
||||
@@ -29,7 +29,7 @@ const messages = defineMessages({
|
||||
<template #description>
|
||||
<span>{{ formatMessage(messages.description) }}</span>
|
||||
</template>
|
||||
<template #actions>
|
||||
<template #actions_right>
|
||||
<div class="flex w-fit flex-row">
|
||||
<ButtonStyled color="red">
|
||||
<nuxt-link to="https://support.modrinth.com" target="_blank" rel="noopener">
|
||||
|
||||
@@ -96,14 +96,12 @@ async function handleResendEmailVerification() {
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
<template #actions>
|
||||
<ButtonStyled v-if="hasEmail">
|
||||
<button @click="handleResendEmailVerification">
|
||||
<template #actions_right>
|
||||
<ButtonStyled color="orange">
|
||||
<button v-if="hasEmail" @click="handleResendEmailVerification">
|
||||
{{ formatMessage(verifyEmailBannerMessages.action) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else>
|
||||
<nuxt-link to="/settings/account">
|
||||
<nuxt-link v-else to="/settings/account">
|
||||
<SettingsIcon aria-hidden="true" />
|
||||
{{ formatMessage(addEmailBannerMessages.action) }}
|
||||
</nuxt-link>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import { PagewideBanner } from '@modrinth/ui'
|
||||
|
||||
const flags = useFeatureFlags()
|
||||
const route = useRoute()
|
||||
|
||||
const url = computed(() => `https://modrinth.com${route.fullPath}`)
|
||||
|
||||
const bannerRoot = ref<HTMLElement | null>(null)
|
||||
|
||||
function onProdLinkClick(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
const el = bannerRoot.value
|
||||
if (el) {
|
||||
const { height } = el.getBoundingClientRect()
|
||||
window.scrollBy({ top: Math.ceil(height), behavior: 'auto' })
|
||||
}
|
||||
window.open(url.value, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="flags.showViewProdRouteBanner || flags.showAllBanners" ref="bannerRoot">
|
||||
<PagewideBanner variant="info" slim>
|
||||
<template #description>
|
||||
<span>
|
||||
View route on production:
|
||||
<a
|
||||
:href="url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-link"
|
||||
@click="onProdLinkClick"
|
||||
>
|
||||
{{ url }}
|
||||
</a>
|
||||
</span>
|
||||
</template>
|
||||
</PagewideBanner>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="shadow-card rounded-2xl border border-solid border-surface-5 bg-surface-3 p-4">
|
||||
<div class="shadow-card rounded-2xl border border-solid border-surface-4 bg-surface-3 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<NuxtLink
|
||||
@@ -107,8 +107,8 @@
|
||||
<LinkIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-tooltip="'Begin review'" circular color="orange">
|
||||
<button @click="openProjectForReview">
|
||||
<ButtonStyled circular color="orange">
|
||||
<button v-tooltip="'Begin review'" @click="openProjectForReview">
|
||||
<ScaleIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
@@ -143,7 +143,7 @@
|
||||
:expand-text="expandText"
|
||||
collapse-text="Collapse thread"
|
||||
>
|
||||
<div class="bg-surface-2 p-4 pt-2">
|
||||
<div class="bg-surface-2 pt-2">
|
||||
<ThreadView
|
||||
v-if="threadWithReportBody"
|
||||
ref="reportThread"
|
||||
|
||||
@@ -1075,6 +1075,7 @@ async function handleSubmitReview(verdict: 'safe' | 'unsafe') {
|
||||
mode="local"
|
||||
:links="navTabsLinks"
|
||||
:active-index="activeTabIndex"
|
||||
class="bg-surface-3! shadow-none!"
|
||||
@tab-click="handleTabClick"
|
||||
/>
|
||||
</div>
|
||||
@@ -1087,7 +1088,7 @@ async function handleSubmitReview(verdict: 'safe' | 'unsafe') {
|
||||
collapse-text="Collapse thread"
|
||||
class="border-x border-b border-solid border-surface-3"
|
||||
>
|
||||
<div class="bg-surface-2 p-4 pt-0">
|
||||
<div class="bg-surface-2 pt-0">
|
||||
<!-- DEV-531 -->
|
||||
<!-- @vue-expect-error TODO: will convert ThreadView to use api-client types at a later date -->
|
||||
<ThreadView
|
||||
|
||||
@@ -358,26 +358,44 @@
|
||||
|
||||
<div v-else-if="generatedMessage" class="flex items-center gap-2">
|
||||
<ButtonStyled>
|
||||
<button @click="goBackToStages">
|
||||
<button :disabled="loadingModerationDecision" @click="goBackToStages">
|
||||
<LeftArrowIcon aria-hidden="true" />
|
||||
Edit
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="red">
|
||||
<button @click="sendMessage('rejected')">
|
||||
<XIcon aria-hidden="true" />
|
||||
<button :disabled="loadingModerationDecision" @click="sendMessage('rejected')">
|
||||
<SpinnerIcon
|
||||
v-if="moderationDecision === 'rejected'"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<XIcon v-else aria-hidden="true" />
|
||||
Reject
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="orange">
|
||||
<button @click="sendMessage('withheld')">
|
||||
<EyeOffIcon aria-hidden="true" />
|
||||
<button :disabled="loadingModerationDecision" @click="sendMessage('withheld')">
|
||||
<SpinnerIcon
|
||||
v-if="moderationDecision === 'withheld'"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<LinkIcon v-else aria-hidden="true" />
|
||||
Withhold
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="green">
|
||||
<button @click="sendMessage(projectV2.requested_status ?? 'approved')">
|
||||
<CheckIcon aria-hidden="true" />
|
||||
<button
|
||||
:disabled="loadingModerationDecision"
|
||||
@click="sendMessage(approveSendStatus)"
|
||||
>
|
||||
<SpinnerIcon
|
||||
v-if="moderationDecision === approveSendStatus"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<CheckIcon v-else aria-hidden="true" />
|
||||
Approve
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
@@ -428,14 +446,15 @@ import {
|
||||
BrushCleaningIcon,
|
||||
CheckIcon,
|
||||
DropdownIcon,
|
||||
EyeOffIcon,
|
||||
FileTextIcon,
|
||||
KeyboardIcon,
|
||||
LeftArrowIcon,
|
||||
LinkIcon,
|
||||
ListBulletedIcon,
|
||||
LockIcon,
|
||||
RightArrowIcon,
|
||||
ScaleIcon,
|
||||
SpinnerIcon,
|
||||
ToggleLeftIcon,
|
||||
ToggleRightIcon,
|
||||
XIcon,
|
||||
@@ -760,6 +779,12 @@ const message = ref(
|
||||
)
|
||||
const generatedMessage = ref(persistedGeneratedMessage.generated === true)
|
||||
const loadingMessage = ref(false)
|
||||
const moderationDecision = ref<ProjectStatus | null>(null)
|
||||
const loadingModerationDecision = computed(() => moderationDecision.value !== null)
|
||||
const approveSendStatus = computed<ProjectStatus>(() => {
|
||||
const requested = projectV2.value.requested_status
|
||||
return requested ?? 'approved'
|
||||
})
|
||||
const done = ref(false)
|
||||
|
||||
function persistGeneratedMessageState() {
|
||||
@@ -1074,6 +1099,7 @@ function resetProgress() {
|
||||
done.value = false
|
||||
clearGeneratedMessageState()
|
||||
loadingMessage.value = false
|
||||
moderationDecision.value = null
|
||||
|
||||
localStorage.removeItem(`modpack-permissions-${projectV2.value.id}`)
|
||||
localStorage.removeItem(`modpack-permissions-index-${projectV2.value.id}`)
|
||||
@@ -1190,7 +1216,7 @@ function handleKeybinds(event: KeyboardEvent) {
|
||||
tryResetProgress: resetProgress,
|
||||
tryExitModeration: handleExit,
|
||||
|
||||
tryApprove: () => sendMessage(projectV2.value.requested_status ?? 'approved'),
|
||||
tryApprove: () => sendMessage(approveSendStatus.value),
|
||||
tryReject: () => sendMessage('rejected'),
|
||||
tryWithhold: () => sendMessage('withheld'),
|
||||
tryEditMessage: goBackToStages,
|
||||
@@ -1977,6 +2003,7 @@ async function sendMessage(status: ProjectStatus) {
|
||||
return
|
||||
}
|
||||
|
||||
moderationDecision.value = status
|
||||
try {
|
||||
await useBaseFetch(`project/${projectId}`, {
|
||||
method: 'PATCH',
|
||||
@@ -2026,6 +2053,8 @@ async function sendMessage(status: ProjectStatus) {
|
||||
text: 'Failed to submit moderation decision. Please try again.',
|
||||
type: 'error',
|
||||
})
|
||||
} finally {
|
||||
moderationDecision.value = null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,31 @@
|
||||
<template>
|
||||
<div>
|
||||
<section class="universal-card">
|
||||
<section>
|
||||
<Breadcrumbs
|
||||
v-if="breadcrumbsStack"
|
||||
:current-title="`Report ${reportId}`"
|
||||
:link-stack="breadcrumbsStack"
|
||||
/>
|
||||
<h2>Report details</h2>
|
||||
<ReportInfo :report="report" :show-thread="false" :show-message="false" :auth="auth" />
|
||||
<ReportInfo
|
||||
:report="report"
|
||||
:show-thread="false"
|
||||
:show-message="false"
|
||||
:auth="auth"
|
||||
class="card-shadow mb-4 rounded-2xl border border-solid border-surface-4 bg-surface-2 p-4"
|
||||
/>
|
||||
</section>
|
||||
<section v-if="report && thread" class="universal-card">
|
||||
<h2>Messages</h2>
|
||||
<section
|
||||
v-if="report && thread"
|
||||
class="card-shadow rounded-2xl border border-solid border-surface-4 bg-surface-3"
|
||||
>
|
||||
<h2 class="m-4 mb-2 text-xl font-semibold text-contrast">Messages with the moderators</h2>
|
||||
<p class="mx-4 mt-0">
|
||||
Make sure to include evidence of all claims you make, or your report may be closed without
|
||||
action.
|
||||
</p>
|
||||
<ConversationThread
|
||||
class="overflow-clip rounded-b-2xl border-0 border-t border-solid border-surface-4 bg-surface-2"
|
||||
:thread="thread"
|
||||
:report="report"
|
||||
:auth="auth"
|
||||
|
||||
@@ -1,28 +1,45 @@
|
||||
<template>
|
||||
<div>
|
||||
<Modal
|
||||
<NewModal
|
||||
ref="modalSubmit"
|
||||
:header="isRejected(project) ? 'Resubmit for review' : 'Submit for review'"
|
||||
:header="
|
||||
formatMessage(
|
||||
isRejected(project)
|
||||
? messages.resubmitModalHeaderResubmitting
|
||||
: messages.resubmitModalHeaderSubmitting,
|
||||
)
|
||||
"
|
||||
>
|
||||
<div class="modal-submit universal-body">
|
||||
<span>
|
||||
You're submitting <span class="project-title">{{ project.title }}</span> to be reviewed
|
||||
again by the moderators.
|
||||
</span>
|
||||
<span>
|
||||
Make sure you have addressed the comments from the moderation team.
|
||||
<span class="known-errors">
|
||||
Repeated submissions without addressing the moderators' comments may result in an
|
||||
account suspension.
|
||||
</span>
|
||||
</span>
|
||||
<div class="flex max-w-[35rem] flex-col gap-3">
|
||||
<p class="m-0">
|
||||
<IntlFormatted
|
||||
:message-id="messages.resubmitModalDescription"
|
||||
:message-values="{ projectTitle: project.title }"
|
||||
>
|
||||
<template #project-title="{ children }">
|
||||
<span class="font-semibold text-contrast">
|
||||
<component :is="() => children" />
|
||||
</span>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</p>
|
||||
<p class="m-0">{{ formatMessage(messages.resubmitModalReminder) }}</p>
|
||||
<p class="m-0 font-semibold text-red">
|
||||
{{ formatMessage(messages.resubmitModalWarning) }}
|
||||
</p>
|
||||
<Checkbox
|
||||
v-model="submissionConfirmation"
|
||||
description="Confirm I have addressed the messages from the moderators"
|
||||
:description="formatMessage(messages.resubmitModalConfirmationDescription)"
|
||||
>
|
||||
I confirm that I have properly addressed the moderators' comments.
|
||||
{{ formatMessage(messages.resubmitModalConfirmationLabel) }}
|
||||
</Checkbox>
|
||||
<div class="input-group push-right">
|
||||
<div class="flex flex-wrap items-center justify-end gap-2">
|
||||
<ButtonStyled type="outlined">
|
||||
<button @click="modalSubmit.hide()">
|
||||
<XIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="orange">
|
||||
<button
|
||||
:disabled="!submissionConfirmation || isLoading"
|
||||
@@ -34,33 +51,37 @@
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<ScaleIcon v-else aria-hidden="true" />
|
||||
Resubmit for review
|
||||
{{ formatMessage(messages.actionResubmitForReview) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal ref="modalReply" header="Reply to thread">
|
||||
<div class="modal-submit universal-body">
|
||||
<span>
|
||||
Your project is already approved. As such, the moderation team does not actively monitor
|
||||
this thread. However, they may still see your message if there is a problem with your
|
||||
project.
|
||||
</span>
|
||||
<span>
|
||||
If you need to get in contact with the moderation team, please use the
|
||||
<a class="text-link" href="https://support.modrinth.com" target="_blank">
|
||||
Modrinth Help Center
|
||||
</a>
|
||||
and click the green bubble to contact support.
|
||||
</span>
|
||||
</NewModal>
|
||||
<NewModal ref="modalReply" :header="formatMessage(messages.replyModalHeader)">
|
||||
<div class="flex max-w-[45rem] flex-col gap-3">
|
||||
<p class="m-0">{{ formatMessage(messages.replyModalDescription) }}</p>
|
||||
<p class="m-0">
|
||||
<IntlFormatted :message-id="messages.replyModalHelpCenterNote">
|
||||
<template #help-center-link="{ children }">
|
||||
<a class="text-link" href="https://support.modrinth.com" target="_blank">
|
||||
<component :is="() => children" />
|
||||
</a>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</p>
|
||||
<Checkbox
|
||||
v-model="replyConfirmation"
|
||||
description="Confirm moderators do not actively monitor this"
|
||||
:description="formatMessage(messages.replyModalConfirmationDescription)"
|
||||
>
|
||||
I acknowledge that the moderators do not actively monitor the thread.
|
||||
{{ formatMessage(messages.replyModalConfirmationLabel) }}
|
||||
</Checkbox>
|
||||
<div class="input-group push-right">
|
||||
<div class="flex flex-wrap items-center justify-end gap-2">
|
||||
<ButtonStyled type="outlined">
|
||||
<button @click="modalReply.hide()">
|
||||
<XIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
:disabled="!replyConfirmation || isLoading"
|
||||
@@ -72,289 +93,318 @@
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<ReplyIcon v-else aria-hidden="true" />
|
||||
Reply to thread
|
||||
{{ formatMessage(messages.actionReplyToThread) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<div v-if="flags.developerMode" class="thread-id">
|
||||
</NewModal>
|
||||
<div v-if="flags.developerMode" class="mx-4 mb-3 font-semibold">
|
||||
Thread ID:
|
||||
<CopyCode :text="thread.id" />
|
||||
</div>
|
||||
<div v-if="sortedMessages.length > 0" class="messages universal-card recessed">
|
||||
<ThreadMessage
|
||||
v-for="message in sortedMessages"
|
||||
:key="'message-' + message.id"
|
||||
:thread="thread"
|
||||
:message="message"
|
||||
:members="members"
|
||||
:report="report"
|
||||
:auth="auth"
|
||||
raised
|
||||
@update-thread="() => updateThreadLocal()"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="report && report.closed">
|
||||
<p>This thread is closed and new messages cannot be sent to it.</p>
|
||||
<ButtonStyled v-if="isStaff(auth.user)">
|
||||
<button :disabled="isLoading" @click="runBlockingAction('reopen', () => reopenReport())">
|
||||
<SpinnerIcon v-if="loadingAction === 'reopen'" class="animate-spin" aria-hidden="true" />
|
||||
<CheckCircleIcon v-else aria-hidden="true" />
|
||||
Reopen thread
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
<template v-else-if="!report || !report.closed">
|
||||
<div class="markdown-editor-spacing">
|
||||
<MarkdownEditor
|
||||
v-model="replyBody"
|
||||
:placeholder="sortedMessages.length > 0 ? 'Reply to thread...' : 'Send a message...'"
|
||||
:on-image-upload="onUploadImage"
|
||||
<div v-bind="$attrs" class="flex flex-col">
|
||||
<div v-if="sortedMessages.length > 0" class="flex flex-col pt-2">
|
||||
<ThreadMessage
|
||||
v-for="message in sortedMessages"
|
||||
:key="'message-' + message.id"
|
||||
:thread="thread"
|
||||
:message="message"
|
||||
:members="members"
|
||||
:report="report"
|
||||
:auth="auth"
|
||||
raised
|
||||
@update-thread="() => updateThreadLocal()"
|
||||
/>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
v-if="sortedMessages.length > 0"
|
||||
:disabled="!replyBody || isLoading"
|
||||
@click="
|
||||
isApproved(project) && !isStaff(auth.user)
|
||||
? openReplyModal()
|
||||
: runBlockingAction('reply', () => sendReply())
|
||||
"
|
||||
>
|
||||
<SpinnerIcon v-if="loadingAction === 'reply'" class="animate-spin" aria-hidden="true" />
|
||||
<ReplyIcon v-else aria-hidden="true" />
|
||||
Reply
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
:disabled="!replyBody || isLoading"
|
||||
@click="
|
||||
isApproved(project) && !isStaff(auth.user)
|
||||
? openReplyModal()
|
||||
: runBlockingAction('send', () => sendReply())
|
||||
"
|
||||
>
|
||||
<SpinnerIcon v-if="loadingAction === 'send'" class="animate-spin" aria-hidden="true" />
|
||||
<SendIcon v-else aria-hidden="true" />
|
||||
Send
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<template v-if="report && report.closed">
|
||||
<p>{{ formatMessage(messages.closedThreadDescription) }}</p>
|
||||
<ButtonStyled v-if="isStaff(auth.user)">
|
||||
<button
|
||||
:disabled="!replyBody || isLoading"
|
||||
@click="runBlockingAction('private-note', () => sendReply(null, true))"
|
||||
>
|
||||
<button :disabled="isLoading" @click="runBlockingAction('reopen', () => reopenReport())">
|
||||
<SpinnerIcon
|
||||
v-if="loadingAction === 'private-note'"
|
||||
v-if="loadingAction === 'reopen'"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<ScaleIcon v-else aria-hidden="true" />
|
||||
Add private note
|
||||
<CheckCircleIcon v-else aria-hidden="true" />
|
||||
{{ formatMessage(messages.actionReopenThread) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<template v-if="currentMember && !isStaff(auth.user)">
|
||||
<template v-if="isRejected(project)">
|
||||
<ButtonStyled color="orange">
|
||||
<button v-if="replyBody" :disabled="isLoading" @click="openResubmitModal(true)">
|
||||
<ScaleIcon aria-hidden="true" />
|
||||
Resubmit for review with reply
|
||||
</template>
|
||||
<template v-else-if="!report || !report.closed">
|
||||
<div class="mx-4 mb-2 mt-2">
|
||||
<MarkdownEditor
|
||||
v-model="replyBody"
|
||||
:placeholder="
|
||||
formatMessage(
|
||||
sortedMessages.length > 0
|
||||
? messages.replyEditorPlaceholderReply
|
||||
: messages.replyEditorPlaceholderSend,
|
||||
)
|
||||
"
|
||||
:on-image-upload="onUploadImage"
|
||||
/>
|
||||
</div>
|
||||
<div class="m-4 mt-3 flex flex-wrap items-center justify-between gap-4">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
v-if="sortedMessages.length > 0"
|
||||
:disabled="!replyBody || isLoading"
|
||||
@click="
|
||||
isApproved(project)
|
||||
? openReplyModal()
|
||||
: runBlockingAction('reply', () => sendReply())
|
||||
"
|
||||
>
|
||||
<SpinnerIcon
|
||||
v-if="loadingAction === 'reply'"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<ReplyIcon v-else aria-hidden="true" />
|
||||
{{ formatMessage(messages.actionReply) }}
|
||||
</button>
|
||||
<button v-else :disabled="isLoading" @click="openResubmitModal(false)">
|
||||
<ScaleIcon aria-hidden="true" />
|
||||
Resubmit for review
|
||||
<button
|
||||
v-else
|
||||
:disabled="!replyBody || isLoading"
|
||||
@click="
|
||||
isApproved(project)
|
||||
? openReplyModal()
|
||||
: runBlockingAction('send', () => sendReply())
|
||||
"
|
||||
>
|
||||
<SpinnerIcon
|
||||
v-if="loadingAction === 'send'"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<SendIcon v-else aria-hidden="true" />
|
||||
{{ formatMessage(messages.actionSend) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</template>
|
||||
<div class="spacer"></div>
|
||||
<div class="input-group extra-options">
|
||||
<template v-if="report">
|
||||
<template v-if="isStaff(auth.user)">
|
||||
<ButtonStyled color="red">
|
||||
<button
|
||||
v-if="replyBody"
|
||||
:disabled="isLoading"
|
||||
@click="runBlockingAction('close-with-reply', () => closeReport(true))"
|
||||
>
|
||||
<SpinnerIcon
|
||||
v-if="loadingAction === 'close-with-reply'"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<CheckCircleIcon v-else aria-hidden="true" />
|
||||
Close with reply
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
:disabled="isLoading"
|
||||
@click="runBlockingAction('close', () => closeReport())"
|
||||
>
|
||||
<SpinnerIcon
|
||||
v-if="loadingAction === 'close'"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<CheckCircleIcon v-else aria-hidden="true" />
|
||||
Close thread
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="isStaff(auth.user)">
|
||||
<button
|
||||
:disabled="!replyBody || isLoading"
|
||||
@click="runBlockingAction('private-note', () => sendReply(null, true))"
|
||||
>
|
||||
<SpinnerIcon
|
||||
v-if="loadingAction === 'private-note'"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<ScaleIcon v-else aria-hidden="true" />
|
||||
{{ formatMessage(messages.actionAddPrivateNote) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<template v-if="currentMember && !currentMember.staffOnly">
|
||||
<template v-if="isRejected(project)">
|
||||
<ButtonStyled color="orange">
|
||||
<button v-if="replyBody" :disabled="isLoading" @click="openResubmitModal(true)">
|
||||
<ScaleIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.actionResubmitForReviewWithReply) }}
|
||||
</button>
|
||||
<button v-else :disabled="isLoading" @click="openResubmitModal(false)">
|
||||
<ScaleIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.actionResubmitForReview) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="project">
|
||||
<template v-if="isStaff(auth.user)">
|
||||
<ButtonStyled v-if="replyBody" color="green">
|
||||
<button
|
||||
:disabled="isApproved(project) || isLoading"
|
||||
@click="runBlockingAction('approve-with-reply', () => sendReply(requestedStatus))"
|
||||
>
|
||||
<SpinnerIcon
|
||||
v-if="loadingAction === 'approve-with-reply'"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<CheckIcon v-else aria-hidden="true" />
|
||||
Approve with reply
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else color="green">
|
||||
<button
|
||||
:disabled="isApproved(project) || isLoading"
|
||||
@click="runBlockingAction('approve', () => setStatus(requestedStatus))"
|
||||
>
|
||||
<SpinnerIcon
|
||||
v-if="loadingAction === 'approve'"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<CheckIcon v-else aria-hidden="true" />
|
||||
Approve
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<div class="joined-buttons">
|
||||
<ButtonStyled v-if="replyBody" color="red">
|
||||
<button
|
||||
:disabled="project.status === 'rejected' || isLoading"
|
||||
@click="runBlockingAction('reject-with-reply', () => sendReply('rejected'))"
|
||||
>
|
||||
<SpinnerIcon
|
||||
v-if="loadingAction === 'reject-with-reply'"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<XIcon v-else aria-hidden="true" />
|
||||
Reject with reply
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else color="red">
|
||||
<button
|
||||
:disabled="project.status === 'rejected' || isLoading"
|
||||
@click="runBlockingAction('reject', () => setStatus('rejected'))"
|
||||
>
|
||||
<SpinnerIcon
|
||||
v-if="loadingAction === 'reject'"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<XIcon v-else aria-hidden="true" />
|
||||
Reject
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<template v-if="report">
|
||||
<template v-if="isStaff(auth.user)">
|
||||
<ButtonStyled color="red">
|
||||
<OverflowMenu
|
||||
class="btn-dropdown-animation"
|
||||
<button
|
||||
v-if="replyBody"
|
||||
:disabled="isLoading"
|
||||
:options="
|
||||
replyBody
|
||||
? [
|
||||
{
|
||||
id: 'withhold-reply',
|
||||
color: 'danger',
|
||||
action: () =>
|
||||
runBlockingAction('withhold-reply', () => sendReply('withheld')),
|
||||
hoverFilled: true,
|
||||
disabled: project.status === 'withheld' || isLoading,
|
||||
},
|
||||
{
|
||||
id: 'set-to-draft-reply',
|
||||
action: () =>
|
||||
runBlockingAction('set-to-draft-reply', () => sendReply('draft')),
|
||||
hoverFilled: true,
|
||||
disabled: project.status === 'draft' || isLoading,
|
||||
},
|
||||
{
|
||||
id: 'send-to-review-reply',
|
||||
action: () =>
|
||||
runBlockingAction('send-to-review-reply', () =>
|
||||
sendReply('processing', true),
|
||||
),
|
||||
hoverFilled: true,
|
||||
disabled: project.status === 'processing' || isLoading,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
id: 'withhold',
|
||||
color: 'danger',
|
||||
action: () =>
|
||||
runBlockingAction('withhold', () => setStatus('withheld')),
|
||||
hoverFilled: true,
|
||||
disabled: project.status === 'withheld' || isLoading,
|
||||
},
|
||||
{
|
||||
id: 'set-to-draft',
|
||||
action: () =>
|
||||
runBlockingAction('set-to-draft', () => setStatus('draft')),
|
||||
hoverFilled: true,
|
||||
disabled: project.status === 'draft' || isLoading,
|
||||
},
|
||||
{
|
||||
id: 'send-to-review',
|
||||
action: () =>
|
||||
runBlockingAction('send-to-review', () => setStatus('processing')),
|
||||
hoverFilled: true,
|
||||
disabled: project.status === 'processing' || isLoading,
|
||||
},
|
||||
]
|
||||
@click="runBlockingAction('close-with-reply', () => closeReport(true))"
|
||||
>
|
||||
<SpinnerIcon
|
||||
v-if="loadingAction === 'close-with-reply'"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<CheckCircleIcon v-else aria-hidden="true" />
|
||||
{{ formatMessage(messages.actionCloseWithReply) }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
:disabled="isLoading"
|
||||
@click="runBlockingAction('close', () => closeReport())"
|
||||
>
|
||||
<SpinnerIcon
|
||||
v-if="loadingAction === 'close'"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<CheckCircleIcon v-else aria-hidden="true" />
|
||||
{{ formatMessage(messages.actionCloseThread) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="project">
|
||||
<template v-if="isStaff(auth.user)">
|
||||
<ButtonStyled v-if="replyBody" color="green">
|
||||
<button
|
||||
:disabled="isApproved(project) || isLoading"
|
||||
@click="
|
||||
runBlockingAction('approve-with-reply', () => sendReply(requestedStatus))
|
||||
"
|
||||
>
|
||||
<SpinnerIcon v-if="isDropdownLoading" class="animate-spin" aria-hidden="true" />
|
||||
<DropdownIcon v-else aria-hidden="true" />
|
||||
<template #withhold-reply>
|
||||
<EyeOffIcon aria-hidden="true" />
|
||||
Withhold with reply
|
||||
</template>
|
||||
<template #withhold>
|
||||
<EyeOffIcon aria-hidden="true" />
|
||||
Withhold
|
||||
</template>
|
||||
<template #set-to-draft-reply>
|
||||
<FileTextIcon aria-hidden="true" />
|
||||
Set to draft with reply
|
||||
</template>
|
||||
<template #set-to-draft>
|
||||
<FileTextIcon aria-hidden="true" />
|
||||
Set to draft
|
||||
</template>
|
||||
<template #send-to-review-reply>
|
||||
<ScaleIcon aria-hidden="true" />
|
||||
Send to review with reply
|
||||
</template>
|
||||
<template #send-to-review>
|
||||
<ScaleIcon aria-hidden="true" />
|
||||
Send to review
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
<SpinnerIcon
|
||||
v-if="loadingAction === 'approve-with-reply'"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<CheckIcon v-else aria-hidden="true" />
|
||||
{{ formatMessage(messages.actionApproveWithReply) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<ButtonStyled v-else color="green">
|
||||
<button
|
||||
:disabled="isApproved(project) || isLoading"
|
||||
@click="runBlockingAction('approve', () => setStatus(requestedStatus))"
|
||||
>
|
||||
<SpinnerIcon
|
||||
v-if="loadingAction === 'approve'"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<CheckIcon v-else aria-hidden="true" />
|
||||
{{ formatMessage(messages.actionApprove) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<div class="joined-buttons">
|
||||
<ButtonStyled v-if="replyBody" color="red">
|
||||
<button
|
||||
:disabled="project.status === 'rejected' || isLoading"
|
||||
@click="runBlockingAction('reject-with-reply', () => sendReply('rejected'))"
|
||||
>
|
||||
<SpinnerIcon
|
||||
v-if="loadingAction === 'reject-with-reply'"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<XIcon v-else aria-hidden="true" />
|
||||
{{ formatMessage(messages.actionRejectWithReply) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else color="red">
|
||||
<button
|
||||
:disabled="project.status === 'rejected' || isLoading"
|
||||
@click="runBlockingAction('reject', () => setStatus('rejected'))"
|
||||
>
|
||||
<SpinnerIcon
|
||||
v-if="loadingAction === 'reject'"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<XIcon v-else aria-hidden="true" />
|
||||
{{ formatMessage(messages.actionReject) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="red">
|
||||
<OverflowMenu
|
||||
class="btn-dropdown-animation"
|
||||
:disabled="isLoading"
|
||||
:options="
|
||||
replyBody
|
||||
? [
|
||||
{
|
||||
id: 'withhold-reply',
|
||||
color: 'danger',
|
||||
action: () =>
|
||||
runBlockingAction('withhold-reply', () => sendReply('withheld')),
|
||||
hoverFilled: true,
|
||||
disabled: project.status === 'withheld' || isLoading,
|
||||
},
|
||||
{
|
||||
id: 'set-to-draft-reply',
|
||||
action: () =>
|
||||
runBlockingAction('set-to-draft-reply', () => sendReply('draft')),
|
||||
hoverFilled: true,
|
||||
disabled: project.status === 'draft' || isLoading,
|
||||
},
|
||||
{
|
||||
id: 'send-to-review-reply',
|
||||
action: () =>
|
||||
runBlockingAction('send-to-review-reply', () =>
|
||||
sendReply('processing', true),
|
||||
),
|
||||
hoverFilled: true,
|
||||
disabled: project.status === 'processing' || isLoading,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
id: 'withhold',
|
||||
color: 'danger',
|
||||
action: () =>
|
||||
runBlockingAction('withhold', () => setStatus('withheld')),
|
||||
hoverFilled: true,
|
||||
disabled: project.status === 'withheld' || isLoading,
|
||||
},
|
||||
{
|
||||
id: 'set-to-draft',
|
||||
action: () =>
|
||||
runBlockingAction('set-to-draft', () => setStatus('draft')),
|
||||
hoverFilled: true,
|
||||
disabled: project.status === 'draft' || isLoading,
|
||||
},
|
||||
{
|
||||
id: 'send-to-review',
|
||||
action: () =>
|
||||
runBlockingAction('send-to-review', () =>
|
||||
setStatus('processing'),
|
||||
),
|
||||
hoverFilled: true,
|
||||
disabled: project.status === 'processing' || isLoading,
|
||||
},
|
||||
]
|
||||
"
|
||||
>
|
||||
<SpinnerIcon
|
||||
v-if="isDropdownLoading"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<DropdownIcon v-else aria-hidden="true" />
|
||||
<template #withhold-reply>
|
||||
<EyeOffIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.actionWithholdWithReply) }}
|
||||
</template>
|
||||
<template #withhold>
|
||||
<EyeOffIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.actionWithhold) }}
|
||||
</template>
|
||||
<template #set-to-draft-reply>
|
||||
<FileTextIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.actionSetToDraftWithReply) }}
|
||||
</template>
|
||||
<template #set-to-draft>
|
||||
<FileTextIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.actionSetToDraft) }}
|
||||
</template>
|
||||
<template #send-to-review-reply>
|
||||
<ScaleIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.actionSendToReviewWithReply) }}
|
||||
</template>
|
||||
<template #send-to-review>
|
||||
<ScaleIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.actionSendToReview) }}
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -374,19 +424,179 @@ import {
|
||||
import {
|
||||
ButtonStyled,
|
||||
Checkbox,
|
||||
commonMessages,
|
||||
CopyCode,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
IntlFormatted,
|
||||
MarkdownEditor,
|
||||
NewModal,
|
||||
OverflowMenu,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
|
||||
import Modal from '~/components/ui/Modal.vue'
|
||||
import ThreadMessage from '~/components/ui/thread/ThreadMessage.vue'
|
||||
import { useImageUpload } from '~/composables/image-upload.ts'
|
||||
import { isApproved, isRejected } from '~/helpers/projects.js'
|
||||
import { isStaff } from '~/helpers/users.js'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
resubmitModalHeaderResubmitting: {
|
||||
id: 'conversation-thread.resubmit-modal.header.resubmitting',
|
||||
defaultMessage: 'Resubmitting for review',
|
||||
},
|
||||
resubmitModalHeaderSubmitting: {
|
||||
id: 'conversation-thread.resubmit-modal.header.submitting',
|
||||
defaultMessage: 'Submitting for review',
|
||||
},
|
||||
resubmitModalDescription: {
|
||||
id: 'conversation-thread.resubmit-modal.description',
|
||||
defaultMessage:
|
||||
"You're submitting <project-title>{projectTitle}</project-title> to be reviewed again by the moderators.",
|
||||
},
|
||||
resubmitModalReminder: {
|
||||
id: 'conversation-thread.resubmit-modal.reminder',
|
||||
defaultMessage: 'Make sure you have addressed all the comments from the moderation team.',
|
||||
},
|
||||
resubmitModalWarning: {
|
||||
id: 'conversation-thread.resubmit-modal.warning',
|
||||
defaultMessage:
|
||||
"Repeated submissions without addressing the moderators' comments may result in an account suspension.",
|
||||
},
|
||||
resubmitModalConfirmationDescription: {
|
||||
id: 'conversation-thread.resubmit-modal.confirmation.description',
|
||||
defaultMessage: 'Confirm I have addressed the messages from the moderators',
|
||||
},
|
||||
resubmitModalConfirmationLabel: {
|
||||
id: 'conversation-thread.resubmit-modal.confirmation.label',
|
||||
defaultMessage: "I confirm that I have properly addressed the moderators' comments.",
|
||||
},
|
||||
replyModalHeader: {
|
||||
id: 'conversation-thread.reply-modal.header',
|
||||
defaultMessage: 'Reply to thread',
|
||||
},
|
||||
replyModalDescription: {
|
||||
id: 'conversation-thread.reply-modal.description',
|
||||
defaultMessage:
|
||||
'Your project is already approved. As such, the moderation team does not actively monitor this thread. However, they may still see your message if there is a problem with your project.',
|
||||
},
|
||||
replyModalHelpCenterNote: {
|
||||
id: 'conversation-thread.reply-modal.help-center-note',
|
||||
defaultMessage:
|
||||
'If you need to get in contact with the moderation team, please use the <help-center-link>Modrinth Help Center</help-center-link> and click the blue bubble in the bottom right corner to contact support.',
|
||||
},
|
||||
replyModalConfirmationDescription: {
|
||||
id: 'conversation-thread.reply-modal.confirmation.description',
|
||||
defaultMessage: 'Confirm moderators do not actively monitor this',
|
||||
},
|
||||
replyModalConfirmationLabel: {
|
||||
id: 'conversation-thread.reply-modal.confirmation.label',
|
||||
defaultMessage: 'I acknowledge that the moderators do not actively monitor the thread.',
|
||||
},
|
||||
closedThreadDescription: {
|
||||
id: 'conversation-thread.closed-thread.description',
|
||||
defaultMessage: 'This thread is closed and new messages cannot be sent to it.',
|
||||
},
|
||||
replyEditorPlaceholderReply: {
|
||||
id: 'conversation-thread.reply-editor.placeholder.reply',
|
||||
defaultMessage: 'Reply to thread...',
|
||||
},
|
||||
replyEditorPlaceholderSend: {
|
||||
id: 'conversation-thread.reply-editor.placeholder.send',
|
||||
defaultMessage: 'Send a message...',
|
||||
},
|
||||
actionResubmitForReview: {
|
||||
id: 'conversation-thread.action.resubmit-for-review',
|
||||
defaultMessage: 'Resubmit for review',
|
||||
},
|
||||
actionReplyToThread: {
|
||||
id: 'conversation-thread.action.reply-to-thread',
|
||||
defaultMessage: 'Reply to thread',
|
||||
},
|
||||
actionReopenThread: {
|
||||
id: 'conversation-thread.action.reopen-thread',
|
||||
defaultMessage: 'Reopen thread',
|
||||
},
|
||||
actionReply: {
|
||||
id: 'conversation-thread.action.reply',
|
||||
defaultMessage: 'Reply',
|
||||
},
|
||||
actionSend: {
|
||||
id: 'conversation-thread.action.send',
|
||||
defaultMessage: 'Send',
|
||||
},
|
||||
actionAddPrivateNote: {
|
||||
id: 'conversation-thread.action.add-private-note',
|
||||
defaultMessage: 'Add private note',
|
||||
},
|
||||
actionResubmitForReviewWithReply: {
|
||||
id: 'conversation-thread.action.resubmit-for-review-with-reply',
|
||||
defaultMessage: 'Resubmit for review with reply',
|
||||
},
|
||||
actionCloseWithReply: {
|
||||
id: 'conversation-thread.action.close-with-reply',
|
||||
defaultMessage: 'Close with reply',
|
||||
},
|
||||
actionCloseThread: {
|
||||
id: 'conversation-thread.action.close-thread',
|
||||
defaultMessage: 'Close thread',
|
||||
},
|
||||
actionApproveWithReply: {
|
||||
id: 'conversation-thread.action.approve-with-reply',
|
||||
defaultMessage: 'Approve with reply',
|
||||
},
|
||||
actionApprove: {
|
||||
id: 'conversation-thread.action.approve',
|
||||
defaultMessage: 'Approve',
|
||||
},
|
||||
actionRejectWithReply: {
|
||||
id: 'conversation-thread.action.reject-with-reply',
|
||||
defaultMessage: 'Reject with reply',
|
||||
},
|
||||
actionReject: {
|
||||
id: 'conversation-thread.action.reject',
|
||||
defaultMessage: 'Reject',
|
||||
},
|
||||
actionWithholdWithReply: {
|
||||
id: 'conversation-thread.action.withhold-with-reply',
|
||||
defaultMessage: 'Withhold with reply',
|
||||
},
|
||||
actionWithhold: {
|
||||
id: 'conversation-thread.action.withhold',
|
||||
defaultMessage: 'Withhold',
|
||||
},
|
||||
actionSetToDraftWithReply: {
|
||||
id: 'conversation-thread.action.set-to-draft-with-reply',
|
||||
defaultMessage: 'Set to draft with reply',
|
||||
},
|
||||
actionSetToDraft: {
|
||||
id: 'conversation-thread.action.set-to-draft',
|
||||
defaultMessage: 'Set to draft',
|
||||
},
|
||||
actionSendToReviewWithReply: {
|
||||
id: 'conversation-thread.action.send-to-review-with-reply',
|
||||
defaultMessage: 'Send to review with reply',
|
||||
},
|
||||
actionSendToReview: {
|
||||
id: 'conversation-thread.action.send-to-review',
|
||||
defaultMessage: 'Send to review',
|
||||
},
|
||||
errorSendingMessage: {
|
||||
id: 'conversation-thread.error.sending-message',
|
||||
defaultMessage: 'Error sending message',
|
||||
},
|
||||
errorClosingReport: {
|
||||
id: 'conversation-thread.error.closing-report',
|
||||
defaultMessage: 'Error closing report',
|
||||
},
|
||||
errorReopeningReport: {
|
||||
id: 'conversation-thread.error.reopening-report',
|
||||
defaultMessage: 'Error reopening report',
|
||||
},
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
thread: {
|
||||
@@ -532,7 +742,7 @@ async function sendReply(status = null, privateMessage = false) {
|
||||
}
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
title: 'Error sending message',
|
||||
title: formatMessage(messages.errorSendingMessage),
|
||||
text: err.data ? err.data.description : err,
|
||||
type: 'error',
|
||||
})
|
||||
@@ -554,7 +764,7 @@ async function closeReport(reply) {
|
||||
await updateThreadLocal()
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
title: 'Error closing report',
|
||||
title: formatMessage(messages.errorClosingReport),
|
||||
text: err.data ? err.data.description : err,
|
||||
type: 'error',
|
||||
})
|
||||
@@ -572,7 +782,7 @@ async function reopenReport() {
|
||||
await updateThreadLocal()
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
title: 'Error reopening report',
|
||||
title: formatMessage(messages.errorReopeningReport),
|
||||
text: err.data ? err.data.description : err,
|
||||
type: 'error',
|
||||
})
|
||||
@@ -604,44 +814,8 @@ async function resubmit() {
|
||||
}
|
||||
|
||||
const requestedStatus = computed(() => props.project.requested_status ?? 'approved')
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.markdown-editor-spacing {
|
||||
margin-bottom: var(--gap-md);
|
||||
}
|
||||
|
||||
.messages {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--spacing-card-md);
|
||||
}
|
||||
|
||||
.thread-id {
|
||||
margin-bottom: var(--spacing-card-md);
|
||||
font-weight: bold;
|
||||
color: var(--color-heading);
|
||||
}
|
||||
|
||||
.input-group {
|
||||
.spacer {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.extra-options {
|
||||
flex-basis: fit-content;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-submit {
|
||||
padding: var(--spacing-card-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-card-lg);
|
||||
|
||||
.project-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<template>
|
||||
<div
|
||||
class="message"
|
||||
class="message px-4 py-3"
|
||||
:class="{
|
||||
'has-body': message.body.type === 'text' && !forceCompact,
|
||||
'no-actions': noLinks,
|
||||
private: isPrivateMessage,
|
||||
'show-private-bg': flags.showModeratorPrivateMessageHighlight,
|
||||
}"
|
||||
>
|
||||
<template v-if="members[message.author_id]">
|
||||
@@ -22,11 +23,6 @@
|
||||
/>
|
||||
</AutoLink>
|
||||
<span :class="`message__author role-${members[message.author_id].role}`">
|
||||
<LockIcon
|
||||
v-if="isPrivateMessage"
|
||||
v-tooltip="'Only visible to moderators'"
|
||||
class="private-icon"
|
||||
/>
|
||||
<AutoLink :to="noLinks ? '' : `/user/${members[message.author_id].username}`">
|
||||
{{ members[message.author_id].username }}
|
||||
</AutoLink>
|
||||
@@ -35,6 +31,11 @@
|
||||
v-else-if="members[message.author_id].role === 'admin'"
|
||||
v-tooltip="'Modrinth Team'"
|
||||
/>
|
||||
<EyeOffIcon
|
||||
v-if="isPrivateMessage"
|
||||
v-tooltip="'Only visible to moderators'"
|
||||
class="ml-1 text-orange"
|
||||
/>
|
||||
<MicrophoneIcon
|
||||
v-if="report && message.author_id === report.reporter_user?.id"
|
||||
v-tooltip="'Reporter'"
|
||||
@@ -79,6 +80,12 @@
|
||||
<span v-if="message.body.new_status === 'processing'">
|
||||
submitted the project for review.
|
||||
</span>
|
||||
<span v-else-if="message.body.old_status === 'processing'">
|
||||
reviewed the project and set its status to <Badge :type="message.body.new_status" />.
|
||||
</span>
|
||||
<span v-else-if="message.body.new_status === 'draft'">
|
||||
reverted this project back to a <Badge :type="message.body.new_status" />.
|
||||
</span>
|
||||
<span v-else>
|
||||
changed the project's status from <Badge :type="message.body.old_status" /> to
|
||||
<Badge :type="message.body.new_status" />.
|
||||
@@ -126,7 +133,7 @@
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
LockIcon,
|
||||
EyeOffIcon,
|
||||
MicrophoneIcon,
|
||||
ModrinthIcon,
|
||||
MoreHorizontalIcon,
|
||||
@@ -178,6 +185,7 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update-thread'])
|
||||
const flags = useFeatureFlags()
|
||||
|
||||
const formattedMessage = computed(() => {
|
||||
const body = renderString(props.message.body.body)
|
||||
@@ -222,15 +230,13 @@ async function deleteMessage() {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.message {
|
||||
--gap-size: var(--spacing-card-xs);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--gap-size);
|
||||
gap: var(--spacing-card-sm);
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
border-radius: var(--size-rounded-card);
|
||||
padding: var(--spacing-card-md);
|
||||
word-break: break-word;
|
||||
position: relative;
|
||||
|
||||
.avatar,
|
||||
.backed-svg {
|
||||
@@ -238,14 +244,12 @@ async function deleteMessage() {
|
||||
}
|
||||
|
||||
&.has-body {
|
||||
--gap-size: var(--spacing-card-sm);
|
||||
display: grid;
|
||||
grid-template:
|
||||
'icon author actions'
|
||||
'icon body actions'
|
||||
'date date date';
|
||||
grid-template-columns: min-content auto 1fr;
|
||||
column-gap: var(--gap-size);
|
||||
row-gap: var(--spacing-card-xs);
|
||||
|
||||
.message__icon {
|
||||
@@ -260,13 +264,22 @@ async function deleteMessage() {
|
||||
|
||||
&:not(.no-actions):hover,
|
||||
&:not(.no-actions):focus-within {
|
||||
background-color: var(--color-table-alternate-row);
|
||||
background-color: var(--surface-2-5);
|
||||
|
||||
.message__actions {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.private.show-private-bg::before {
|
||||
content: '';
|
||||
inset: 0;
|
||||
position: absolute;
|
||||
background-color: var(--color-orange);
|
||||
opacity: 0.05;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.no-actions {
|
||||
padding: 0;
|
||||
|
||||
@@ -346,10 +359,6 @@ a:active + .message__author a,
|
||||
color: var(--color-purple);
|
||||
}
|
||||
|
||||
.private-icon {
|
||||
color: var(--color-gray);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 600px) {
|
||||
.message {
|
||||
//grid-template:
|
||||
@@ -363,6 +372,7 @@ a:active + .message__author a,
|
||||
'icon body actions'
|
||||
'date date date';
|
||||
grid-template-columns: min-content auto 1fr;
|
||||
grid-template-rows: min-content 1fr auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -377,7 +387,7 @@ a:active + .message__author a,
|
||||
'icon author date actions'
|
||||
'icon body body actions';
|
||||
grid-template-columns: min-content auto 1fr;
|
||||
grid-template-rows: min-content 1fr auto;
|
||||
grid-template-rows: min-content 1fr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="flags.developerMode" class="mt-4 font-bold text-heading">
|
||||
<div v-if="flags.developerMode" class="m-4 font-bold text-heading">
|
||||
Thread ID:
|
||||
<CopyCode :text="thread.id" />
|
||||
</div>
|
||||
|
||||
<div v-if="sortedMessages.length > 0" class="flex flex-col space-y-4 rounded-xl p-3 sm:p-4">
|
||||
<div v-if="sortedMessages.length > 0" class="flex flex-col rounded-xl">
|
||||
<ThreadMessage
|
||||
v-for="message in sortedMessages"
|
||||
:key="'message-' + message.id"
|
||||
@@ -28,7 +28,7 @@
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div>
|
||||
<div class="px-4 py-2">
|
||||
<MarkdownEditor
|
||||
v-model="replyBody"
|
||||
:placeholder="sortedMessages.length > 0 ? 'Reply to thread...' : 'Send a message...'"
|
||||
@@ -37,7 +37,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-4 flex flex-col items-stretch justify-between gap-3 sm:flex-row sm:items-center sm:gap-2"
|
||||
class="mt-4 flex flex-col items-stretch justify-between gap-3 px-4 pb-4 sm:flex-row sm:items-center sm:gap-2"
|
||||
>
|
||||
<div class="flex flex-col items-stretch gap-2 sm:flex-row sm:items-center">
|
||||
<ButtonStyled v-if="sortedMessages.length > 0" color="brand">
|
||||
|
||||
Reference in New Issue
Block a user