1 parent
9bdc965cd5
commit
146b88afce
5 files changed
+551
-6
No files matched your search
@@ -0,0 +1,425 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { SpinnerIcon } from '@modrinth/assets'
|
||||||
|
import { defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
import {
|
||||||
|
type SkinSiteBugComment,
|
||||||
|
skinSiteBugComments,
|
||||||
|
type SkinSiteBugIssue,
|
||||||
|
skinSiteBugIssue,
|
||||||
|
type SkinSiteBugIssueType,
|
||||||
|
type SkinSiteBugPriority,
|
||||||
|
type SkinSiteBugResolution,
|
||||||
|
type SkinSiteBugStatus,
|
||||||
|
skinSiteCreateBugComment,
|
||||||
|
skinSiteDeleteBugComment,
|
||||||
|
} from '@/helpers/skin-site-api'
|
||||||
|
|
||||||
|
const props = defineProps<{ issueId: number }>()
|
||||||
|
const emit = defineEmits<{ (e: 'back'): void }>()
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
const { handleError, addNotification } = injectNotificationManager()
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
back: { id: 'app.skin-site.bug-detail.back', defaultMessage: 'Back to list' },
|
||||||
|
loading: { id: 'app.skin-site.bug-detail.loading', defaultMessage: 'Loading…' },
|
||||||
|
failed: { id: 'app.skin-site.bug-detail.failed', defaultMessage: 'Could not load this issue.' },
|
||||||
|
issueNo: { id: 'app.skin-site.bug-detail.issue-no', defaultMessage: 'Issue #{id}' },
|
||||||
|
by: { id: 'app.skin-site.bug-detail.by', defaultMessage: 'by {name}' },
|
||||||
|
assignee: { id: 'app.skin-site.bug-detail.assignee', defaultMessage: 'Assignee: {name}' },
|
||||||
|
unassigned: { id: 'app.skin-site.bug-detail.unassigned', defaultMessage: 'Unassigned' },
|
||||||
|
descriptionEmpty: {
|
||||||
|
id: 'app.skin-site.bug-detail.description-empty',
|
||||||
|
defaultMessage: 'No description provided.',
|
||||||
|
},
|
||||||
|
resolution: { id: 'app.skin-site.bug-detail.resolution', defaultMessage: 'Resolution: {name}' },
|
||||||
|
commentsTitle: { id: 'app.skin-site.bug-detail.comments-title', defaultMessage: 'Comments' },
|
||||||
|
commentsEmpty: {
|
||||||
|
id: 'app.skin-site.bug-detail.comments-empty',
|
||||||
|
defaultMessage: 'No comments yet. Be the first to reply.',
|
||||||
|
},
|
||||||
|
commentsLoading: {
|
||||||
|
id: 'app.skin-site.bug-detail.comments-loading',
|
||||||
|
defaultMessage: 'Loading comments…',
|
||||||
|
},
|
||||||
|
commentPlaceholder: {
|
||||||
|
id: 'app.skin-site.bug-detail.comment-placeholder',
|
||||||
|
defaultMessage: 'Write a comment…',
|
||||||
|
},
|
||||||
|
commentSubmit: { id: 'app.skin-site.bug-detail.comment-submit', defaultMessage: 'Comment' },
|
||||||
|
commentSending: { id: 'app.skin-site.bug-detail.comment-sending', defaultMessage: 'Sending…' },
|
||||||
|
commentEmptyError: {
|
||||||
|
id: 'app.skin-site.bug-detail.comment-empty-error',
|
||||||
|
defaultMessage: 'Comment cannot be empty.',
|
||||||
|
},
|
||||||
|
commentPosted: {
|
||||||
|
id: 'app.skin-site.bug-detail.comment-posted',
|
||||||
|
defaultMessage: 'Comment posted.',
|
||||||
|
},
|
||||||
|
commentDeleted: {
|
||||||
|
id: 'app.skin-site.bug-detail.comment-deleted',
|
||||||
|
defaultMessage: 'Comment deleted.',
|
||||||
|
},
|
||||||
|
delete: { id: 'app.skin-site.bug-detail.delete', defaultMessage: 'Delete' },
|
||||||
|
deleting: { id: 'app.skin-site.bug-detail.deleting', defaultMessage: 'Deleting…' },
|
||||||
|
system: { id: 'app.skin-site.bug-detail.system', defaultMessage: 'System' },
|
||||||
|
typeBug: { id: 'app.skin-site.bugs.type.bug', defaultMessage: 'Bug' },
|
||||||
|
typeFeature: { id: 'app.skin-site.bugs.type.feature', defaultMessage: 'Feature' },
|
||||||
|
typeImprovement: { id: 'app.skin-site.bugs.type.improvement', defaultMessage: 'Improvement' },
|
||||||
|
statusPending: { id: 'app.skin-site.bugs.status.pending', defaultMessage: 'Pending' },
|
||||||
|
statusConfirmed: { id: 'app.skin-site.bugs.status.confirmed', defaultMessage: 'Confirmed' },
|
||||||
|
statusNeedsInfo: { id: 'app.skin-site.bugs.status.needs-info', defaultMessage: 'Needs info' },
|
||||||
|
statusInProgress: { id: 'app.skin-site.bugs.status.in-progress', defaultMessage: 'In progress' },
|
||||||
|
statusVerifying: { id: 'app.skin-site.bugs.status.verifying', defaultMessage: 'Verifying' },
|
||||||
|
statusClosed: { id: 'app.skin-site.bugs.status.closed', defaultMessage: 'Closed' },
|
||||||
|
priorityCritical: { id: 'app.skin-site.bugs.priority.critical', defaultMessage: 'Critical' },
|
||||||
|
priorityHigh: { id: 'app.skin-site.bugs.priority.high', defaultMessage: 'High' },
|
||||||
|
priorityNormal: { id: 'app.skin-site.bugs.priority.normal', defaultMessage: 'Normal' },
|
||||||
|
priorityLow: { id: 'app.skin-site.bugs.priority.low', defaultMessage: 'Low' },
|
||||||
|
resFixed: { id: 'app.skin-site.bugs.resolution.fixed', defaultMessage: 'Fixed' },
|
||||||
|
resWontFix: { id: 'app.skin-site.bugs.resolution.wont-fix', defaultMessage: 'Won’t fix' },
|
||||||
|
resByDesign: { id: 'app.skin-site.bugs.resolution.by-design', defaultMessage: 'By design' },
|
||||||
|
resDuplicate: { id: 'app.skin-site.bugs.resolution.duplicate', defaultMessage: 'Duplicate' },
|
||||||
|
resCannotReproduce: {
|
||||||
|
id: 'app.skin-site.bugs.resolution.cannot-reproduce',
|
||||||
|
defaultMessage: 'Cannot reproduce',
|
||||||
|
},
|
||||||
|
resInvalid: { id: 'app.skin-site.bugs.resolution.invalid', defaultMessage: 'Invalid' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const issue = ref<SkinSiteBugIssue | null>(null)
|
||||||
|
const loading = ref(true)
|
||||||
|
const failed = ref(false)
|
||||||
|
|
||||||
|
const comments = ref<SkinSiteBugComment[]>([])
|
||||||
|
const commentsLoading = ref(true)
|
||||||
|
const commentsHasMore = ref(false)
|
||||||
|
const commentsLoadingMore = ref(false)
|
||||||
|
const commentDraft = ref('')
|
||||||
|
const commentSending = ref(false)
|
||||||
|
const deletingId = ref<number | null>(null)
|
||||||
|
|
||||||
|
function typeLabel(type: SkinSiteBugIssueType): string {
|
||||||
|
switch (type) {
|
||||||
|
case 'FEATURE':
|
||||||
|
return formatMessage(messages.typeFeature)
|
||||||
|
case 'IMPROVEMENT':
|
||||||
|
return formatMessage(messages.typeImprovement)
|
||||||
|
default:
|
||||||
|
return formatMessage(messages.typeBug)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusLabel(status: SkinSiteBugStatus): string {
|
||||||
|
switch (status) {
|
||||||
|
case 'CONFIRMED':
|
||||||
|
return formatMessage(messages.statusConfirmed)
|
||||||
|
case 'NEEDS_INFO':
|
||||||
|
return formatMessage(messages.statusNeedsInfo)
|
||||||
|
case 'IN_PROGRESS':
|
||||||
|
return formatMessage(messages.statusInProgress)
|
||||||
|
case 'VERIFYING':
|
||||||
|
return formatMessage(messages.statusVerifying)
|
||||||
|
case 'CLOSED':
|
||||||
|
return formatMessage(messages.statusClosed)
|
||||||
|
default:
|
||||||
|
return formatMessage(messages.statusPending)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function priorityLabel(priority: SkinSiteBugPriority): string {
|
||||||
|
switch (priority) {
|
||||||
|
case 'CRITICAL':
|
||||||
|
return formatMessage(messages.priorityCritical)
|
||||||
|
case 'HIGH':
|
||||||
|
return formatMessage(messages.priorityHigh)
|
||||||
|
case 'LOW':
|
||||||
|
return formatMessage(messages.priorityLow)
|
||||||
|
default:
|
||||||
|
return formatMessage(messages.priorityNormal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolutionLabel(resolution: SkinSiteBugResolution): string {
|
||||||
|
switch (resolution) {
|
||||||
|
case 'WONT_FIX':
|
||||||
|
return formatMessage(messages.resWontFix)
|
||||||
|
case 'BY_DESIGN':
|
||||||
|
return formatMessage(messages.resByDesign)
|
||||||
|
case 'DUPLICATE':
|
||||||
|
return formatMessage(messages.resDuplicate)
|
||||||
|
case 'CANNOT_REPRODUCE':
|
||||||
|
return formatMessage(messages.resCannotReproduce)
|
||||||
|
case 'INVALID':
|
||||||
|
return formatMessage(messages.resInvalid)
|
||||||
|
default:
|
||||||
|
return formatMessage(messages.resFixed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(ts: number): string {
|
||||||
|
try {
|
||||||
|
return new Date(ts).toLocaleString()
|
||||||
|
} catch {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const canSubmit = computed(() => !commentSending.value && commentDraft.value.trim().length > 0)
|
||||||
|
|
||||||
|
async function loadIssue() {
|
||||||
|
loading.value = true
|
||||||
|
failed.value = false
|
||||||
|
try {
|
||||||
|
const result = await skinSiteBugIssue(props.issueId)
|
||||||
|
if (!result.success) throw new Error(result.errorMessage)
|
||||||
|
issue.value = result.payload
|
||||||
|
} catch (error) {
|
||||||
|
failed.value = true
|
||||||
|
handleError(error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadComments(reset = true) {
|
||||||
|
if (reset) {
|
||||||
|
commentsLoading.value = true
|
||||||
|
comments.value = []
|
||||||
|
} else {
|
||||||
|
commentsLoadingMore.value = true
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await skinSiteBugComments(props.issueId, reset ? 0 : comments.value.length, 50)
|
||||||
|
if (!result.success) throw new Error(result.errorMessage)
|
||||||
|
const page = result.payload
|
||||||
|
comments.value = reset ? page.items : [...comments.value, ...page.items]
|
||||||
|
commentsHasMore.value = page.hasMore
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error)
|
||||||
|
} finally {
|
||||||
|
commentsLoading.value = false
|
||||||
|
commentsLoadingMore.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitComment() {
|
||||||
|
const content = commentDraft.value.trim()
|
||||||
|
if (!content || commentSending.value) return
|
||||||
|
commentSending.value = true
|
||||||
|
try {
|
||||||
|
const result = await skinSiteCreateBugComment(props.issueId, content)
|
||||||
|
if (!result.success) throw new Error(result.errorMessage)
|
||||||
|
comments.value = [...comments.value, result.payload]
|
||||||
|
commentDraft.value = ''
|
||||||
|
addNotification({ type: 'success', title: formatMessage(messages.commentPosted) })
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error)
|
||||||
|
} finally {
|
||||||
|
commentSending.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteComment(comment: SkinSiteBugComment) {
|
||||||
|
if (deletingId.value !== null) return
|
||||||
|
deletingId.value = comment.id
|
||||||
|
try {
|
||||||
|
const result = await skinSiteDeleteBugComment(comment.id)
|
||||||
|
if (!result.success) throw new Error(result.errorMessage)
|
||||||
|
comments.value = comments.value.filter((c) => c.id !== comment.id)
|
||||||
|
addNotification({ type: 'success', title: formatMessage(messages.commentDeleted) })
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error)
|
||||||
|
} finally {
|
||||||
|
deletingId.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void loadIssue()
|
||||||
|
void loadComments(true)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex w-fit items-center gap-1 rounded-full bg-button-bg px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110"
|
||||||
|
@click="emit('back')"
|
||||||
|
>
|
||||||
|
← {{ formatMessage(messages.back) }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div v-if="loading" class="flex items-center gap-2 text-sm text-secondary">
|
||||||
|
<SpinnerIcon class="size-4 animate-spin" />
|
||||||
|
{{ formatMessage(messages.loading) }}
|
||||||
|
</div>
|
||||||
|
<p v-else-if="failed" class="m-0 text-sm text-secondary">
|
||||||
|
{{ formatMessage(messages.failed) }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<template v-else-if="issue">
|
||||||
|
<header
|
||||||
|
class="flex flex-col gap-2 rounded-xl border border-solid border-surface-5 bg-bg-raised p-4"
|
||||||
|
>
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<span
|
||||||
|
class="shrink-0 rounded-full bg-button-bg px-2 py-0.5 text-[10px] font-semibold text-secondary"
|
||||||
|
>
|
||||||
|
{{ typeLabel(issue.type) }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold"
|
||||||
|
:class="{
|
||||||
|
'bg-green text-contrast':
|
||||||
|
issue.status === 'CONFIRMED' || issue.status === 'VERIFYING',
|
||||||
|
'bg-yellow text-contrast':
|
||||||
|
issue.status === 'NEEDS_INFO' || issue.status === 'IN_PROGRESS',
|
||||||
|
'bg-button-bg text-secondary': issue.status === 'PENDING',
|
||||||
|
'bg-surface-5 text-secondary': issue.status === 'CLOSED',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ statusLabel(issue.status) }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold"
|
||||||
|
:class="{
|
||||||
|
'bg-red text-contrast': issue.priority === 'CRITICAL',
|
||||||
|
'bg-orange text-contrast': issue.priority === 'HIGH',
|
||||||
|
'bg-button-bg text-secondary':
|
||||||
|
issue.priority === 'NORMAL' || issue.priority === 'LOW',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ priorityLabel(issue.priority) }}
|
||||||
|
</span>
|
||||||
|
<span v-if="issue.private" class="shrink-0 text-[10px] font-semibold text-secondary">
|
||||||
|
🔒
|
||||||
|
</span>
|
||||||
|
<span class="min-w-0 flex-1 truncate text-xs text-secondary">
|
||||||
|
{{ formatMessage(messages.issueNo, { id: issue.id }) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="m-0 text-xl font-bold text-contrast">{{ issue.title }}</h1>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-3 text-xs text-secondary">
|
||||||
|
<span>{{ formatMessage(messages.by, { name: issue.authorName }) }}</span>
|
||||||
|
<span>{{ formatDateTime(issue.createdAt) }}</span>
|
||||||
|
<span v-if="issue.assigneeName">
|
||||||
|
{{ formatMessage(messages.assignee, { name: issue.assigneeName }) }}
|
||||||
|
</span>
|
||||||
|
<span v-else>{{ formatMessage(messages.unassigned) }}</span>
|
||||||
|
</div>
|
||||||
|
<span v-if="issue.resolution" class="text-xs font-semibold text-secondary">
|
||||||
|
{{ formatMessage(messages.resolution, { name: resolutionLabel(issue.resolution) }) }}
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="rounded-xl border border-solid border-surface-5 bg-bg-raised p-4">
|
||||||
|
<p
|
||||||
|
v-if="issue.description"
|
||||||
|
class="m-0 whitespace-pre-wrap break-words text-sm text-contrast"
|
||||||
|
>
|
||||||
|
{{ issue.description }}
|
||||||
|
</p>
|
||||||
|
<p v-else class="m-0 text-sm text-secondary">
|
||||||
|
{{ formatMessage(messages.descriptionEmpty) }}
|
||||||
|
</p>
|
||||||
|
<div v-if="issue.images.length" class="mt-3 flex flex-wrap gap-2">
|
||||||
|
<span
|
||||||
|
v-for="image in issue.images"
|
||||||
|
:key="image.hash"
|
||||||
|
class="rounded-lg bg-surface-5 px-2 py-1 text-[10px] font-mono text-secondary"
|
||||||
|
>
|
||||||
|
{{ image.hash }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="flex flex-col gap-3">
|
||||||
|
<h2 class="m-0 text-base font-bold text-contrast">
|
||||||
|
{{ formatMessage(messages.commentsTitle) }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div v-if="commentsLoading" class="flex items-center gap-2 text-sm text-secondary">
|
||||||
|
<SpinnerIcon class="size-4 animate-spin" />
|
||||||
|
{{ formatMessage(messages.commentsLoading) }}
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
v-else-if="!comments.length"
|
||||||
|
class="m-0 rounded-xl border border-solid border-surface-5 bg-bg-raised p-4 text-sm text-secondary"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.commentsEmpty) }}
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
v-for="comment in comments"
|
||||||
|
v-else
|
||||||
|
:key="comment.id"
|
||||||
|
class="flex flex-col gap-1.5 rounded-xl border border-solid border-surface-5 bg-bg-raised p-3"
|
||||||
|
>
|
||||||
|
<div class="flex flex-wrap items-center gap-2 text-xs text-secondary">
|
||||||
|
<span class="font-semibold text-contrast">{{ comment.authorName }}</span>
|
||||||
|
<span
|
||||||
|
v-if="comment.system"
|
||||||
|
class="rounded-full bg-button-bg px-2 py-0.5 text-[10px] font-semibold"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.system) }}
|
||||||
|
</span>
|
||||||
|
<span>{{ formatDateTime(comment.createdAt) }}</span>
|
||||||
|
<span v-if="comment.editedAt">(edited)</span>
|
||||||
|
<button
|
||||||
|
v-if="comment.canDelete"
|
||||||
|
type="button"
|
||||||
|
class="ml-auto rounded-full bg-button-bg px-2 py-0.5 text-[10px] font-semibold text-secondary hover:brightness-110 disabled:opacity-50"
|
||||||
|
:disabled="deletingId !== null"
|
||||||
|
@click="deleteComment(comment)"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
deletingId === comment.id
|
||||||
|
? formatMessage(messages.deleting)
|
||||||
|
: formatMessage(messages.delete)
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="m-0 whitespace-pre-wrap break-words text-sm text-contrast">
|
||||||
|
{{ comment.content }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="commentsHasMore"
|
||||||
|
type="button"
|
||||||
|
class="w-fit self-center rounded-full bg-button-bg px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110 disabled:opacity-50"
|
||||||
|
:disabled="commentsLoadingMore"
|
||||||
|
@click="loadComments(false)"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.commentsTitle) }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<form class="flex flex-col gap-2" @submit.prevent="submitComment">
|
||||||
|
<textarea
|
||||||
|
v-model="commentDraft"
|
||||||
|
rows="3"
|
||||||
|
class="w-full resize-y rounded-xl border border-solid border-surface-5 bg-bg-raised p-3 text-sm text-contrast outline-none focus:border-brand"
|
||||||
|
:placeholder="formatMessage(messages.commentPlaceholder)"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="w-fit self-end rounded-full bg-brand px-4 py-2 text-sm font-semibold text-contrast disabled:opacity-50"
|
||||||
|
:disabled="!canSubmit"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
commentSending
|
||||||
|
? formatMessage(messages.commentSending)
|
||||||
|
: formatMessage(messages.commentSubmit)
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -42,7 +42,7 @@ const messages = defineMessages({
|
|||||||
priorityNormal: { id: 'app.skin-site.bugs.priority.normal', defaultMessage: 'Normal' },
|
priorityNormal: { id: 'app.skin-site.bugs.priority.normal', defaultMessage: 'Normal' },
|
||||||
priorityLow: { id: 'app.skin-site.bugs.priority.low', defaultMessage: 'Low' },
|
priorityLow: { id: 'app.skin-site.bugs.priority.low', defaultMessage: 'Low' },
|
||||||
})
|
})
|
||||||
const emit = defineEmits<{ (e: 'back'): void }>()
|
const emit = defineEmits<{ (e: 'back'): void; (e: 'open', id: number): void }>()
|
||||||
|
|
||||||
const items = ref<SkinSiteBugIssue[]>([])
|
const items = ref<SkinSiteBugIssue[]>([])
|
||||||
const hasMore = ref(false)
|
const hasMore = ref(false)
|
||||||
@@ -221,7 +221,8 @@ onMounted(() => load(true))
|
|||||||
<div
|
<div
|
||||||
v-for="issue in items"
|
v-for="issue in items"
|
||||||
:key="issue.id"
|
:key="issue.id"
|
||||||
class="flex flex-col gap-1.5 rounded-xl border border-solid border-surface-5 bg-bg-raised p-3"
|
class="flex cursor-pointer flex-col gap-1.5 rounded-xl border border-solid border-surface-5 bg-bg-raised p-3 hover:border-brand"
|
||||||
|
@click="emit('open', issue.id)"
|
||||||
>
|
>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ref } from 'vue'
|
|||||||
|
|
||||||
import SkinSiteAccountCard from './SkinSiteAccountCard.vue'
|
import SkinSiteAccountCard from './SkinSiteAccountCard.vue'
|
||||||
import SkinSiteAnnouncements from './SkinSiteAnnouncements.vue'
|
import SkinSiteAnnouncements from './SkinSiteAnnouncements.vue'
|
||||||
|
import SkinSiteBugDetailView from './SkinSiteBugDetailView.vue'
|
||||||
import SkinSiteBugListView from './SkinSiteBugListView.vue'
|
import SkinSiteBugListView from './SkinSiteBugListView.vue'
|
||||||
import SkinSiteDonationCard from './SkinSiteDonationCard.vue'
|
import SkinSiteDonationCard from './SkinSiteDonationCard.vue'
|
||||||
import SkinSiteDownloadsView from './SkinSiteDownloadsView.vue'
|
import SkinSiteDownloadsView from './SkinSiteDownloadsView.vue'
|
||||||
@@ -31,7 +32,20 @@ const messages = defineMessages({
|
|||||||
const YGGDRASIL_URL = 'https://skin.starlight.cool/yggdrasil'
|
const YGGDRASIL_URL = 'https://skin.starlight.cool/yggdrasil'
|
||||||
const loginModal = ref<InstanceType<typeof SkinSiteLoginModal>>()
|
const loginModal = ref<InstanceType<typeof SkinSiteLoginModal>>()
|
||||||
const profileModal = ref<InstanceType<typeof SkinSiteProfileModal>>()
|
const profileModal = ref<InstanceType<typeof SkinSiteProfileModal>>()
|
||||||
const view = ref<'home' | 'players' | 'downloads' | 'map' | 'review' | 'bugs'>('home')
|
const view = ref<'home' | 'players' | 'downloads' | 'map' | 'review' | 'bugs' | 'bug-detail'>(
|
||||||
|
'home',
|
||||||
|
)
|
||||||
|
const selectedBugId = ref<number | null>(null)
|
||||||
|
|
||||||
|
function openBug(id: number) {
|
||||||
|
selectedBugId.value = id
|
||||||
|
view.value = 'bug-detail'
|
||||||
|
}
|
||||||
|
|
||||||
|
function backToBugList() {
|
||||||
|
selectedBugId.value = null
|
||||||
|
view.value = 'bugs'
|
||||||
|
}
|
||||||
|
|
||||||
async function copyAddress() {
|
async function copyAddress() {
|
||||||
try {
|
try {
|
||||||
@@ -50,7 +64,12 @@ async function copyAddress() {
|
|||||||
<SkinSiteDownloadsView v-else-if="view === 'downloads'" @back="view = 'home'" />
|
<SkinSiteDownloadsView v-else-if="view === 'downloads'" @back="view = 'home'" />
|
||||||
<SkinSiteMapView v-else-if="view === 'map'" @back="view = 'home'" />
|
<SkinSiteMapView v-else-if="view === 'map'" @back="view = 'home'" />
|
||||||
<SkinSiteReviewView v-else-if="view === 'review'" @back="view = 'home'" />
|
<SkinSiteReviewView v-else-if="view === 'review'" @back="view = 'home'" />
|
||||||
<SkinSiteBugListView v-else-if="view === 'bugs'" @back="view = 'home'" />
|
<SkinSiteBugListView v-else-if="view === 'bugs'" @back="view = 'home'" @open="openBug" />
|
||||||
|
<SkinSiteBugDetailView
|
||||||
|
v-else-if="view === 'bug-detail' && selectedBugId !== null"
|
||||||
|
:issue-id="selectedBugId"
|
||||||
|
@back="backToBugList"
|
||||||
|
/>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<header class="flex flex-col items-center gap-1 text-center">
|
<header class="flex flex-col items-center gap-1 text-center">
|
||||||
|
|||||||
@@ -11687,5 +11687,55 @@
|
|||||||
"app.shortcut-settings.page-up-action-description": {"message": "Moves the page you are reading up by one screen."},
|
"app.shortcut-settings.page-up-action-description": {"message": "Moves the page you are reading up by one screen."},
|
||||||
"app.shortcut-settings.scroll-description": {"message": "Move through long pages with the keyboard."},
|
"app.shortcut-settings.scroll-description": {"message": "Move through long pages with the keyboard."},
|
||||||
"app.shortcut-settings.scroll-title": {"message": "Quick scrolling"},
|
"app.shortcut-settings.scroll-title": {"message": "Quick scrolling"},
|
||||||
"app.settings.tabs.shortcut-settings": {"message": "Keyboard shortcuts"}
|
"app.settings.tabs.shortcut-settings": {"message": "Keyboard shortcuts"},
|
||||||
|
"app.skin-site.bugs.title": {"message": "Bugs & suggestions"},
|
||||||
|
"app.skin-site.bugs.back": {"message": "Back to home"},
|
||||||
|
"app.skin-site.bugs.subtitle": {"message": "Report issues and follow their progress."},
|
||||||
|
"app.skin-site.bugs.loading": {"message": "Loading…"},
|
||||||
|
"app.skin-site.bugs.failed": {"message": "Could not load issues."},
|
||||||
|
"app.skin-site.bugs.empty": {"message": "No issues match these filters."},
|
||||||
|
"app.skin-site.bugs.all": {"message": "All"},
|
||||||
|
"app.skin-site.bugs.more": {"message": "Load more"},
|
||||||
|
"app.skin-site.bugs.open-only": {"message": "Open only"},
|
||||||
|
"app.skin-site.bugs.comments": {"message": "{count} comments"},
|
||||||
|
"app.skin-site.bugs.type.bug": {"message": "Bug"},
|
||||||
|
"app.skin-site.bugs.type.feature": {"message": "Feature"},
|
||||||
|
"app.skin-site.bugs.type.improvement": {"message": "Improvement"},
|
||||||
|
"app.skin-site.bugs.status.pending": {"message": "Pending"},
|
||||||
|
"app.skin-site.bugs.status.confirmed": {"message": "Confirmed"},
|
||||||
|
"app.skin-site.bugs.status.needs-info": {"message": "Needs info"},
|
||||||
|
"app.skin-site.bugs.status.in-progress": {"message": "In progress"},
|
||||||
|
"app.skin-site.bugs.status.verifying": {"message": "Verifying"},
|
||||||
|
"app.skin-site.bugs.status.closed": {"message": "Closed"},
|
||||||
|
"app.skin-site.bugs.priority.critical": {"message": "Critical"},
|
||||||
|
"app.skin-site.bugs.priority.high": {"message": "High"},
|
||||||
|
"app.skin-site.bugs.priority.normal": {"message": "Normal"},
|
||||||
|
"app.skin-site.bugs.priority.low": {"message": "Low"},
|
||||||
|
"app.skin-site.bugs.resolution.fixed": {"message": "Fixed"},
|
||||||
|
"app.skin-site.bugs.resolution.wont-fix": {"message": "Won’t fix"},
|
||||||
|
"app.skin-site.bugs.resolution.by-design": {"message": "By design"},
|
||||||
|
"app.skin-site.bugs.resolution.duplicate": {"message": "Duplicate"},
|
||||||
|
"app.skin-site.bugs.resolution.cannot-reproduce": {"message": "Cannot reproduce"},
|
||||||
|
"app.skin-site.bugs.resolution.invalid": {"message": "Invalid"},
|
||||||
|
"app.skin-site.bug-detail.back": {"message": "Back to list"},
|
||||||
|
"app.skin-site.bug-detail.loading": {"message": "Loading…"},
|
||||||
|
"app.skin-site.bug-detail.failed": {"message": "Could not load this issue."},
|
||||||
|
"app.skin-site.bug-detail.issue-no": {"message": "Issue #{id}"},
|
||||||
|
"app.skin-site.bug-detail.by": {"message": "by {name}"},
|
||||||
|
"app.skin-site.bug-detail.assignee": {"message": "Assignee: {name}"},
|
||||||
|
"app.skin-site.bug-detail.unassigned": {"message": "Unassigned"},
|
||||||
|
"app.skin-site.bug-detail.description-empty": {"message": "No description provided."},
|
||||||
|
"app.skin-site.bug-detail.resolution": {"message": "Resolution: {name}"},
|
||||||
|
"app.skin-site.bug-detail.comments-title": {"message": "Comments"},
|
||||||
|
"app.skin-site.bug-detail.comments-empty": {"message": "No comments yet. Be the first to reply."},
|
||||||
|
"app.skin-site.bug-detail.comments-loading": {"message": "Loading comments…"},
|
||||||
|
"app.skin-site.bug-detail.comment-placeholder": {"message": "Write a comment…"},
|
||||||
|
"app.skin-site.bug-detail.comment-submit": {"message": "Comment"},
|
||||||
|
"app.skin-site.bug-detail.comment-sending": {"message": "Sending…"},
|
||||||
|
"app.skin-site.bug-detail.comment-empty-error": {"message": "Comment cannot be empty."},
|
||||||
|
"app.skin-site.bug-detail.comment-posted": {"message": "Comment posted."},
|
||||||
|
"app.skin-site.bug-detail.comment-deleted": {"message": "Comment deleted."},
|
||||||
|
"app.skin-site.bug-detail.delete": {"message": "Delete"},
|
||||||
|
"app.skin-site.bug-detail.deleting": {"message": "Deleting…"},
|
||||||
|
"app.skin-site.bug-detail.system": {"message": "System"}
|
||||||
}
|
}
|
||||||
@@ -11518,5 +11518,55 @@
|
|||||||
"app.shortcut-settings.page-up-action-description": {"message": "将当前页面向上移动一屏。"},
|
"app.shortcut-settings.page-up-action-description": {"message": "将当前页面向上移动一屏。"},
|
||||||
"app.shortcut-settings.scroll-description": {"message": "使用键盘在长页面中快速滚动。"},
|
"app.shortcut-settings.scroll-description": {"message": "使用键盘在长页面中快速滚动。"},
|
||||||
"app.shortcut-settings.scroll-title": {"message": "快速滚动"},
|
"app.shortcut-settings.scroll-title": {"message": "快速滚动"},
|
||||||
"app.settings.tabs.shortcut-settings": {"message": "键盘快捷键"}
|
"app.settings.tabs.shortcut-settings": {"message": "键盘快捷键"},
|
||||||
|
"app.skin-site.bugs.title": {"message": "问题与建议"},
|
||||||
|
"app.skin-site.bugs.back": {"message": "返回主页"},
|
||||||
|
"app.skin-site.bugs.subtitle": {"message": "反馈问题并跟踪处理进度。"},
|
||||||
|
"app.skin-site.bugs.loading": {"message": "加载中…"},
|
||||||
|
"app.skin-site.bugs.failed": {"message": "无法加载问题列表。"},
|
||||||
|
"app.skin-site.bugs.empty": {"message": "没有符合筛选条件的问题。"},
|
||||||
|
"app.skin-site.bugs.all": {"message": "全部"},
|
||||||
|
"app.skin-site.bugs.more": {"message": "加载更多"},
|
||||||
|
"app.skin-site.bugs.open-only": {"message": "仅显示未关闭"},
|
||||||
|
"app.skin-site.bugs.comments": {"message": "{count} 条评论"},
|
||||||
|
"app.skin-site.bugs.type.bug": {"message": "缺陷"},
|
||||||
|
"app.skin-site.bugs.type.feature": {"message": "功能建议"},
|
||||||
|
"app.skin-site.bugs.type.improvement": {"message": "改进"},
|
||||||
|
"app.skin-site.bugs.status.pending": {"message": "待处理"},
|
||||||
|
"app.skin-site.bugs.status.confirmed": {"message": "已确认"},
|
||||||
|
"app.skin-site.bugs.status.needs-info": {"message": "待补充信息"},
|
||||||
|
"app.skin-site.bugs.status.in-progress": {"message": "处理中"},
|
||||||
|
"app.skin-site.bugs.status.verifying": {"message": "验证中"},
|
||||||
|
"app.skin-site.bugs.status.closed": {"message": "已关闭"},
|
||||||
|
"app.skin-site.bugs.priority.critical": {"message": "紧急"},
|
||||||
|
"app.skin-site.bugs.priority.high": {"message": "高"},
|
||||||
|
"app.skin-site.bugs.priority.normal": {"message": "普通"},
|
||||||
|
"app.skin-site.bugs.priority.low": {"message": "低"},
|
||||||
|
"app.skin-site.bugs.resolution.fixed": {"message": "已修复"},
|
||||||
|
"app.skin-site.bugs.resolution.wont-fix": {"message": "不予修复"},
|
||||||
|
"app.skin-site.bugs.resolution.by-design": {"message": "设计如此"},
|
||||||
|
"app.skin-site.bugs.resolution.duplicate": {"message": "重复问题"},
|
||||||
|
"app.skin-site.bugs.resolution.cannot-reproduce": {"message": "无法复现"},
|
||||||
|
"app.skin-site.bugs.resolution.invalid": {"message": "无效"},
|
||||||
|
"app.skin-site.bug-detail.back": {"message": "返回列表"},
|
||||||
|
"app.skin-site.bug-detail.loading": {"message": "加载中…"},
|
||||||
|
"app.skin-site.bug-detail.failed": {"message": "无法加载该问题。"},
|
||||||
|
"app.skin-site.bug-detail.issue-no": {"message": "问题 #{id}"},
|
||||||
|
"app.skin-site.bug-detail.by": {"message": "由 {name} 提交"},
|
||||||
|
"app.skin-site.bug-detail.assignee": {"message": "负责人:{name}"},
|
||||||
|
"app.skin-site.bug-detail.unassigned": {"message": "未分配"},
|
||||||
|
"app.skin-site.bug-detail.description-empty": {"message": "未提供描述。"},
|
||||||
|
"app.skin-site.bug-detail.resolution": {"message": "处理结果:{name}"},
|
||||||
|
"app.skin-site.bug-detail.comments-title": {"message": "评论"},
|
||||||
|
"app.skin-site.bug-detail.comments-empty": {"message": "还没有评论,来抢沙发吧。"},
|
||||||
|
"app.skin-site.bug-detail.comments-loading": {"message": "加载评论中…"},
|
||||||
|
"app.skin-site.bug-detail.comment-placeholder": {"message": "写下你的评论…"},
|
||||||
|
"app.skin-site.bug-detail.comment-submit": {"message": "发表评论"},
|
||||||
|
"app.skin-site.bug-detail.comment-sending": {"message": "发送中…"},
|
||||||
|
"app.skin-site.bug-detail.comment-empty-error": {"message": "评论内容不能为空。"},
|
||||||
|
"app.skin-site.bug-detail.comment-posted": {"message": "评论已发表。"},
|
||||||
|
"app.skin-site.bug-detail.comment-deleted": {"message": "评论已删除。"},
|
||||||
|
"app.skin-site.bug-detail.delete": {"message": "删除"},
|
||||||
|
"app.skin-site.bug-detail.deleting": {"message": "删除中…"},
|
||||||
|
"app.skin-site.bug-detail.system": {"message": "系统"}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user