1 parent
d6d4fcf8a0
commit
9bdc965cd5
3 files changed
+421
No files matched your search
@@ -0,0 +1,279 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { SpinnerIcon } from '@modrinth/assets'
|
||||||
|
import { defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
import {
|
||||||
|
type SkinSiteBugIssue,
|
||||||
|
skinSiteBugIssues,
|
||||||
|
type SkinSiteBugIssueType,
|
||||||
|
type SkinSiteBugPriority,
|
||||||
|
type SkinSiteBugStatus,
|
||||||
|
} from '@/helpers/skin-site-api'
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: { id: 'app.skin-site.bugs.title', defaultMessage: 'Bugs & suggestions' },
|
||||||
|
back: { id: 'app.skin-site.bugs.back', defaultMessage: 'Back to home' },
|
||||||
|
subtitle: {
|
||||||
|
id: 'app.skin-site.bugs.subtitle',
|
||||||
|
defaultMessage: 'Report issues and follow their progress.',
|
||||||
|
},
|
||||||
|
loading: { id: 'app.skin-site.bugs.loading', defaultMessage: 'Loading…' },
|
||||||
|
failed: { id: 'app.skin-site.bugs.failed', defaultMessage: 'Could not load issues.' },
|
||||||
|
empty: { id: 'app.skin-site.bugs.empty', defaultMessage: 'No issues match these filters.' },
|
||||||
|
all: { id: 'app.skin-site.bugs.all', defaultMessage: 'All' },
|
||||||
|
more: { id: 'app.skin-site.bugs.more', defaultMessage: 'Load more' },
|
||||||
|
openOnly: { id: 'app.skin-site.bugs.open-only', defaultMessage: 'Open only' },
|
||||||
|
comments: { id: 'app.skin-site.bugs.comments', defaultMessage: '{count} comments' },
|
||||||
|
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' },
|
||||||
|
})
|
||||||
|
const emit = defineEmits<{ (e: 'back'): void }>()
|
||||||
|
|
||||||
|
const items = ref<SkinSiteBugIssue[]>([])
|
||||||
|
const hasMore = ref(false)
|
||||||
|
const loading = ref(true)
|
||||||
|
const loadingMore = ref(false)
|
||||||
|
const failed = ref(false)
|
||||||
|
|
||||||
|
const typeFilter = ref<SkinSiteBugIssueType | ''>('')
|
||||||
|
const statusFilter = ref<SkinSiteBugStatus | ''>('')
|
||||||
|
const priorityFilter = ref<SkinSiteBugPriority | ''>('')
|
||||||
|
const openOnly = ref(true)
|
||||||
|
|
||||||
|
const TYPE_OPTIONS: SkinSiteBugIssueType[] = ['BUG', 'FEATURE', 'IMPROVEMENT']
|
||||||
|
const STATUS_OPTIONS: SkinSiteBugStatus[] = [
|
||||||
|
'PENDING',
|
||||||
|
'CONFIRMED',
|
||||||
|
'NEEDS_INFO',
|
||||||
|
'IN_PROGRESS',
|
||||||
|
'VERIFYING',
|
||||||
|
'CLOSED',
|
||||||
|
]
|
||||||
|
const PRIORITY_OPTIONS: SkinSiteBugPriority[] = ['CRITICAL', 'HIGH', 'NORMAL', 'LOW']
|
||||||
|
|
||||||
|
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 formatDate(ts: number): string {
|
||||||
|
try {
|
||||||
|
return new Date(ts).toLocaleDateString()
|
||||||
|
} catch {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load(reset = true) {
|
||||||
|
if (reset) {
|
||||||
|
loading.value = true
|
||||||
|
items.value = []
|
||||||
|
} else {
|
||||||
|
loadingMore.value = true
|
||||||
|
}
|
||||||
|
failed.value = false
|
||||||
|
try {
|
||||||
|
const result = await skinSiteBugIssues({
|
||||||
|
offset: reset ? 0 : items.value.length,
|
||||||
|
limit: 20,
|
||||||
|
type: typeFilter.value || undefined,
|
||||||
|
status: statusFilter.value || undefined,
|
||||||
|
priority: priorityFilter.value || undefined,
|
||||||
|
openOnly: openOnly.value,
|
||||||
|
})
|
||||||
|
if (!result.success) throw new Error(result.errorMessage)
|
||||||
|
const page = result.payload
|
||||||
|
items.value = reset ? page.items : [...items.value, ...page.items]
|
||||||
|
hasMore.value = page.hasMore
|
||||||
|
} catch (error) {
|
||||||
|
failed.value = true
|
||||||
|
handleError(error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
loadingMore.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFilters() {
|
||||||
|
typeFilter.value = ''
|
||||||
|
statusFilter.value = ''
|
||||||
|
priorityFilter.value = ''
|
||||||
|
void load(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => load(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>
|
||||||
|
|
||||||
|
<header class="flex flex-col gap-1">
|
||||||
|
<h1 class="m-0 text-xl font-bold text-contrast">{{ formatMessage(messages.title) }}</h1>
|
||||||
|
<p class="m-0 text-sm text-secondary">{{ formatMessage(messages.subtitle) }}</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<select
|
||||||
|
v-model="typeFilter"
|
||||||
|
class="rounded-full bg-button-bg px-3 py-1 text-xs font-semibold text-contrast"
|
||||||
|
@change="load(true)"
|
||||||
|
>
|
||||||
|
<option value="">{{ formatMessage(messages.all) }}</option>
|
||||||
|
<option v-for="t in TYPE_OPTIONS" :key="t" :value="t">{{ typeLabel(t) }}</option>
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
v-model="statusFilter"
|
||||||
|
class="rounded-full bg-button-bg px-3 py-1 text-xs font-semibold text-contrast"
|
||||||
|
@change="load(true)"
|
||||||
|
>
|
||||||
|
<option value="">{{ formatMessage(messages.all) }}</option>
|
||||||
|
<option v-for="s in STATUS_OPTIONS" :key="s" :value="s">{{ statusLabel(s) }}</option>
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
v-model="priorityFilter"
|
||||||
|
class="rounded-full bg-button-bg px-3 py-1 text-xs font-semibold text-contrast"
|
||||||
|
@change="load(true)"
|
||||||
|
>
|
||||||
|
<option value="">{{ formatMessage(messages.all) }}</option>
|
||||||
|
<option v-for="p in PRIORITY_OPTIONS" :key="p" :value="p">{{ priorityLabel(p) }}</option>
|
||||||
|
</select>
|
||||||
|
<label class="flex items-center gap-1.5 text-xs text-secondary">
|
||||||
|
<input v-model="openOnly" type="checkbox" @change="load(true)" />
|
||||||
|
{{ formatMessage(messages.openOnly) }}
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ml-auto rounded-full bg-button-bg px-3 py-1 text-xs font-semibold text-secondary hover:brightness-110"
|
||||||
|
@click="clearFilters"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.all) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<p v-else-if="!items.length" class="m-0 text-sm text-secondary">
|
||||||
|
{{ formatMessage(messages.empty) }}
|
||||||
|
</p>
|
||||||
|
<div v-else class="flex flex-col gap-2">
|
||||||
|
<div
|
||||||
|
v-for="issue in items"
|
||||||
|
:key="issue.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">
|
||||||
|
<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="min-w-0 flex-1 truncate text-sm font-semibold text-contrast">
|
||||||
|
#{{ issue.id }} {{ issue.title }}
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap items-center gap-3 text-xs text-secondary">
|
||||||
|
<span>{{ issue.authorName }}</span>
|
||||||
|
<span>{{ formatDate(issue.createdAt) }}</span>
|
||||||
|
<span>{{ formatMessage(messages.comments, { count: issue.commentCount }) }}</span>
|
||||||
|
<span v-if="issue.assigneeName">→ {{ issue.assigneeName }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="hasMore"
|
||||||
|
type="button"
|
||||||
|
class="w-fit self-center rounded-full bg-brand px-4 py-2 text-sm font-semibold text-contrast disabled:opacity-50"
|
||||||
|
:disabled="loadingMore"
|
||||||
|
@click="load(false)"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.more) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -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 SkinSiteBugListView from './SkinSiteBugListView.vue'
|
||||||
import SkinSiteDonationCard from './SkinSiteDonationCard.vue'
|
import SkinSiteDonationCard from './SkinSiteDonationCard.vue'
|
||||||
import SkinSiteDownloadsView from './SkinSiteDownloadsView.vue'
|
import SkinSiteDownloadsView from './SkinSiteDownloadsView.vue'
|
||||||
import SkinSiteLoginModal from './SkinSiteLoginModal.vue'
|
import SkinSiteLoginModal from './SkinSiteLoginModal.vue'
|
||||||
@@ -49,6 +50,7 @@ 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'" />
|
||||||
|
|
||||||
<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">
|
||||||
|
|||||||
@@ -628,3 +628,143 @@ export function skinSiteSubmitReview(
|
|||||||
auth: false,
|
auth: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Bug tracker
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export type SkinSiteBugIssueType = 'BUG' | 'FEATURE' | 'IMPROVEMENT'
|
||||||
|
export type SkinSiteBugStatus =
|
||||||
|
| 'PENDING'
|
||||||
|
| 'CONFIRMED'
|
||||||
|
| 'NEEDS_INFO'
|
||||||
|
| 'IN_PROGRESS'
|
||||||
|
| 'VERIFYING'
|
||||||
|
| 'CLOSED'
|
||||||
|
export type SkinSiteBugPriority = 'CRITICAL' | 'HIGH' | 'NORMAL' | 'LOW'
|
||||||
|
export type SkinSiteBugSearchType = 'CONTENT' | 'SUBMITTER'
|
||||||
|
export type SkinSiteBugResolution =
|
||||||
|
| 'FIXED'
|
||||||
|
| 'WONT_FIX'
|
||||||
|
| 'BY_DESIGN'
|
||||||
|
| 'DUPLICATE'
|
||||||
|
| 'CANNOT_REPRODUCE'
|
||||||
|
| 'INVALID'
|
||||||
|
|
||||||
|
export interface SkinSiteBugImage {
|
||||||
|
hash: string
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SkinSiteBugIssue {
|
||||||
|
id: number
|
||||||
|
type: SkinSiteBugIssueType
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
private: boolean
|
||||||
|
authorUuid: string
|
||||||
|
authorName: string
|
||||||
|
authorAvatar: string | null
|
||||||
|
status: SkinSiteBugStatus
|
||||||
|
priority: SkinSiteBugPriority
|
||||||
|
resolution: SkinSiteBugResolution | null
|
||||||
|
assigneeUuid: string | null
|
||||||
|
assigneeName: string | null
|
||||||
|
commentCount: number
|
||||||
|
images: SkinSiteBugImage[]
|
||||||
|
createdAt: number
|
||||||
|
updatedAt: number
|
||||||
|
closedAt: number | null
|
||||||
|
canEdit: boolean
|
||||||
|
canManage: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SkinSiteBugIssuePage {
|
||||||
|
items: SkinSiteBugIssue[]
|
||||||
|
hasMore: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function skinSiteBugIssues(params: {
|
||||||
|
offset?: number
|
||||||
|
limit?: number
|
||||||
|
type?: SkinSiteBugIssueType | ''
|
||||||
|
status?: SkinSiteBugStatus | ''
|
||||||
|
priority?: SkinSiteBugPriority | ''
|
||||||
|
assigned?: boolean
|
||||||
|
openOnly?: boolean
|
||||||
|
pendingFirst?: boolean
|
||||||
|
searchType?: SkinSiteBugSearchType
|
||||||
|
keyword?: string
|
||||||
|
}): Promise<SkinSiteResponse<SkinSiteBugIssuePage>> {
|
||||||
|
const query = new URLSearchParams({
|
||||||
|
offset: String(params.offset ?? 0),
|
||||||
|
limit: String(params.limit ?? 20),
|
||||||
|
})
|
||||||
|
if (params.type) query.set('type', params.type)
|
||||||
|
if (params.status) query.set('status', params.status)
|
||||||
|
if (params.priority) query.set('priority', params.priority)
|
||||||
|
if (params.assigned) query.set('assigned', 'true')
|
||||||
|
if (params.searchType && params.keyword) {
|
||||||
|
query.set('searchType', params.searchType)
|
||||||
|
query.set('keyword', params.keyword)
|
||||||
|
}
|
||||||
|
query.set('openOnly', String(params.openOnly ?? true))
|
||||||
|
query.set('pendingFirst', String(params.pendingFirst ?? true))
|
||||||
|
return skinSiteRequest<SkinSiteBugIssuePage>(`bug?${query.toString()}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function skinSiteBugIssue(id: number): Promise<SkinSiteResponse<SkinSiteBugIssue>> {
|
||||||
|
return skinSiteRequest<SkinSiteBugIssue>(`bug/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SkinSiteBugComment {
|
||||||
|
id: number
|
||||||
|
issueId: number
|
||||||
|
authorUuid: string
|
||||||
|
authorName: string
|
||||||
|
authorAvatar: string | null
|
||||||
|
content: string
|
||||||
|
system: boolean
|
||||||
|
images: SkinSiteBugImage[]
|
||||||
|
createdAt: number
|
||||||
|
editedAt: number | null
|
||||||
|
deletedAt: number | null
|
||||||
|
canEdit: boolean
|
||||||
|
canDelete: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SkinSiteBugCommentPage {
|
||||||
|
items: SkinSiteBugComment[]
|
||||||
|
hasMore: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function skinSiteBugComments(
|
||||||
|
issueId: number,
|
||||||
|
offset = 0,
|
||||||
|
limit = 50,
|
||||||
|
): Promise<SkinSiteResponse<SkinSiteBugCommentPage>> {
|
||||||
|
return skinSiteRequest<SkinSiteBugCommentPage>(
|
||||||
|
`bug/${issueId}/comments?offset=${offset}&limit=${limit}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function skinSiteCreateBugComment(
|
||||||
|
issueId: number,
|
||||||
|
content: string,
|
||||||
|
): Promise<SkinSiteResponse<SkinSiteBugComment>> {
|
||||||
|
return skinSiteRequest<SkinSiteBugComment>(`bug/${issueId}/comments`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: { content },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function skinSiteDeleteBugComment(id: number): Promise<SkinSiteResponse<string>> {
|
||||||
|
return skinSiteRequest<string>(`bug/comments/${id}`, { method: 'DELETE' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function skinSiteBugUnreadCount(): Promise<SkinSiteResponse<number>> {
|
||||||
|
return skinSiteRequest<number>('bug/notifications/count')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function skinSiteMarkBugNotificationsRead(): Promise<SkinSiteResponse<string>> {
|
||||||
|
return skinSiteRequest<string>('bug/notifications/read', { method: 'POST' })
|
||||||
|
}
|
||||||
Reference in new issue
Block a user