You've already forked AstralRinth
forked from didirus/AstralRinth
Implement Editor MOD-349 (#1427)
* Implement Editor * content oveflow fix for description * Description card fix * make everything fix in report modal * seperate report page with image upload * Bump Omorphia * Update pages/report.vue Co-authored-by: Emma Alexia <emma@modrinth.com> * suggested changes and cleanup * fix button spacing * clean up and replace report implementations * corepack fix * Remove ModalReport * image uploads for conversations * image uploading context for versions and threads * adjust information about thread messages * Update pages/report.vue Co-authored-by: Emma Alexia <emma@modrinth.com> * Adjust image upload imports * fix api changes for useImageUpload * correct report redirection uri * report button feedback * omorphia ver bump --------- Co-authored-by: Emma Alexia <emma@modrinth.com>
This commit is contained in:
@@ -221,8 +221,6 @@
|
||||
:where(input) {
|
||||
box-sizing: border-box;
|
||||
max-height: 40px;
|
||||
width: 24rem;
|
||||
flex-basis: 24rem;
|
||||
|
||||
&:not(.stylized-toggle) {
|
||||
max-width: 100%;
|
||||
|
||||
@@ -96,5 +96,6 @@
|
||||
|
||||
.normal-page__content {
|
||||
max-width: calc(60rem - 0.75rem);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
<template>
|
||||
<Modal ref="modal" :header="`Report ${itemType}`">
|
||||
<div class="modal-report universal-labels">
|
||||
<div class="markdown-body">
|
||||
<p>
|
||||
Modding should be safe for everyone, so we take abuse and malicious intent seriously at
|
||||
Modrinth. We want to hear about harmful content on the site that violates our
|
||||
<nuxt-link class="text-link" to="/legal/terms">ToS</nuxt-link> and
|
||||
<nuxt-link class="text-link" to="/legal/rules">Rules</nuxt-link>. Rest assured, we’ll keep
|
||||
your identifying information private.
|
||||
</p>
|
||||
<p v-if="itemType === 'project' || itemType === 'version'">
|
||||
Please <strong>do not</strong> use this to report bugs with the project itself. This form
|
||||
is only for submitting a report to Modrinth staff. If the project has an Issues link or a
|
||||
Discord invite, consider reporting it there.
|
||||
</p>
|
||||
</div>
|
||||
<label for="report-type">
|
||||
<span class="label__title">Reason</span>
|
||||
</label>
|
||||
<Multiselect
|
||||
id="report-type"
|
||||
v-model="reportType"
|
||||
:options="tags.reportTypes"
|
||||
:custom-label="(value) => value.charAt(0).toUpperCase() + value.slice(1)"
|
||||
:multiple="false"
|
||||
:searchable="false"
|
||||
:show-no-results="false"
|
||||
:show-labels="false"
|
||||
placeholder="Choose report type"
|
||||
/>
|
||||
<label for="report-body">
|
||||
<span class="label__title">Additional information</span>
|
||||
<span class="label__description add-line-height">
|
||||
Please provide additional context about your report. Include links and images if possible.
|
||||
<strong>Empty reports will be closed.</strong> This editor supports
|
||||
<a
|
||||
class="text-link"
|
||||
href="https://docs.modrinth.com/docs/tutorials/markdown/"
|
||||
target="_blank"
|
||||
>Markdown formatting</a
|
||||
>.
|
||||
</span>
|
||||
</label>
|
||||
<div class="textarea-wrapper">
|
||||
<Chips v-model="bodyViewType" class="separator" :items="['source', 'preview']" />
|
||||
<div v-if="bodyViewType === 'source'" class="textarea-wrapper">
|
||||
<textarea id="body" v-model="body" spellcheck="true" />
|
||||
</div>
|
||||
<div v-else class="preview" v-html="renderString(body)" />
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button class="iconified-button" @click="cancel">
|
||||
<CrossIcon />
|
||||
Cancel
|
||||
</button>
|
||||
<button class="iconified-button brand-button" @click="submitReport">
|
||||
<CheckIcon />
|
||||
Report
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Multiselect } from 'vue-multiselect'
|
||||
import CrossIcon from '~/assets/images/utils/x.svg'
|
||||
import CheckIcon from '~/assets/images/utils/check.svg'
|
||||
import Modal from '~/components/ui/Modal.vue'
|
||||
import Chips from '~/components/ui/Chips.vue'
|
||||
import { renderString } from '~/helpers/parse.js'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Chips,
|
||||
CrossIcon,
|
||||
CheckIcon,
|
||||
Modal,
|
||||
Multiselect,
|
||||
},
|
||||
props: {
|
||||
itemType: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
itemId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const tags = useTags()
|
||||
|
||||
return { tags }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
reportType: '',
|
||||
body: '',
|
||||
bodyViewType: 'source',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
renderString,
|
||||
cancel() {
|
||||
this.reportType = ''
|
||||
this.body = ''
|
||||
this.bodyViewType = 'source'
|
||||
|
||||
this.$refs.modal.hide()
|
||||
},
|
||||
async submitReport() {
|
||||
startLoading()
|
||||
try {
|
||||
const data = {
|
||||
report_type: this.reportType,
|
||||
item_id: this.itemId,
|
||||
item_type: this.itemType,
|
||||
body: this.body,
|
||||
}
|
||||
await useBaseFetch('report', {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
})
|
||||
|
||||
this.$refs.modal.hide()
|
||||
await this.$router.push('/dashboard/reports')
|
||||
} catch (err) {
|
||||
this.$notify({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
stopLoading()
|
||||
},
|
||||
show() {
|
||||
this.$refs.modal.show()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.modal-report {
|
||||
padding: var(--spacing-card-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.add-line-height {
|
||||
line-height: 1.5;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.multiselect {
|
||||
max-width: 20rem;
|
||||
}
|
||||
|
||||
.textarea-wrapper {
|
||||
margin-top: 1rem;
|
||||
height: 12rem;
|
||||
|
||||
textarea {
|
||||
// here due to a bug in safari
|
||||
max-height: 9rem;
|
||||
}
|
||||
|
||||
.preview {
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -51,14 +51,12 @@
|
||||
This thread is closed and new messages cannot be sent to it.
|
||||
</span>
|
||||
<template v-else-if="!report || !report.closed">
|
||||
<div class="resizable-textarea-wrapper">
|
||||
<Chips v-model="replyViewMode" class="chips" :items="['source', 'preview']" />
|
||||
<textarea
|
||||
v-if="replyViewMode === 'source'"
|
||||
<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-else class="markdown-body preview" v-html="renderString(replyBody)" />
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<button
|
||||
@@ -170,7 +168,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Chips from '~/components/ui/Chips.vue'
|
||||
import { MarkdownEditor } from 'omorphia'
|
||||
import { useImageUpload } from '~/composables/image-upload.ts'
|
||||
import CopyCode from '~/components/ui/CopyCode.vue'
|
||||
import ReplyIcon from '~/assets/images/utils/reply.svg'
|
||||
import SendIcon from '~/assets/images/utils/send.svg'
|
||||
@@ -179,7 +178,6 @@ import CrossIcon from '~/assets/images/utils/x.svg'
|
||||
import EyeOffIcon from '~/assets/images/utils/eye-off.svg'
|
||||
import CheckIcon from '~/assets/images/utils/check.svg'
|
||||
import ModerationIcon from '~/assets/images/sidebar/admin.svg'
|
||||
import { renderString } from '~/helpers/parse.js'
|
||||
import ThreadMessage from '~/components/ui/thread/ThreadMessage.vue'
|
||||
import { isStaff } from '~/helpers/users.js'
|
||||
import { isApproved, isRejected } from '~/helpers/projects.js'
|
||||
@@ -233,7 +231,6 @@ const members = computed(() => {
|
||||
return members
|
||||
})
|
||||
|
||||
const replyViewMode = ref('source')
|
||||
const replyBody = ref('')
|
||||
|
||||
const sortedMessages = computed(() => {
|
||||
@@ -261,18 +258,41 @@ async function updateThreadLocal() {
|
||||
props.updateThread(thread)
|
||||
}
|
||||
|
||||
const imageIDs = ref([])
|
||||
|
||||
async function onUploadImage(file) {
|
||||
const response = await useImageUpload(file, { context: 'thread_message' })
|
||||
|
||||
imageIDs.value.push(response.id)
|
||||
// Keep the last 10 entries of image IDs
|
||||
imageIDs.value = imageIDs.value.slice(-10)
|
||||
|
||||
return response.url
|
||||
}
|
||||
|
||||
async function sendReply(status = null) {
|
||||
try {
|
||||
const body = {
|
||||
body: {
|
||||
type: 'text',
|
||||
body: replyBody.value,
|
||||
},
|
||||
}
|
||||
|
||||
if (imageIDs.value.length > 0) {
|
||||
body.body = {
|
||||
...body.body,
|
||||
uploaded_images: imageIDs.value,
|
||||
}
|
||||
}
|
||||
|
||||
await useBaseFetch(`thread/${props.thread.id}`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
body: {
|
||||
type: 'text',
|
||||
body: replyBody.value,
|
||||
},
|
||||
},
|
||||
body,
|
||||
})
|
||||
|
||||
replyBody.value = ''
|
||||
|
||||
await updateThreadLocal()
|
||||
if (status !== null) {
|
||||
props.setStatus(status)
|
||||
@@ -332,6 +352,10 @@ const requestedStatus = computed(() => props.project.requested_status ?? 'approv
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.markdown-editor-spacing {
|
||||
margin-bottom: var(--gap-md);
|
||||
}
|
||||
|
||||
.messages {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
45
composables/image-upload.ts
Normal file
45
composables/image-upload.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
type ImageUploadContext = {
|
||||
projectID?: string
|
||||
context: 'project' | 'version' | 'thread_message' | 'report'
|
||||
}
|
||||
|
||||
interface ImageUploadResponse {
|
||||
id: string
|
||||
url: string
|
||||
}
|
||||
|
||||
export const useImageUpload = async (file: File, ctx: ImageUploadContext) => {
|
||||
// Make sure file is of type image/png, image/jpeg, image/gif, or image/webp
|
||||
if (
|
||||
!file.type.startsWith('image/') ||
|
||||
!['png', 'jpeg', 'gif', 'webp'].includes(file.type.split('/')[1])
|
||||
) {
|
||||
throw new Error('File is not an accepted image type')
|
||||
}
|
||||
|
||||
// Make sure file is less than 1MB
|
||||
if (file.size > 1024 * 1024) {
|
||||
throw new Error('File is too large')
|
||||
}
|
||||
|
||||
const qs = new URLSearchParams()
|
||||
if (ctx.projectID) qs.set('project_id', ctx.projectID)
|
||||
qs.set('context', ctx.context)
|
||||
qs.set('ext', file.type.split('/')[1])
|
||||
const url = `image?${qs.toString()}`
|
||||
|
||||
const response = (await useBaseFetch(url, {
|
||||
method: 'POST',
|
||||
body: file,
|
||||
})) as ImageUploadResponse // TODO: zod or object validation
|
||||
|
||||
// Type check to see if response has a url property and an id property
|
||||
if (!response?.id || typeof response.id !== 'string') {
|
||||
throw new Error('Unexpected response from server')
|
||||
}
|
||||
if (!response?.url || typeof response.url !== 'string') {
|
||||
throw new Error('Unexpected response from server')
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
@@ -46,7 +46,7 @@
|
||||
"js-yaml": "^4.1.0",
|
||||
"jszip": "^3.10.1",
|
||||
"markdown-it": "^13.0.1",
|
||||
"omorphia": "^0.6.7",
|
||||
"omorphia": "^0.7.1",
|
||||
"qrcode.vue": "^3.4.0",
|
||||
"semver": "^7.5.4",
|
||||
"vue-multiselect": "^3.0.0-alpha.2",
|
||||
|
||||
@@ -130,12 +130,6 @@
|
||||
<div class="markdown-body" v-html="renderString(licenseText)" />
|
||||
</div>
|
||||
</Modal>
|
||||
<ModalReport
|
||||
v-if="auth.user"
|
||||
ref="modal_project_report"
|
||||
:item-id="project.id"
|
||||
item-type="project"
|
||||
/>
|
||||
<div
|
||||
:class="{
|
||||
'normal-page': true,
|
||||
@@ -258,7 +252,7 @@
|
||||
<hr class="card-divider" />
|
||||
<div class="input-group">
|
||||
<template v-if="auth.user">
|
||||
<button class="iconified-button" @click="$refs.modal_project_report.show()">
|
||||
<button class="iconified-button" @click="() => reportProject(project.id)">
|
||||
<ReportIcon aria-hidden="true" />
|
||||
Report
|
||||
</button>
|
||||
@@ -689,7 +683,6 @@ import Badge from '~/components/ui/Badge.vue'
|
||||
import Categories from '~/components/ui/search/Categories.vue'
|
||||
import EnvironmentIndicator from '~/components/ui/EnvironmentIndicator.vue'
|
||||
import Modal from '~/components/ui/Modal.vue'
|
||||
import ModalReport from '~/components/ui/ModalReport.vue'
|
||||
import NavRow from '~/components/ui/NavRow.vue'
|
||||
import CopyCode from '~/components/ui/CopyCode.vue'
|
||||
import Avatar from '~/components/ui/Avatar.vue'
|
||||
@@ -706,6 +699,7 @@ import LicenseIcon from '~/assets/images/utils/copyright.svg'
|
||||
import GalleryIcon from '~/assets/images/utils/image.svg'
|
||||
import VersionIcon from '~/assets/images/utils/version.svg'
|
||||
import { renderString } from '~/helpers/parse.js'
|
||||
import { reportProject } from '~/utils/report-helpers.ts'
|
||||
import Breadcrumbs from '~/components/ui/Breadcrumbs.vue'
|
||||
|
||||
const data = useNuxtApp()
|
||||
|
||||
@@ -63,9 +63,9 @@
|
||||
<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.
|
||||
This is a private conversation thread with the Modrinth moderators. They may message you
|
||||
with issues concerning this project. Additionally, you are welcome to start a discussion
|
||||
here regarding this project and its status.
|
||||
</p>
|
||||
<ConversationThread
|
||||
v-if="thread"
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
<template>
|
||||
<div>
|
||||
<section class="universal-card">
|
||||
<label for="project-description">
|
||||
<span class="label__title size-card-header">Description</span>
|
||||
<Card>
|
||||
<div class="markdown-disclaimer">
|
||||
<h2>Description</h2>
|
||||
<span class="label__description">
|
||||
You can type an extended description of your mod here. This editor supports
|
||||
<a
|
||||
class="text-link"
|
||||
href="https://docs.modrinth.com/docs/tutorials/markdown/"
|
||||
target="_blank"
|
||||
>Markdown formatting</a
|
||||
>. HTML can also be used inside your description, not including styles, scripts, and
|
||||
iframes (though YouTube iframes are allowed).
|
||||
You can type an extended description of your mod here.
|
||||
<span class="label__subdescription">
|
||||
The description must clearly and honestly describe the purpose and function of the
|
||||
project. See section 2.1 of the
|
||||
@@ -19,21 +12,13 @@
|
||||
for the full requirements.
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<Chips v-model="bodyViewMode" :items="['source', 'preview']" />
|
||||
<div v-if="bodyViewMode === 'source'" class="resizable-textarea-wrapper">
|
||||
<textarea
|
||||
id="project-description"
|
||||
v-model="description"
|
||||
:disabled="(currentMember.permissions & EDIT_BODY) !== EDIT_BODY"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="bodyViewMode === 'preview'"
|
||||
class="markdown-body"
|
||||
v-html="description ? renderHighlightedString(description) : 'No body specified.'"
|
||||
<MarkdownEditor
|
||||
v-model="description"
|
||||
:on-image-upload="onUploadHandler"
|
||||
:disabled="(currentMember.permissions & EDIT_BODY) !== EDIT_BODY"
|
||||
/>
|
||||
<div class="input-group">
|
||||
<div class="input-group markdown-disclaimer">
|
||||
<button
|
||||
type="button"
|
||||
class="iconified-button brand-button"
|
||||
@@ -44,19 +29,23 @@
|
||||
Save changes
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { MarkdownEditor, Card } from 'omorphia'
|
||||
import Chips from '~/components/ui/Chips.vue'
|
||||
import SaveIcon from '~/assets/images/utils/save.svg'
|
||||
import { renderHighlightedString } from '~/helpers/highlight.js'
|
||||
import { useImageUpload } from '~/composables/image-upload.ts'
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
Card,
|
||||
Chips,
|
||||
SaveIcon,
|
||||
MarkdownEditor,
|
||||
},
|
||||
props: {
|
||||
project: {
|
||||
@@ -121,15 +110,23 @@ export default defineNuxtComponent({
|
||||
this.patchProject(this.patchData)
|
||||
}
|
||||
},
|
||||
async onUploadHandler(file) {
|
||||
const response = await useImageUpload(file, {
|
||||
context: 'project',
|
||||
projectID: this.project.id,
|
||||
})
|
||||
return response.url
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.resizable-textarea-wrapper textarea {
|
||||
min-height: 40rem;
|
||||
|
||||
<style scoped>
|
||||
.markdown-disclaimer {
|
||||
margin-block: 1rem;
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
margin-bottom: var(--spacing-card-md);
|
||||
.universal-card {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,12 +9,6 @@
|
||||
proceed-label="Delete"
|
||||
@proceed="deleteVersion()"
|
||||
/>
|
||||
<ModalReport
|
||||
v-if="auth.user"
|
||||
ref="modal_version_report"
|
||||
:item-id="version.id"
|
||||
item-type="version"
|
||||
/>
|
||||
<Modal v-if="auth.user && currentMember" ref="modal_package_mod" header="Package data pack">
|
||||
<div class="modal-package-mod universal-labels">
|
||||
<div class="markdown-body">
|
||||
@@ -157,7 +151,7 @@
|
||||
<ReportIcon aria-hidden="true" />
|
||||
Report
|
||||
</nuxt-link>
|
||||
<button v-else class="iconified-button" @click="$refs.modal_version_report.show()">
|
||||
<button v-else class="iconified-button" @click="() => reportVersion(version.id)">
|
||||
<ReportIcon aria-hidden="true" />
|
||||
Report
|
||||
</button>
|
||||
@@ -195,29 +189,9 @@
|
||||
<div class="version-page__changelog universal-card">
|
||||
<h3>Changelog</h3>
|
||||
<template v-if="isEditing">
|
||||
<span
|
||||
>This editor supports
|
||||
<a
|
||||
class="text-link"
|
||||
href="https://docs.modrinth.com/docs/tutorials/markdown/"
|
||||
target="_blank"
|
||||
>Markdown formatting</a
|
||||
>. HTML can also be used inside your changelog, not including styles, scripts, and
|
||||
iframes.
|
||||
</span>
|
||||
<Chips v-model="changelogViewMode" class="separator" :items="['source', 'preview']" />
|
||||
<div v-if="changelogViewMode === 'source'" class="resizable-textarea-wrapper">
|
||||
<textarea id="body" v-model="version.changelog" maxlength="65536" />
|
||||
<div class="changelog-editor-spacing">
|
||||
<MarkdownEditor v-model="version.changelog" :on-image-upload="onImageUpload" />
|
||||
</div>
|
||||
<div
|
||||
v-if="changelogViewMode === 'preview'"
|
||||
class="markdown-body"
|
||||
v-html="
|
||||
version.changelog
|
||||
? renderHighlightedString(version.changelog)
|
||||
: 'No changelog specified.'
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<div
|
||||
v-else
|
||||
@@ -656,11 +630,14 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { MarkdownEditor } from 'omorphia'
|
||||
import { Multiselect } from 'vue-multiselect'
|
||||
import { acceptFileFromProjectType } from '~/helpers/fileUtils.js'
|
||||
import { inferVersionInfo } from '~/helpers/infer.js'
|
||||
import { createDataPackVersion } from '~/helpers/package.js'
|
||||
import { renderHighlightedString } from '~/helpers/highlight.js'
|
||||
import { reportVersion } from '~/utils/report-helpers.ts'
|
||||
import { useImageUpload } from '~/composables/image-upload.ts'
|
||||
|
||||
import Avatar from '~/components/ui/Avatar.vue'
|
||||
import Badge from '~/components/ui/Badge.vue'
|
||||
@@ -668,7 +645,6 @@ import Breadcrumbs from '~/components/ui/Breadcrumbs.vue'
|
||||
import CopyCode from '~/components/ui/CopyCode.vue'
|
||||
import Categories from '~/components/ui/search/Categories.vue'
|
||||
import ModalConfirm from '~/components/ui/ModalConfirm.vue'
|
||||
import ModalReport from '~/components/ui/ModalReport.vue'
|
||||
import Chips from '~/components/ui/Chips.vue'
|
||||
import Checkbox from '~/components/ui/Checkbox.vue'
|
||||
import FileInput from '~/components/ui/FileInput.vue'
|
||||
@@ -693,6 +669,7 @@ import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg'
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
MarkdownEditor,
|
||||
Modal,
|
||||
FileInput,
|
||||
Checkbox,
|
||||
@@ -717,7 +694,6 @@ export default defineNuxtComponent({
|
||||
Breadcrumbs,
|
||||
CopyCode,
|
||||
ModalConfirm,
|
||||
ModalReport,
|
||||
Multiselect,
|
||||
BoxIcon,
|
||||
RightArrowIcon,
|
||||
@@ -919,6 +895,7 @@ export default defineNuxtComponent({
|
||||
primaryFile: ref(primaryFile),
|
||||
alternateFile: ref(alternateFile),
|
||||
replaceFile: ref(replaceFile),
|
||||
uploadedImageIds: ref([]),
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -927,7 +904,6 @@ export default defineNuxtComponent({
|
||||
newDependencyType: 'required',
|
||||
newDependencyId: '',
|
||||
|
||||
changelogViewMode: 'source',
|
||||
showSnapshots: false,
|
||||
|
||||
newFiles: [],
|
||||
@@ -967,6 +943,14 @@ export default defineNuxtComponent({
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async onImageUpload(file) {
|
||||
const response = await useImageUpload(file, { context: 'version' })
|
||||
|
||||
this.uploadedImageIds.push(response.id)
|
||||
this.uploadedImageIds = this.uploadedImageIds.slice(-10)
|
||||
|
||||
return response.url
|
||||
},
|
||||
getPreviousLink() {
|
||||
if (this.$router.options.history.state.back) {
|
||||
if (
|
||||
@@ -1171,6 +1155,7 @@ export default defineNuxtComponent({
|
||||
}
|
||||
stopLoading()
|
||||
},
|
||||
reportVersion,
|
||||
async createVersion() {
|
||||
this.shouldPreventActions = true
|
||||
startLoading()
|
||||
@@ -1349,6 +1334,10 @@ export default defineNuxtComponent({
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.changelog-editor-spacing {
|
||||
padding-block: var(--gap-md);
|
||||
}
|
||||
|
||||
.version-page {
|
||||
display: grid;
|
||||
|
||||
|
||||
293
pages/report.vue
Normal file
293
pages/report.vue
Normal file
@@ -0,0 +1,293 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<Card>
|
||||
<div class="content">
|
||||
<div>
|
||||
<h1 class="card-title-adjustments">Submit a Report</h1>
|
||||
<div>
|
||||
<p>
|
||||
Modding should be safe for everyone, so we take abuse and malicious intent seriously
|
||||
at Modrinth. If you encounter content that violates our
|
||||
<nuxt-link class="text-link" to="/legal/terms">Terms of Service</nuxt-link> or our
|
||||
<nuxt-link class="text-link" to="/legal/rules">Rules</nuxt-link>, please report it to
|
||||
us here.
|
||||
</p>
|
||||
<p>
|
||||
This form is intended exclusively for reporting abuse or harmful content to Modrinth
|
||||
staff. For bugs related to specific projects, please use the project's designated
|
||||
Issues link or Discord channel.
|
||||
</p>
|
||||
<p>
|
||||
Your privacy is important to us; rest assured that your identifying information will
|
||||
be kept confidential.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="report-info-section">
|
||||
<div class="report-info-item">
|
||||
<label for="report-item">Item type to report</label>
|
||||
<DropdownSelect
|
||||
id="report-item"
|
||||
v-model="reportItem"
|
||||
name="report-item"
|
||||
:options="reportItems"
|
||||
:display-name="capitalizeString"
|
||||
:multiple="false"
|
||||
:searchable="false"
|
||||
:show-no-results="false"
|
||||
:show-labels="false"
|
||||
placeholder="Choose report item"
|
||||
/>
|
||||
</div>
|
||||
<div class="report-info-item">
|
||||
<label for="report-item-id">Item ID</label>
|
||||
<input
|
||||
id="report-item-id"
|
||||
v-model="reportItemID"
|
||||
type="text"
|
||||
placeholder="ex. project ID"
|
||||
autocomplete="off"
|
||||
:disabled="reportItem === ''"
|
||||
/>
|
||||
</div>
|
||||
<div class="report-info-item">
|
||||
<label for="report-type">Reason for report</label>
|
||||
<DropdownSelect
|
||||
id="report-type"
|
||||
v-model="reportType"
|
||||
name="report-type"
|
||||
:options="reportTypes"
|
||||
:multiple="false"
|
||||
:searchable="false"
|
||||
:show-no-results="false"
|
||||
:show-labels="false"
|
||||
:display-name="capitalizeString"
|
||||
placeholder="Choose report type"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="report-submission-section">
|
||||
<div>
|
||||
<p>
|
||||
Please provide additional context about your report. Include links and images if
|
||||
possible. <strong>Empty reports will be closed.</strong>
|
||||
</p>
|
||||
</div>
|
||||
<MarkdownEditor v-model="reportBody" placeholder="" :on-image-upload="onImageUpload" />
|
||||
</div>
|
||||
<div class="submit-button">
|
||||
<Button
|
||||
id="submit-button"
|
||||
color="primary"
|
||||
:disabled="submitLoading || !canSubmit"
|
||||
@click="submitReport"
|
||||
>
|
||||
<SaveIcon />
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Card, Button, MarkdownEditor, DropdownSelect, SaveIcon } from 'omorphia'
|
||||
import { useImageUpload } from '~/composables/image-upload.ts'
|
||||
|
||||
const tags = useTags()
|
||||
const route = useRoute()
|
||||
|
||||
const accessQuery = (id: string): string => {
|
||||
return route.query?.[id]?.toString() || ''
|
||||
}
|
||||
|
||||
const submitLoading = ref<boolean>(false)
|
||||
|
||||
const uploadedImageIDs = ref<string[]>([])
|
||||
|
||||
const reportBody = ref<string>(accessQuery('body'))
|
||||
const reportItem = ref<string>(accessQuery('item'))
|
||||
const reportItemID = ref<string>(accessQuery('itemID'))
|
||||
const reportType = ref<string>('')
|
||||
|
||||
const reportItems = ['project', 'version', 'user']
|
||||
const reportTypes = computed(() => tags.value.reportTypes)
|
||||
|
||||
const canSubmit = computed(() => {
|
||||
return (
|
||||
reportItem.value !== '' &&
|
||||
reportItemID.value !== '' &&
|
||||
reportType.value !== '' &&
|
||||
reportBody.value !== ''
|
||||
)
|
||||
})
|
||||
|
||||
const submissionValidation = () => {
|
||||
if (!canSubmit.value) {
|
||||
throw new Error('Please fill out all required fields')
|
||||
}
|
||||
|
||||
if (reportItem.value === '') {
|
||||
throw new Error('Please select a report item')
|
||||
}
|
||||
|
||||
if (reportItemID.value === '') {
|
||||
throw new Error('Please enter a report item ID')
|
||||
}
|
||||
|
||||
if (reportType.value === '') {
|
||||
throw new Error('Please select a report type')
|
||||
}
|
||||
|
||||
if (reportBody.value === '') {
|
||||
throw new Error('Please enter a report body')
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const capitalizeString = (value?: string) => {
|
||||
if (!value) return ''
|
||||
return value?.charAt(0).toUpperCase() + value?.slice(1)
|
||||
}
|
||||
|
||||
const submitReport = async () => {
|
||||
submitLoading.value = true
|
||||
|
||||
let data: {
|
||||
[key: string]: unknown
|
||||
} = {
|
||||
report_type: reportType.value,
|
||||
item_type: reportItem.value,
|
||||
item_id: reportItemID.value,
|
||||
body: reportBody.value,
|
||||
}
|
||||
|
||||
function takeNLast<T>(arr: T[], n: number): T[] {
|
||||
return arr.slice(Math.max(arr.length - n, 0))
|
||||
}
|
||||
|
||||
if (uploadedImageIDs.value.length > 0) {
|
||||
data = {
|
||||
...data,
|
||||
uploaded_images: takeNLast(uploadedImageIDs.value, 10),
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
submissionValidation()
|
||||
} catch (error) {
|
||||
submitLoading.value = false
|
||||
|
||||
if (error instanceof Error) {
|
||||
addNotification({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: error.message,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = (await useBaseFetch('report', {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
})) as { id: string }
|
||||
|
||||
submitLoading.value = false
|
||||
|
||||
if (response?.id) {
|
||||
navigateTo(`/dashboard/report/${response.id}`)
|
||||
}
|
||||
} catch (error) {
|
||||
submitLoading.value = false
|
||||
|
||||
if (error instanceof Error) {
|
||||
addNotification({
|
||||
group: 'main',
|
||||
title: 'An error occurred',
|
||||
text: error.message,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const onImageUpload = async (file: File) => {
|
||||
const item = await useImageUpload(file, { context: 'report' })
|
||||
uploadedImageIDs.value.push(item.id)
|
||||
return item.url
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.submit-button {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
|
||||
margin-top: var(--spacing-card-md);
|
||||
}
|
||||
|
||||
.card-title-adjustments {
|
||||
margin-block: var(--spacing-card-md) var(--spacing-card-sm);
|
||||
}
|
||||
|
||||
.page {
|
||||
padding: 0.5rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 56rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
// TODO: Get rid of this hack when removing global styles from the website.
|
||||
// Overflow decides the behavior of md editor but also clips the border.
|
||||
// In the future, we should use ring instead of block-shadow for the
|
||||
// green ring around the md editor
|
||||
padding-inline: var(--gap-md);
|
||||
padding-bottom: var(--gap-md);
|
||||
margin-inline: calc(var(--gap-md) * -1);
|
||||
|
||||
display: grid;
|
||||
|
||||
// Disable horizontal stretch
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.report-info-section {
|
||||
display: block;
|
||||
|
||||
width: 100%;
|
||||
gap: var(--gap-md);
|
||||
|
||||
:global(.animated-dropdown) {
|
||||
& > .selected {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.report-info-item {
|
||||
display: block;
|
||||
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: var(--gap-sm);
|
||||
color: var(--color-text-dark);
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-bold);
|
||||
margin-block: var(--spacing-card-md) var(--spacing-card-sm);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<div v-if="user">
|
||||
<ModalCreation ref="modal_creation" />
|
||||
<ModalReport ref="modal_report" :item-id="user.id" item-type="user" />
|
||||
<div class="user-header-wrapper">
|
||||
<div class="user-header">
|
||||
<Avatar
|
||||
@@ -44,7 +43,7 @@
|
||||
<button
|
||||
v-else-if="auth.user"
|
||||
class="iconified-button"
|
||||
@click="$refs.modal_report.show()"
|
||||
@click="() => reportUser(user.id)"
|
||||
>
|
||||
<ReportIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.profileReportButton) }}
|
||||
@@ -264,6 +263,7 @@
|
||||
import { Promotion } from 'omorphia'
|
||||
import ProjectCard from '~/components/ui/ProjectCard.vue'
|
||||
import Badge from '~/components/ui/Badge.vue'
|
||||
import { reportUser } from '~/utils/report-helpers.ts'
|
||||
|
||||
import ReportIcon from '~/assets/images/utils/report.svg'
|
||||
import SunriseIcon from '~/assets/images/utils/sunrise.svg'
|
||||
@@ -280,7 +280,6 @@ import ListIcon from '~/assets/images/utils/list.svg'
|
||||
import ImageIcon from '~/assets/images/utils/image.svg'
|
||||
import UploadIcon from '~/assets/images/utils/upload.svg'
|
||||
import FileInput from '~/components/ui/FileInput.vue'
|
||||
import ModalReport from '~/components/ui/ModalReport.vue'
|
||||
import ModalCreation from '~/components/ui/ModalCreation.vue'
|
||||
import NavRow from '~/components/ui/NavRow.vue'
|
||||
import CopyCode from '~/components/ui/CopyCode.vue'
|
||||
|
||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -35,8 +35,8 @@ dependencies:
|
||||
specifier: ^13.0.1
|
||||
version: 13.0.1(patch_hash=3vlxaukqep4gvqytxeznhg6wbq)
|
||||
omorphia:
|
||||
specifier: ^0.6.7
|
||||
version: 0.6.7(vue@3.3.4)
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.1(vue@3.3.4)
|
||||
qrcode.vue:
|
||||
specifier: ^3.4.0
|
||||
version: 3.4.0(vue@3.3.4)
|
||||
@@ -6965,8 +6965,8 @@ packages:
|
||||
resolution: {integrity: sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==}
|
||||
dev: true
|
||||
|
||||
/omorphia@0.6.7(vue@3.3.4):
|
||||
resolution: {integrity: sha512-ExAgYnNjaUsS3TxG9mipzEszxHzoQyx291lH8iPGhAja3ovseq/bVWgNvTSFlByANaqdotGcyQ+29Ndq7oLgjw==}
|
||||
/omorphia@0.7.1(vue@3.3.4):
|
||||
resolution: {integrity: sha512-YQ+u+V52LxeWaEGjEfQk6h+cF//Q9UNSiQLcpDyacTqmrsPYEQstWriBpapANG3ZsPTyAAdkGZePj5uBdYcuIg==}
|
||||
peerDependencies:
|
||||
vue: ^3.3.4
|
||||
dependencies:
|
||||
|
||||
21
utils/report-helpers.ts
Normal file
21
utils/report-helpers.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
const startReport = (type: string, id: string) => {
|
||||
const prefill = new URLSearchParams()
|
||||
|
||||
// type
|
||||
prefill.set('item', type)
|
||||
prefill.set('itemID', id)
|
||||
|
||||
navigateTo('/report?' + prefill.toString())
|
||||
}
|
||||
|
||||
export const reportProject = (id: string) => {
|
||||
return startReport('project', id)
|
||||
}
|
||||
|
||||
export const reportVersion = (id: string) => {
|
||||
return startReport('version', id)
|
||||
}
|
||||
|
||||
export const reportUser = (id: string) => {
|
||||
return startReport('user', id)
|
||||
}
|
||||
Reference in New Issue
Block a user