Improve moderation messages and add moderation UI on projects. (#889)

This commit is contained in:
Prospector 2023-01-11 13:05:11 -08:00 committed by GitHub
parent 8fff3e5389
commit bb80dcb4e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 325 additions and 165 deletions

View File

@ -200,6 +200,10 @@
text-decoration: underline;
}
}
&.moderation-card {
background-color: var(--color-banner-bg);
}
}
.universal-labels {
@ -1458,7 +1462,7 @@ h3 {
}
}
.push-right {
.push-right:not(.input-group), .push-right.input-group > :first-child {
margin-left: auto;
margin-right: 0;
}

View File

@ -15,8 +15,8 @@
<!-- Project statuses -->
<template v-else-if="type === 'approved'"><ListIcon /> Listed</template>
<template v-else-if="type === 'withheld'"><EyeOffIcon /> Withheld</template>
<template v-else-if="type === 'unlisted'"><EyeOffIcon /> Unlisted</template>
<template v-else-if="type === 'withheld'"><EyeOffIcon /> Withheld</template>
<template v-else-if="type === 'private'"><LockIcon /> Private</template>
<template v-else-if="type === 'scheduled'">
<CalendarIcon /> Scheduled
@ -106,6 +106,7 @@ export default {
margin-right: 0.25rem;
}
&.type--withheld,
&.type--rejected,
&.red {
--badge-color: var(--color-special-red);
@ -131,7 +132,6 @@ export default {
color: var(--color-special-blue);
}
&.type--withheld,
&.type--unlisted,
&.purple {
color: var(--color-special-purple);

View File

@ -0,0 +1,193 @@
<template>
<Modal ref="modal" header="Project moderation">
<div v-if="project !== null" class="moderation-modal universal-body">
<p>
A moderation message is optional, but it can be used to communicate
problems with a project's team members. The body is also optional and
supports markdown formatting!
</p>
<div v-if="status" class="status">
<span>New project status: </span>
<Badge :type="status" />
</div>
<h3>Message title</h3>
<input
v-model="moderationMessage"
type="text"
placeholder="Enter the message..."
/>
<h3>Message body</h3>
<div class="textarea-wrapper">
<Chips
v-model="bodyViewMode"
class="separator"
:items="['source', 'preview']"
/>
<textarea
v-if="bodyViewMode === 'source'"
id="body"
v-model="moderationMessageBody"
:disabled="!moderationMessage"
:placeholder="
moderationMessage
? 'Type a body to your moderation message here...'
: 'You must add a title before you add a body.'
"
/>
<div
v-else
v-highlightjs
class="markdown-body preview"
v-html="$xss($md.render(moderationMessageBody))"
></div>
</div>
<div class="push-right input-group">
<button
v-if="moderationMessage || moderationMessageBody"
class="iconified-button"
@click="
moderationMessage = ''
moderationMessageBody = ''
"
>
<TrashIcon />
Clear message
</button>
<button class="iconified-button" @click="$refs.modal.hide()">
<CrossIcon />
Cancel
</button>
<button class="iconified-button brand-button" @click="saveProject">
<CheckIcon />
Confirm
</button>
</div>
</div>
</Modal>
</template>
<script>
import TrashIcon from '~/assets/images/utils/trash.svg?inline'
import CrossIcon from '~/assets/images/utils/x.svg?inline'
import Modal from '~/components/ui/Modal'
import Chips from '~/components/ui/Chips'
import Badge from '~/components/ui/Badge'
import CheckIcon from '~/assets/images/utils/check.svg?inline'
export default {
name: 'ModalModeration',
components: {
TrashIcon,
CrossIcon,
CheckIcon,
Modal,
Chips,
Badge,
},
props: {
project: {
type: Object,
default: null,
},
status: {
type: String,
default: null,
},
onClose: {
type: Function,
default: null,
},
},
data() {
return {
bodyViewMode: 'source',
moderationMessage:
this.project && this.project.moderation_message
? this.project.moderation_message
: '',
moderationMessageBody:
this.project && this.project.moderation_message_body
? this.project.moderation_message_body
: '',
}
},
methods: {
async saveProject() {
this.$nuxt.$loading.start()
try {
const data = {
moderation_message: this.moderationMessage
? this.moderationMessage
: null,
moderation_message_body: this.moderationMessageBody
? this.moderationMessageBody
: null,
}
if (this.status) {
data.status = this.status
}
await this.$axios.patch(
`project/${this.project.id}`,
data,
this.$defaultHeaders()
)
if (this.onClose !== null) {
this.onClose()
}
this.$refs.modal.hide()
} catch (err) {
this.$notify({
group: 'main',
title: 'An error occurred',
text: err.response ? err.response.data.description : err,
type: 'error',
})
}
this.$nuxt.$loading.finish()
},
show() {
this.$refs.modal.show()
this.moderationMessage =
this.project && this.project.moderator_message.message
? this.project.moderator_message.message
: ''
this.moderationMessageBody =
this.project && this.project.moderator_message.body
? this.project.moderator_message.body
: ''
},
},
}
</script>
<style scoped lang="scss">
.moderation-modal {
padding: var(--spacing-card-lg);
.status {
display: flex;
align-items: center;
margin-bottom: 0.5rem;
span {
margin-right: 0.5rem;
}
}
.textarea-wrapper {
margin-top: 0.5rem;
height: 15rem;
.preview {
overflow-y: auto;
}
}
.separator {
margin: var(--spacing-card-sm) 0;
}
}
</style>

View File

@ -106,6 +106,12 @@
</div>
</div>
<div v-else>
<ModalModeration
ref="modal_moderation"
:project="project"
:status="moderationStatus"
:on-close="resetProject"
/>
<Modal
ref="modal_license"
:header="project.license.name ? project.license.name : 'License'"
@ -275,20 +281,20 @@
v-if="
currentMember &&
((project.status !== 'approved' &&
project.status !== 'unlisted' &&
project.status !== 'draft' &&
project.status !== 'processing') ||
(project.moderator_message &&
(project.moderator_message.message ||
project.moderator_message.body)))
"
class="project-status card"
class="universal-card"
>
<h3 class="card-header">Project status</h3>
<div class="status-info"></div>
<p>
Your project is currently:
<h3 class="card-header">Moderation status</h3>
<div class="current-status">
Project status:
<Badge :type="project.status" />
</p>
</div>
<div class="message">
<p v-if="project.status === 'rejected'">
Your project has been rejected by Modrinth's staff. In most cases,
@ -299,9 +305,7 @@
<div v-if="project.moderator_message">
<hr class="card-divider" />
<div v-if="project.moderator_message.body">
<h3 class="card-header">
Message from the Modrinth moderators:
</h3>
<h3 class="card-header">Message from the moderators:</h3>
<p
v-if="project.moderator_message.message"
class="mod-message__title"
@ -315,18 +319,16 @@
/>
</div>
<div v-else>
<h3 class="card-header">
Message from the Modrinth moderators:
</h3>
<h3 class="card-header">Message from the moderators:</h3>
<p>{{ project.moderator_message.message }}</p>
</div>
<hr class="card-divider" />
</div>
</div>
<div class="buttons status-buttons">
<button
v-if="
project.status === 'rejected' || project.status === 'withheld'
!$tag.approvedStatuses.includes(project.status) &&
project.status !== 'processing'
"
class="iconified-button brand-button"
@click="submitForReview"
@ -335,7 +337,7 @@
Resubmit for review
</button>
<button
v-if="project.status === 'approved'"
v-if="$tag.approvedStatuses.includes(project.status)"
class="iconified-button"
@click="clearMessage"
>
@ -363,6 +365,64 @@
</ul>
</div>
</div>
<div
v-if="
$auth.user &&
($auth.user.role === 'admin' || $auth.user.role === 'moderator')
"
class="universal-card moderation-card"
>
<h3>Moderation actions</h3>
<div class="input-stack">
<button
v-if="
!$tag.approvedStatuses.includes(project.status) ||
project.status === 'processing'
"
class="iconified-button brand-button"
@click="
openModerationModal(
project.requested_status
? project.requested_status
: 'approved'
)
"
>
<CheckIcon />
Approve
</button>
<button
v-if="
$tag.approvedStatuses.includes(project.status) ||
project.status === 'processing'
"
class="iconified-button danger-button"
@click="openModerationModal('withheld')"
>
<EyeIcon />
Withhold
</button>
<button
v-if="
$tag.approvedStatuses.includes(project.status) ||
project.status === 'processing'
"
class="iconified-button danger-button"
@click="openModerationModal('rejected')"
>
<CrossIcon />
Reject
</button>
<button class="iconified-button" @click="openModerationModal(null)">
<EditIcon />
Edit message
</button>
<nuxt-link class="iconified-button" to="/moderation">
<ModerationIcon />
Visit moderation queue
</nuxt-link>
</div>
</div>
</div>
<div class="card normal-page__info">
<template
@ -680,11 +740,7 @@
>.
</div>
<Advertisement
v-if="
['approved', 'unlisted', 'archived', 'private'].includes(
project.status
)
"
v-if="$tag.approvedStatuses.includes(project.status)"
type="banner"
small-screen="square"
/>
@ -769,6 +825,7 @@ import Categories from '~/components/ui/search/Categories'
import EnvironmentIndicator from '~/components/ui/EnvironmentIndicator'
import Modal from '~/components/ui/Modal'
import ModalReport from '~/components/ui/ModalReport'
import ModalModeration from '~/components/ui/ModalModeration'
import NavRow from '~/components/ui/NavRow'
import CopyCode from '~/components/ui/CopyCode'
import Avatar from '~/components/ui/Avatar'
@ -783,6 +840,9 @@ import LinksIcon from '~/assets/images/utils/link.svg?inline'
import LicenseIcon from '~/assets/images/utils/copyright.svg?inline'
import GalleryIcon from '~/assets/images/utils/image.svg?inline'
import VersionIcon from '~/assets/images/utils/version.svg?inline'
import CrossIcon from '~/assets/images/utils/x.svg?inline'
import EditIcon from '~/assets/images/utils/edit.svg?inline'
import ModerationIcon from '~/assets/images/sidebar/admin.svg?inline'
export default {
components: {
@ -793,6 +853,7 @@ export default {
Advertisement,
Modal,
ModalReport,
ModalModeration,
ProjectPublishingChecklist,
EnvironmentIndicator,
IssuesIcon,
@ -818,6 +879,9 @@ export default {
NavStackItem,
SettingsIcon,
EyeIcon,
CrossIcon,
EditIcon,
ModerationIcon,
GalleryIcon,
VersionIcon,
UsersIcon,
@ -968,6 +1032,7 @@ export default {
routeName: '',
from: '',
collapsedChecklist: false,
moderationStatus: null,
}
},
fetch() {
@ -1291,6 +1356,11 @@ export default {
)
this.project.icon_url = response.data.icon_url
},
openModerationModal(status) {
this.moderationStatus = status
this.$refs.modal_moderation.show()
},
},
}
</script>
@ -1577,4 +1647,15 @@ export default {
}
}
}
.current-status {
display: flex;
flex-direction: row;
gap: var(--spacing-card-sm);
margin-top: var(--spacing-card-md);
}
.normal-page__sidebar .mod-button {
margin-top: var(--spacing-card-sm);
}
</style>

View File

@ -161,7 +161,7 @@
id="project-visibility"
v-model="visibility"
placeholder="Select one"
:options="statusOptions"
:options="$tag.approvedStatuses"
:custom-label="(value) => $formatProjectStatus(value)"
:searchable="false"
:close-on-select="true"
@ -298,7 +298,7 @@ export default {
this.summary = this.project.description
this.clientSide = this.project.client_side
this.serverSide = this.project.server_side
this.visibility = this.statusOptions.includes(this.project.status)
this.visibility = this.$tag.approvedStatuses.includes(this.project.status)
? this.project.status
: this.project.requested_status
},
@ -316,9 +316,6 @@ export default {
sideTypes() {
return ['required', 'optional', 'unsupported']
},
statusOptions() {
return ['approved', 'archived', 'unlisted', 'private']
},
patchData() {
const data = {}
@ -337,12 +334,12 @@ export default {
if (this.serverSide !== this.project.server_side) {
data.server_side = this.serverSide
}
if (this.visibility !== this.project.requested_status) {
if (!this.statusOptions.includes(this.project.status)) {
data.requested_status = this.visibility
} else {
if (this.$tag.approvedStatuses.includes(this.project.status)) {
if (this.visibility !== this.project.status) {
data.status = this.visibility
}
} else if (this.visibility !== this.project.requested_status) {
data.requested_status = this.visibility
}
return data

View File

@ -1,58 +1,11 @@
<template>
<div>
<Modal ref="modal" header="Moderation Form">
<div v-if="currentProject !== null" class="moderation-modal">
<p>
Both of these fields are optional, but can be used to communicate
problems with a project's team members. The body supports markdown
formatting!
</p>
<div class="status">
<span>New project status: </span>
<Badge :type="currentProject.newStatus" />
</div>
<input
v-model="currentProject.moderation_message"
type="text"
placeholder="Enter the message..."
/>
<h3>Body</h3>
<div class="textarea-wrapper">
<Chips
v-model="bodyViewMode"
class="separator"
:items="['source', 'preview']"
/>
<textarea
v-if="bodyViewMode === 'source'"
id="body"
v-model="currentProject.moderation_message_body"
/>
<div
v-else
v-highlightjs
class="markdown-body preview"
v-html="$xss($md.render(currentProject.moderation_message_body))"
></div>
</div>
<div class="buttons">
<button
class="iconified-button"
@click="
$refs.modal.hide()
currentProject = null
"
>
<CrossIcon />
Cancel
</button>
<button class="iconified-button brand-button" @click="saveProject">
<CheckIcon />
Confirm
</button>
</div>
</div>
</Modal>
<ModalModeration
ref="modal"
:project="currentProject"
:status="currentStatus"
:on-close="onModalClose"
/>
<div class="normal-page">
<div class="normal-page__sidebar">
<aside class="universal-card">
@ -175,9 +128,7 @@
</template>
<script>
import Chips from '~/components/ui/Chips'
import ProjectCard from '~/components/ui/ProjectCard'
import Modal from '~/components/ui/Modal'
import Badge from '~/components/ui/Badge'
import CheckIcon from '~/assets/images/utils/check.svg?inline'
@ -188,18 +139,18 @@ import CalendarIcon from '~/assets/images/utils/calendar.svg?inline'
import Security from '~/assets/images/illustrations/security.svg?inline'
import NavStack from '~/components/ui/NavStack'
import NavStackItem from '~/components/ui/NavStackItem'
import ModalModeration from '~/components/ui/ModalModeration'
export default {
name: 'Moderation',
components: {
ModalModeration,
NavStack,
NavStackItem,
Chips,
ProjectCard,
CheckIcon,
CrossIcon,
UnlistIcon,
Modal,
Badge,
Security,
TrashIcon,
@ -287,8 +238,8 @@ export default {
},
data() {
return {
bodyViewMode: 'source',
currentProject: null,
currentStatus: null,
}
},
head: {
@ -311,47 +262,17 @@ export default {
},
methods: {
setProjectStatus(project, status) {
project.moderation_message = ''
project.moderation_message_body = ''
project.newStatus = status
this.currentProject = project
this.currentStatus = status
this.$refs.modal.show()
},
async saveProject() {
this.$nuxt.$loading.start()
try {
await this.$axios.patch(
`project/${this.currentProject.id}`,
{
moderation_message: this.currentProject.moderation_message
? this.currentProject.moderation_message
: null,
moderation_message_body: this.currentProject.moderation_message_body
? this.currentProject.moderation_message_body
: null,
status: this.currentProject.newStatus,
},
this.$defaultHeaders()
)
this.projects.splice(
this.projects.findIndex((x) => this.currentProject.id === x.id),
1
)
this.currentProject = null
this.$refs.modal.hide()
} catch (err) {
this.$notify({
group: 'main',
title: 'An error occurred',
text: err.response.data.description,
type: 'error',
})
}
this.$nuxt.$loading.finish()
onModalClose() {
this.projects.splice(
this.projects.findIndex((x) => this.project.id === x.id),
1
)
this.project = null
},
async deleteReport(index) {
this.$nuxt.$loading.start()
@ -379,43 +300,6 @@ export default {
</script>
<style lang="scss" scoped>
.moderation-modal {
width: auto;
padding: var(--spacing-card-md) var(--spacing-card-lg);
.status {
display: flex;
align-items: center;
margin-bottom: 0.5rem;
span {
margin-right: 0.5rem;
}
}
.textarea-wrapper {
margin-top: 0.5rem;
height: 15rem;
.preview {
overflow-y: auto;
}
}
.separator {
margin: var(--spacing-card-sm) 0;
}
.buttons {
display: flex;
margin-top: 0.5rem;
:first-child {
margin-left: auto;
}
}
}
h1 {
color: var(--color-text-dark);
}

View File

@ -55,4 +55,5 @@ export const state = () => ({
modLoaders: ['forge', 'fabric', 'quilt', 'liteloader', 'modloader', 'rift'],
},
projectViewModes: ['list', 'grid', 'gallery'],
approvedStatuses: ['approved', 'archived', 'unlisted', 'private'],
})