You've already forked AstralRinth
forked from didirus/AstralRinth
Threads and more! (#1232)
* Begin UI for threads and moderation overhaul * Hide close button on non-report threads * Fix review age coloring * Add project count * Remove action buttons from queue page and add queued date to project page * Hook up to actual data * Remove unused icon * Get up to 1000 projects in queue * prettier * more prettier * Changed all the things * lint * rebuild * Add omorphia * Workaround formatjs bug in ThreadSummary.vue * Fix notifications page on prod * Fix a few notifications and threads bugs * lockfile * Fix duplicate button styles * more fixes and polishing * More fixes * Remove legacy pages * More bugfixes * Add some error catching for reports and notifications * More error handling * fix lint * Add inbox links * Remove loading component and rename member header * Rely on threads always existing * Handle if project update notifs are not grouped * oops * Fix chips on notifications page * Import ModalModeration * finish threads --------- Co-authored-by: triphora <emma@modrinth.com> Co-authored-by: Jai A <jaiagr+gpg@pm.me>
This commit is contained in:
193
pages/[type]/[id]/moderation.vue
Normal file
193
pages/[type]/[id]/moderation.vue
Normal file
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<div>
|
||||
<section class="universal-card">
|
||||
<h2>Project status</h2>
|
||||
<Badge :type="project.status" />
|
||||
<p v-if="isApproved(project)">
|
||||
Your project been approved by the moderators and you may freely change project visibility in
|
||||
<router-link :to="`${getProjectLink(project)}/settings`" class="text-link"
|
||||
>your project's settings</router-link
|
||||
>.
|
||||
</p>
|
||||
<p v-else-if="isUnderReview(project)">
|
||||
Project reviews typically take 24 to 48 hours and they will leave a message below if they
|
||||
have any questions or concerns for you. If your review has taken more than 48 hours, check
|
||||
our Discord or social media for moderation delays.
|
||||
</p>
|
||||
<template v-else-if="isRejected(project)">
|
||||
<p>
|
||||
Your project does not currently meet Modrinth's
|
||||
<nuxt-link to="/legal/rules" class="text-link" target="_blank">content rules</nuxt-link>
|
||||
and the moderators have requested you make changes before it can be approved. Read the
|
||||
messages from the moderators below and address their comments before resubmitting.
|
||||
</p>
|
||||
<p class="warning">
|
||||
Repeated submissions without addressing the moderators' comments may result in an account
|
||||
suspension.
|
||||
</p>
|
||||
</template>
|
||||
<h3>Current visibility</h3>
|
||||
<ul class="visibility-info">
|
||||
<li v-if="isListed(project)">
|
||||
<CheckIcon class="good" />
|
||||
Listed in search results
|
||||
</li>
|
||||
<li v-else>
|
||||
<ExitIcon class="bad" />
|
||||
Not listed in search results
|
||||
</li>
|
||||
<li v-if="isListed(project)">
|
||||
<CheckIcon class="good" />
|
||||
Listed on the profiles of members
|
||||
</li>
|
||||
<li v-else>
|
||||
<ExitIcon class="bad" />
|
||||
Not listed on the profiles of members
|
||||
</li>
|
||||
<li v-if="isPrivate(project)">
|
||||
<ExitIcon class="bad" />
|
||||
Not accessible with a direct link
|
||||
</li>
|
||||
<li v-else>
|
||||
<CheckIcon class="good" />
|
||||
Accessible with a direct link
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section id="messages" class="universal-card">
|
||||
<h2>Messages</h2>
|
||||
<p>
|
||||
This is a private conversation thread with the Modrinth moderators. They will message you
|
||||
for issues concerning your project on Modrinth, and you are welcome to message them about
|
||||
things concerning your project.
|
||||
</p>
|
||||
<ConversationThread
|
||||
v-if="thread"
|
||||
:thread="thread"
|
||||
:update-thread="(newThread) => (thread = newThread)"
|
||||
:project="project"
|
||||
:set-status="setStatus"
|
||||
:current-member="currentMember"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import ConversationThread from '~/components/ui/thread/ConversationThread.vue'
|
||||
import Badge from '~/components/ui/Badge.vue'
|
||||
import {
|
||||
getProjectLink,
|
||||
isApproved,
|
||||
isListed,
|
||||
isPrivate,
|
||||
isRejected,
|
||||
isUnderReview,
|
||||
} from '~/helpers/projects.js'
|
||||
import ExitIcon from 'assets/images/utils/x.svg'
|
||||
import CheckIcon from 'assets/images/utils/check.svg'
|
||||
|
||||
const props = defineProps({
|
||||
project: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
currentMember: {
|
||||
type: Object,
|
||||
default() {
|
||||
return null
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:project'])
|
||||
|
||||
const app = useNuxtApp()
|
||||
|
||||
const { data: thread } = await useAsyncData(`thread/${props.project.thread_id}`, () =>
|
||||
useBaseFetch(`thread/${props.project.thread_id}`, app.$defaultHeaders())
|
||||
)
|
||||
async function setStatus(status) {
|
||||
startLoading()
|
||||
|
||||
try {
|
||||
const data = {}
|
||||
data.status = status
|
||||
await useBaseFetch(`project/${props.project.id}`, {
|
||||
method: 'PATCH',
|
||||
body: data,
|
||||
...app.$defaultHeaders(),
|
||||
})
|
||||
const project = props.project
|
||||
project.status = status
|
||||
emit('update:project', project)
|
||||
thread.value = await useBaseFetch(`thread/${thread.value.id}`, app.$defaultHeaders())
|
||||
} catch (err) {
|
||||
app.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.data ? err.data.description : err,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
stopLoading()
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.stacked {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.status-message {
|
||||
:deep(.badge) {
|
||||
display: contents;
|
||||
|
||||
svg {
|
||||
vertical-align: top;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.unavailable-error {
|
||||
.code {
|
||||
margin-top: var(--spacing-card-sm);
|
||||
}
|
||||
|
||||
svg {
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
.visibility-info {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-card-xs);
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
&.good {
|
||||
color: var(--color-brand-green);
|
||||
}
|
||||
|
||||
&.bad {
|
||||
color: var(--color-special-red);
|
||||
}
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: var(--color-special-orange);
|
||||
}
|
||||
</style>
|
||||
@@ -147,10 +147,12 @@
|
||||
<div class="adjacent-input">
|
||||
<label for="project-visibility">
|
||||
<span class="label__title">Visibility</span>
|
||||
<span class="label__description">
|
||||
<div class="label__description">
|
||||
Listed and archived projects are visible in search. Unlisted projects are published, but
|
||||
not visible in search or on user profiles. Private projects are only accessible by
|
||||
members of the project.
|
||||
|
||||
<p>If approved by the moderators:</p>
|
||||
<ul class="visibility-info">
|
||||
<li>
|
||||
<CheckIcon
|
||||
@@ -183,7 +185,7 @@
|
||||
{{ hasModifiedVisibility() ? 'Will be v' : 'V' }}isible via URL
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
<Multiselect
|
||||
id="project-visibility"
|
||||
@@ -408,7 +410,7 @@ export default defineNuxtComponent({
|
||||
...this.$defaultHeaders(),
|
||||
})
|
||||
await initUserProjects()
|
||||
await this.$router.push('/dashboard/projects')
|
||||
await this.$router.push('/dashboard/review')
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'Project deleted',
|
||||
|
||||
@@ -13,17 +13,39 @@
|
||||
project.
|
||||
</span>
|
||||
</span>
|
||||
<div
|
||||
v-if="(currentMember.permissions & MANAGE_INVITES) === MANAGE_INVITES"
|
||||
class="input-group"
|
||||
>
|
||||
<input id="username" v-model="currentUsername" type="text" placeholder="Username" />
|
||||
<div class="input-group">
|
||||
<input
|
||||
id="username"
|
||||
v-model="currentUsername"
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
:disabled="(currentMember.permissions & MANAGE_INVITES) !== MANAGE_INVITES"
|
||||
@keypress.enter="inviteTeamMember()"
|
||||
/>
|
||||
<label for="username" class="hidden">Username</label>
|
||||
<button class="iconified-button brand-button" @click="inviteTeamMember">
|
||||
<button
|
||||
class="iconified-button brand-button"
|
||||
:disabled="(currentMember.permissions & MANAGE_INVITES) !== MANAGE_INVITES"
|
||||
@click="inviteTeamMember()"
|
||||
>
|
||||
<UserPlusIcon />
|
||||
Invite
|
||||
</button>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<span class="label">
|
||||
<span class="label__title">Leave project</span>
|
||||
<span class="label__description"> Remove yourself as a member of this project. </span>
|
||||
</span>
|
||||
<button
|
||||
class="iconified-button danger-button"
|
||||
:disabled="currentMember.role === 'Owner'"
|
||||
@click="leaveProject()"
|
||||
>
|
||||
<UserRemoveIcon />
|
||||
Leave project
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="(member, index) in allTeamMembers"
|
||||
@@ -227,6 +249,7 @@ import TransferIcon from '~/assets/images/utils/transfer.svg'
|
||||
import UserPlusIcon from '~/assets/images/utils/user-plus.svg'
|
||||
import UserRemoveIcon from '~/assets/images/utils/user-x.svg'
|
||||
import Avatar from '~/components/ui/Avatar.vue'
|
||||
import { removeSelfFromTeam } from '~/helpers/teams.js'
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
@@ -282,6 +305,11 @@ export default defineNuxtComponent({
|
||||
this.VIEW_PAYOUTS = 1 << 9
|
||||
},
|
||||
methods: {
|
||||
removeSelfFromTeam,
|
||||
async leaveProject() {
|
||||
await removeSelfFromTeam(project.team)
|
||||
await this.$router.push('/dashboard/projects')
|
||||
},
|
||||
async inviteTeamMember() {
|
||||
startLoading()
|
||||
|
||||
@@ -297,6 +325,7 @@ export default defineNuxtComponent({
|
||||
body: data,
|
||||
...this.$defaultHeaders(),
|
||||
})
|
||||
this.currentUsername = ''
|
||||
await this.updateMembers()
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
|
||||
@@ -135,8 +135,8 @@
|
||||
</button>
|
||||
<button class="iconified-button" @click="version.featured = !version.featured">
|
||||
<StarIcon aria-hidden="true" />
|
||||
<template v-if="!version.featured"> Feature version </template>
|
||||
<template v-else> Unfeature version </template>
|
||||
<template v-if="!version.featured"> Feature version</template>
|
||||
<template v-else> Unfeature version</template>
|
||||
</button>
|
||||
<nuxt-link
|
||||
v-if="currentMember"
|
||||
@@ -160,11 +160,7 @@
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
Download
|
||||
</a>
|
||||
<button
|
||||
v-if="$auth.user && !currentMember"
|
||||
class="iconified-button"
|
||||
@click="$refs.modal_version_report.show()"
|
||||
>
|
||||
<button class="iconified-button" @click="$refs.modal_version_report.show()">
|
||||
<ReportIcon aria-hidden="true" />
|
||||
Report
|
||||
</button>
|
||||
@@ -240,12 +236,12 @@
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="version.dependencies.length > 0 || (isEditing && project.project_type !== 'modpack')"
|
||||
v-if="deps.length > 0 || (isEditing && project.project_type !== 'modpack')"
|
||||
class="version-page__dependencies universal-card"
|
||||
>
|
||||
<h3>Dependencies</h3>
|
||||
<div
|
||||
v-for="(dependency, index) in version.dependencies.filter((x) => !x.file_name)"
|
||||
v-for="(dependency, index) in deps.filter((x) => !x.file_name)"
|
||||
:key="index"
|
||||
class="dependency"
|
||||
:class="{ 'button-transparent': !isEditing }"
|
||||
@@ -260,11 +256,11 @@
|
||||
<span class="project-title">
|
||||
{{ dependency.project ? dependency.project.title : 'Unknown Project' }}
|
||||
</span>
|
||||
<span v-if="dependency.version">
|
||||
<span v-if="dependency.version" class="dep-type" :class="dependency.dependency_type">
|
||||
Version {{ dependency.version.version_number }} is
|
||||
{{ dependency.dependency_type }}
|
||||
</span>
|
||||
<span v-else class="dep-type">
|
||||
<span v-else class="dep-type" :class="dependency.dependency_type">
|
||||
{{ dependency.dependency_type }}
|
||||
</span>
|
||||
</nuxt-link>
|
||||
@@ -272,11 +268,11 @@
|
||||
<span class="project-title">
|
||||
{{ dependency.project ? dependency.project.title : 'Unknown Project' }}
|
||||
</span>
|
||||
<span v-if="dependency.version">
|
||||
<span v-if="dependency.version" class="dep-type" :class="dependency.dependency_type">
|
||||
Version {{ dependency.version.version_number }} is
|
||||
{{ dependency.dependency_type }}
|
||||
</span>
|
||||
<span v-else class="dep-type">
|
||||
<span v-else class="dep-type" :class="dependency.dependency_type">
|
||||
{{ dependency.dependency_type }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -290,7 +286,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-for="(dependency, index) in version.dependencies.filter((x) => x.file_name)"
|
||||
v-for="(dependency, index) in deps.filter((x) => x.file_name)"
|
||||
:key="index"
|
||||
class="dependency"
|
||||
>
|
||||
@@ -299,7 +295,7 @@
|
||||
<span class="project-title">
|
||||
{{ dependency.file_name }}
|
||||
</span>
|
||||
<span>Added via overrides</span>
|
||||
<span class="dep-type" :class="dependency.dependency_type">Added via overrides</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isEditing && project.project_type !== 'modpack'" class="add-dependency">
|
||||
@@ -630,7 +626,7 @@
|
||||
<div v-if="!isEditing">
|
||||
<h4>Publication date</h4>
|
||||
<span>
|
||||
{{ $dayjs(version.date_published).format('MMMM D, YYYY [at] h:mm:ss A') }}
|
||||
{{ $dayjs(version.date_published).format('MMMM D, YYYY [at] h:mm A') }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="!isEditing && version.author">
|
||||
@@ -896,6 +892,8 @@ export default defineNuxtComponent({
|
||||
|
||||
oldFileTypes = version.files.map((x) => fileTypes.find((y) => y.value === x.file_type))
|
||||
|
||||
const order = ['required', 'optional', 'incompatible', 'embedded']
|
||||
|
||||
return {
|
||||
fileTypes: ref(fileTypes),
|
||||
oldFileTypes: ref(oldFileTypes),
|
||||
@@ -919,6 +917,11 @@ export default defineNuxtComponent({
|
||||
.$dayjs(version.date_published)
|
||||
.format('MMM D, YYYY')}. ${version.downloads} downloads.`
|
||||
),
|
||||
deps: computed(() =>
|
||||
version.dependencies.sort(
|
||||
(a, b) => order.indexOf(a.dependency_type) - order.indexOf(b.dependency_type)
|
||||
)
|
||||
),
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -1428,6 +1431,11 @@ export default defineNuxtComponent({
|
||||
|
||||
.dep-type {
|
||||
text-transform: capitalize;
|
||||
color: var(--color-text-secondary);
|
||||
|
||||
&.incompatible {
|
||||
color: var(--color-red);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1529,6 +1537,7 @@ export default defineNuxtComponent({
|
||||
h4 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
@@ -290,10 +290,6 @@ async function handleFiles(files) {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-card-xs);
|
||||
}
|
||||
|
||||
&:active:not(&:disabled) {
|
||||
transform: scale(0.99) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user