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) {
|
:where(input) {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
max-height: 40px;
|
max-height: 40px;
|
||||||
width: 24rem;
|
|
||||||
flex-basis: 24rem;
|
|
||||||
|
|
||||||
&:not(.stylized-toggle) {
|
&:not(.stylized-toggle) {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|||||||
@@ -96,5 +96,6 @@
|
|||||||
|
|
||||||
.normal-page__content {
|
.normal-page__content {
|
||||||
max-width: calc(60rem - 0.75rem);
|
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.
|
This thread is closed and new messages cannot be sent to it.
|
||||||
</span>
|
</span>
|
||||||
<template v-else-if="!report || !report.closed">
|
<template v-else-if="!report || !report.closed">
|
||||||
<div class="resizable-textarea-wrapper">
|
<div class="markdown-editor-spacing">
|
||||||
<Chips v-model="replyViewMode" class="chips" :items="['source', 'preview']" />
|
<MarkdownEditor
|
||||||
<textarea
|
|
||||||
v-if="replyViewMode === 'source'"
|
|
||||||
v-model="replyBody"
|
v-model="replyBody"
|
||||||
:placeholder="sortedMessages.length > 0 ? 'Reply to thread...' : 'Send a message...'"
|
: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>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<button
|
<button
|
||||||
@@ -170,7 +168,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<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 CopyCode from '~/components/ui/CopyCode.vue'
|
||||||
import ReplyIcon from '~/assets/images/utils/reply.svg'
|
import ReplyIcon from '~/assets/images/utils/reply.svg'
|
||||||
import SendIcon from '~/assets/images/utils/send.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 EyeOffIcon from '~/assets/images/utils/eye-off.svg'
|
||||||
import CheckIcon from '~/assets/images/utils/check.svg'
|
import CheckIcon from '~/assets/images/utils/check.svg'
|
||||||
import ModerationIcon from '~/assets/images/sidebar/admin.svg'
|
import ModerationIcon from '~/assets/images/sidebar/admin.svg'
|
||||||
import { renderString } from '~/helpers/parse.js'
|
|
||||||
import ThreadMessage from '~/components/ui/thread/ThreadMessage.vue'
|
import ThreadMessage from '~/components/ui/thread/ThreadMessage.vue'
|
||||||
import { isStaff } from '~/helpers/users.js'
|
import { isStaff } from '~/helpers/users.js'
|
||||||
import { isApproved, isRejected } from '~/helpers/projects.js'
|
import { isApproved, isRejected } from '~/helpers/projects.js'
|
||||||
@@ -233,7 +231,6 @@ const members = computed(() => {
|
|||||||
return members
|
return members
|
||||||
})
|
})
|
||||||
|
|
||||||
const replyViewMode = ref('source')
|
|
||||||
const replyBody = ref('')
|
const replyBody = ref('')
|
||||||
|
|
||||||
const sortedMessages = computed(() => {
|
const sortedMessages = computed(() => {
|
||||||
@@ -261,18 +258,41 @@ async function updateThreadLocal() {
|
|||||||
props.updateThread(thread)
|
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) {
|
async function sendReply(status = null) {
|
||||||
try {
|
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}`, {
|
await useBaseFetch(`thread/${props.thread.id}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: {
|
body,
|
||||||
body: {
|
|
||||||
type: 'text',
|
|
||||||
body: replyBody.value,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
replyBody.value = ''
|
replyBody.value = ''
|
||||||
|
|
||||||
await updateThreadLocal()
|
await updateThreadLocal()
|
||||||
if (status !== null) {
|
if (status !== null) {
|
||||||
props.setStatus(status)
|
props.setStatus(status)
|
||||||
@@ -332,6 +352,10 @@ const requestedStatus = computed(() => props.project.requested_status ?? 'approv
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.markdown-editor-spacing {
|
||||||
|
margin-bottom: var(--gap-md);
|
||||||
|
}
|
||||||
|
|
||||||
.messages {
|
.messages {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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",
|
"js-yaml": "^4.1.0",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.1",
|
||||||
"omorphia": "^0.6.7",
|
"omorphia": "^0.7.1",
|
||||||
"qrcode.vue": "^3.4.0",
|
"qrcode.vue": "^3.4.0",
|
||||||
"semver": "^7.5.4",
|
"semver": "^7.5.4",
|
||||||
"vue-multiselect": "^3.0.0-alpha.2",
|
"vue-multiselect": "^3.0.0-alpha.2",
|
||||||
|
|||||||
@@ -130,12 +130,6 @@
|
|||||||
<div class="markdown-body" v-html="renderString(licenseText)" />
|
<div class="markdown-body" v-html="renderString(licenseText)" />
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
<ModalReport
|
|
||||||
v-if="auth.user"
|
|
||||||
ref="modal_project_report"
|
|
||||||
:item-id="project.id"
|
|
||||||
item-type="project"
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
:class="{
|
:class="{
|
||||||
'normal-page': true,
|
'normal-page': true,
|
||||||
@@ -258,7 +252,7 @@
|
|||||||
<hr class="card-divider" />
|
<hr class="card-divider" />
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<template v-if="auth.user">
|
<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" />
|
<ReportIcon aria-hidden="true" />
|
||||||
Report
|
Report
|
||||||
</button>
|
</button>
|
||||||
@@ -689,7 +683,6 @@ import Badge from '~/components/ui/Badge.vue'
|
|||||||
import Categories from '~/components/ui/search/Categories.vue'
|
import Categories from '~/components/ui/search/Categories.vue'
|
||||||
import EnvironmentIndicator from '~/components/ui/EnvironmentIndicator.vue'
|
import EnvironmentIndicator from '~/components/ui/EnvironmentIndicator.vue'
|
||||||
import Modal from '~/components/ui/Modal.vue'
|
import Modal from '~/components/ui/Modal.vue'
|
||||||
import ModalReport from '~/components/ui/ModalReport.vue'
|
|
||||||
import NavRow from '~/components/ui/NavRow.vue'
|
import NavRow from '~/components/ui/NavRow.vue'
|
||||||
import CopyCode from '~/components/ui/CopyCode.vue'
|
import CopyCode from '~/components/ui/CopyCode.vue'
|
||||||
import Avatar from '~/components/ui/Avatar.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 GalleryIcon from '~/assets/images/utils/image.svg'
|
||||||
import VersionIcon from '~/assets/images/utils/version.svg'
|
import VersionIcon from '~/assets/images/utils/version.svg'
|
||||||
import { renderString } from '~/helpers/parse.js'
|
import { renderString } from '~/helpers/parse.js'
|
||||||
|
import { reportProject } from '~/utils/report-helpers.ts'
|
||||||
import Breadcrumbs from '~/components/ui/Breadcrumbs.vue'
|
import Breadcrumbs from '~/components/ui/Breadcrumbs.vue'
|
||||||
|
|
||||||
const data = useNuxtApp()
|
const data = useNuxtApp()
|
||||||
|
|||||||
@@ -63,9 +63,9 @@
|
|||||||
<section id="messages" class="universal-card">
|
<section id="messages" class="universal-card">
|
||||||
<h2>Messages</h2>
|
<h2>Messages</h2>
|
||||||
<p>
|
<p>
|
||||||
This is a private conversation thread with the Modrinth moderators. They will message you
|
This is a private conversation thread with the Modrinth moderators. They may message you
|
||||||
for issues concerning your project on Modrinth, and you are welcome to message them about
|
with issues concerning this project. Additionally, you are welcome to start a discussion
|
||||||
things concerning your project.
|
here regarding this project and its status.
|
||||||
</p>
|
</p>
|
||||||
<ConversationThread
|
<ConversationThread
|
||||||
v-if="thread"
|
v-if="thread"
|
||||||
|
|||||||
@@ -1,17 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<section class="universal-card">
|
<Card>
|
||||||
<label for="project-description">
|
<div class="markdown-disclaimer">
|
||||||
<span class="label__title size-card-header">Description</span>
|
<h2>Description</h2>
|
||||||
<span class="label__description">
|
<span class="label__description">
|
||||||
You can type an extended description of your mod here. This editor supports
|
You can type an extended description of your mod here.
|
||||||
<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).
|
|
||||||
<span class="label__subdescription">
|
<span class="label__subdescription">
|
||||||
The description must clearly and honestly describe the purpose and function of the
|
The description must clearly and honestly describe the purpose and function of the
|
||||||
project. See section 2.1 of the
|
project. See section 2.1 of the
|
||||||
@@ -19,21 +12,13 @@
|
|||||||
for the full requirements.
|
for the full requirements.
|
||||||
</span>
|
</span>
|
||||||
</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>
|
||||||
<div
|
<MarkdownEditor
|
||||||
v-else-if="bodyViewMode === 'preview'"
|
v-model="description"
|
||||||
class="markdown-body"
|
:on-image-upload="onUploadHandler"
|
||||||
v-html="description ? renderHighlightedString(description) : 'No body specified.'"
|
:disabled="(currentMember.permissions & EDIT_BODY) !== EDIT_BODY"
|
||||||
/>
|
/>
|
||||||
<div class="input-group">
|
<div class="input-group markdown-disclaimer">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="iconified-button brand-button"
|
class="iconified-button brand-button"
|
||||||
@@ -44,19 +29,23 @@
|
|||||||
Save changes
|
Save changes
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { MarkdownEditor, Card } from 'omorphia'
|
||||||
import Chips from '~/components/ui/Chips.vue'
|
import Chips from '~/components/ui/Chips.vue'
|
||||||
import SaveIcon from '~/assets/images/utils/save.svg'
|
import SaveIcon from '~/assets/images/utils/save.svg'
|
||||||
import { renderHighlightedString } from '~/helpers/highlight.js'
|
import { renderHighlightedString } from '~/helpers/highlight.js'
|
||||||
|
import { useImageUpload } from '~/composables/image-upload.ts'
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
export default defineNuxtComponent({
|
||||||
components: {
|
components: {
|
||||||
|
Card,
|
||||||
Chips,
|
Chips,
|
||||||
SaveIcon,
|
SaveIcon,
|
||||||
|
MarkdownEditor,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
project: {
|
project: {
|
||||||
@@ -121,15 +110,23 @@ export default defineNuxtComponent({
|
|||||||
this.patchProject(this.patchData)
|
this.patchProject(this.patchData)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async onUploadHandler(file) {
|
||||||
|
const response = await useImageUpload(file, {
|
||||||
|
context: 'project',
|
||||||
|
projectID: this.project.id,
|
||||||
|
})
|
||||||
|
return response.url
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
|
||||||
.resizable-textarea-wrapper textarea {
|
<style scoped>
|
||||||
min-height: 40rem;
|
.markdown-disclaimer {
|
||||||
|
margin-block: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body {
|
.universal-card {
|
||||||
margin-bottom: var(--spacing-card-md);
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -9,12 +9,6 @@
|
|||||||
proceed-label="Delete"
|
proceed-label="Delete"
|
||||||
@proceed="deleteVersion()"
|
@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">
|
<Modal v-if="auth.user && currentMember" ref="modal_package_mod" header="Package data pack">
|
||||||
<div class="modal-package-mod universal-labels">
|
<div class="modal-package-mod universal-labels">
|
||||||
<div class="markdown-body">
|
<div class="markdown-body">
|
||||||
@@ -157,7 +151,7 @@
|
|||||||
<ReportIcon aria-hidden="true" />
|
<ReportIcon aria-hidden="true" />
|
||||||
Report
|
Report
|
||||||
</nuxt-link>
|
</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" />
|
<ReportIcon aria-hidden="true" />
|
||||||
Report
|
Report
|
||||||
</button>
|
</button>
|
||||||
@@ -195,29 +189,9 @@
|
|||||||
<div class="version-page__changelog universal-card">
|
<div class="version-page__changelog universal-card">
|
||||||
<h3>Changelog</h3>
|
<h3>Changelog</h3>
|
||||||
<template v-if="isEditing">
|
<template v-if="isEditing">
|
||||||
<span
|
<div class="changelog-editor-spacing">
|
||||||
>This editor supports
|
<MarkdownEditor v-model="version.changelog" :on-image-upload="onImageUpload" />
|
||||||
<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>
|
</div>
|
||||||
<div
|
|
||||||
v-if="changelogViewMode === 'preview'"
|
|
||||||
class="markdown-body"
|
|
||||||
v-html="
|
|
||||||
version.changelog
|
|
||||||
? renderHighlightedString(version.changelog)
|
|
||||||
: 'No changelog specified.'
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
@@ -656,11 +630,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
import { MarkdownEditor } from 'omorphia'
|
||||||
import { Multiselect } from 'vue-multiselect'
|
import { Multiselect } from 'vue-multiselect'
|
||||||
import { acceptFileFromProjectType } from '~/helpers/fileUtils.js'
|
import { acceptFileFromProjectType } from '~/helpers/fileUtils.js'
|
||||||
import { inferVersionInfo } from '~/helpers/infer.js'
|
import { inferVersionInfo } from '~/helpers/infer.js'
|
||||||
import { createDataPackVersion } from '~/helpers/package.js'
|
import { createDataPackVersion } from '~/helpers/package.js'
|
||||||
import { renderHighlightedString } from '~/helpers/highlight.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 Avatar from '~/components/ui/Avatar.vue'
|
||||||
import Badge from '~/components/ui/Badge.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 CopyCode from '~/components/ui/CopyCode.vue'
|
||||||
import Categories from '~/components/ui/search/Categories.vue'
|
import Categories from '~/components/ui/search/Categories.vue'
|
||||||
import ModalConfirm from '~/components/ui/ModalConfirm.vue'
|
import ModalConfirm from '~/components/ui/ModalConfirm.vue'
|
||||||
import ModalReport from '~/components/ui/ModalReport.vue'
|
|
||||||
import Chips from '~/components/ui/Chips.vue'
|
import Chips from '~/components/ui/Chips.vue'
|
||||||
import Checkbox from '~/components/ui/Checkbox.vue'
|
import Checkbox from '~/components/ui/Checkbox.vue'
|
||||||
import FileInput from '~/components/ui/FileInput.vue'
|
import FileInput from '~/components/ui/FileInput.vue'
|
||||||
@@ -693,6 +669,7 @@ import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg'
|
|||||||
|
|
||||||
export default defineNuxtComponent({
|
export default defineNuxtComponent({
|
||||||
components: {
|
components: {
|
||||||
|
MarkdownEditor,
|
||||||
Modal,
|
Modal,
|
||||||
FileInput,
|
FileInput,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
@@ -717,7 +694,6 @@ export default defineNuxtComponent({
|
|||||||
Breadcrumbs,
|
Breadcrumbs,
|
||||||
CopyCode,
|
CopyCode,
|
||||||
ModalConfirm,
|
ModalConfirm,
|
||||||
ModalReport,
|
|
||||||
Multiselect,
|
Multiselect,
|
||||||
BoxIcon,
|
BoxIcon,
|
||||||
RightArrowIcon,
|
RightArrowIcon,
|
||||||
@@ -919,6 +895,7 @@ export default defineNuxtComponent({
|
|||||||
primaryFile: ref(primaryFile),
|
primaryFile: ref(primaryFile),
|
||||||
alternateFile: ref(alternateFile),
|
alternateFile: ref(alternateFile),
|
||||||
replaceFile: ref(replaceFile),
|
replaceFile: ref(replaceFile),
|
||||||
|
uploadedImageIds: ref([]),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -927,7 +904,6 @@ export default defineNuxtComponent({
|
|||||||
newDependencyType: 'required',
|
newDependencyType: 'required',
|
||||||
newDependencyId: '',
|
newDependencyId: '',
|
||||||
|
|
||||||
changelogViewMode: 'source',
|
|
||||||
showSnapshots: false,
|
showSnapshots: false,
|
||||||
|
|
||||||
newFiles: [],
|
newFiles: [],
|
||||||
@@ -967,6 +943,14 @@ export default defineNuxtComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
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() {
|
getPreviousLink() {
|
||||||
if (this.$router.options.history.state.back) {
|
if (this.$router.options.history.state.back) {
|
||||||
if (
|
if (
|
||||||
@@ -1171,6 +1155,7 @@ export default defineNuxtComponent({
|
|||||||
}
|
}
|
||||||
stopLoading()
|
stopLoading()
|
||||||
},
|
},
|
||||||
|
reportVersion,
|
||||||
async createVersion() {
|
async createVersion() {
|
||||||
this.shouldPreventActions = true
|
this.shouldPreventActions = true
|
||||||
startLoading()
|
startLoading()
|
||||||
@@ -1349,6 +1334,10 @@ export default defineNuxtComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.changelog-editor-spacing {
|
||||||
|
padding-block: var(--gap-md);
|
||||||
|
}
|
||||||
|
|
||||||
.version-page {
|
.version-page {
|
||||||
display: grid;
|
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>
|
<template>
|
||||||
<div v-if="user">
|
<div v-if="user">
|
||||||
<ModalCreation ref="modal_creation" />
|
<ModalCreation ref="modal_creation" />
|
||||||
<ModalReport ref="modal_report" :item-id="user.id" item-type="user" />
|
|
||||||
<div class="user-header-wrapper">
|
<div class="user-header-wrapper">
|
||||||
<div class="user-header">
|
<div class="user-header">
|
||||||
<Avatar
|
<Avatar
|
||||||
@@ -44,7 +43,7 @@
|
|||||||
<button
|
<button
|
||||||
v-else-if="auth.user"
|
v-else-if="auth.user"
|
||||||
class="iconified-button"
|
class="iconified-button"
|
||||||
@click="$refs.modal_report.show()"
|
@click="() => reportUser(user.id)"
|
||||||
>
|
>
|
||||||
<ReportIcon aria-hidden="true" />
|
<ReportIcon aria-hidden="true" />
|
||||||
{{ formatMessage(messages.profileReportButton) }}
|
{{ formatMessage(messages.profileReportButton) }}
|
||||||
@@ -264,6 +263,7 @@
|
|||||||
import { Promotion } from 'omorphia'
|
import { Promotion } from 'omorphia'
|
||||||
import ProjectCard from '~/components/ui/ProjectCard.vue'
|
import ProjectCard from '~/components/ui/ProjectCard.vue'
|
||||||
import Badge from '~/components/ui/Badge.vue'
|
import Badge from '~/components/ui/Badge.vue'
|
||||||
|
import { reportUser } from '~/utils/report-helpers.ts'
|
||||||
|
|
||||||
import ReportIcon from '~/assets/images/utils/report.svg'
|
import ReportIcon from '~/assets/images/utils/report.svg'
|
||||||
import SunriseIcon from '~/assets/images/utils/sunrise.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 ImageIcon from '~/assets/images/utils/image.svg'
|
||||||
import UploadIcon from '~/assets/images/utils/upload.svg'
|
import UploadIcon from '~/assets/images/utils/upload.svg'
|
||||||
import FileInput from '~/components/ui/FileInput.vue'
|
import FileInput from '~/components/ui/FileInput.vue'
|
||||||
import ModalReport from '~/components/ui/ModalReport.vue'
|
|
||||||
import ModalCreation from '~/components/ui/ModalCreation.vue'
|
import ModalCreation from '~/components/ui/ModalCreation.vue'
|
||||||
import NavRow from '~/components/ui/NavRow.vue'
|
import NavRow from '~/components/ui/NavRow.vue'
|
||||||
import CopyCode from '~/components/ui/CopyCode.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
|
specifier: ^13.0.1
|
||||||
version: 13.0.1(patch_hash=3vlxaukqep4gvqytxeznhg6wbq)
|
version: 13.0.1(patch_hash=3vlxaukqep4gvqytxeznhg6wbq)
|
||||||
omorphia:
|
omorphia:
|
||||||
specifier: ^0.6.7
|
specifier: ^0.7.1
|
||||||
version: 0.6.7(vue@3.3.4)
|
version: 0.7.1(vue@3.3.4)
|
||||||
qrcode.vue:
|
qrcode.vue:
|
||||||
specifier: ^3.4.0
|
specifier: ^3.4.0
|
||||||
version: 3.4.0(vue@3.3.4)
|
version: 3.4.0(vue@3.3.4)
|
||||||
@@ -6965,8 +6965,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==}
|
resolution: {integrity: sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/omorphia@0.6.7(vue@3.3.4):
|
/omorphia@0.7.1(vue@3.3.4):
|
||||||
resolution: {integrity: sha512-ExAgYnNjaUsS3TxG9mipzEszxHzoQyx291lH8iPGhAja3ovseq/bVWgNvTSFlByANaqdotGcyQ+29Ndq7oLgjw==}
|
resolution: {integrity: sha512-YQ+u+V52LxeWaEGjEfQk6h+cF//Q9UNSiQLcpDyacTqmrsPYEQstWriBpapANG3ZsPTyAAdkGZePj5uBdYcuIg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
vue: ^3.3.4
|
vue: ^3.3.4
|
||||||
dependencies:
|
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