You've already forked AstralRinth
feat: hosting access tab (#5995)
* feat: implement access tab with dummy data * fix: spacing * feat: qa * feat: implement backend * qa: qa pass * feat: fix user "search" * fix: lint * feat: change to bitfield * feat: fix fields * fix: lint * fix: lint * feat: hook up api * feat: fix permissions * feat: audit log table event start * feat: better mobile mode for audit log table * feat: i18n * feat: qa * feat: enforce permissions * feat: email template start * feat: qa * fix: tooltip bug * feat: qa * impl: sse support in api-client * feat: sse impl * fix: desync path * feat: time frame picker from analytics * feat: QA * fix: spacing * fix: permisison audit log entries * fix: hosting manage page shared server detection * fix: lint * feat: qa + lint * feat: audit log table sort by time * feat: finish frontend panel stuff * fix: lint * fix: backend alignment * fix: lint * fix: supress friend errors * feat: qa * fix: qa * fix: lint * fix: utils barrel * fix: safari cookies in dev * fix: pin nuxt * feat: fixes + notif fix * fix: notifications * feat: qa * fix: notification sync not happening immediately * fix: qa * fix: qa * feat: qa * blog + prepr * feat: toast shit * blog images * thumbnail update one last time * prepr * feat: use reinvite route * update images * fix: reinvite stuff * fix: lint * fix: alignment of save bar * fix: notif sizing * fix: split up access * fix: lint * fix: lint * fix: link --------- Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
@@ -1,244 +1,284 @@
|
||||
<template>
|
||||
<div
|
||||
class="notification"
|
||||
:class="{
|
||||
'has-body': hasBody,
|
||||
compact: compact,
|
||||
read: notification.read,
|
||||
}"
|
||||
:class="
|
||||
type === 'server_invite'
|
||||
? { read: notification.read }
|
||||
: {
|
||||
notification: true,
|
||||
'has-body': hasBody,
|
||||
compact: compact,
|
||||
read: notification.read,
|
||||
}
|
||||
"
|
||||
>
|
||||
<nuxt-link
|
||||
v-if="!type"
|
||||
:to="notification.link"
|
||||
class="notification__icon backed-svg"
|
||||
:class="{ raised: raised }"
|
||||
>
|
||||
<BellIcon />
|
||||
</nuxt-link>
|
||||
<DoubleIcon v-else class="notification__icon">
|
||||
<template #primary>
|
||||
<nuxt-link v-if="project" :to="getProjectLink(project)" tabindex="-1">
|
||||
<Avatar size="xs" :src="project.icon_url" :raised="raised" no-shadow />
|
||||
</nuxt-link>
|
||||
<nuxt-link
|
||||
v-else-if="organization"
|
||||
:to="`/organization/${organization.slug}`"
|
||||
tabindex="-1"
|
||||
>
|
||||
<Avatar size="xs" :src="organization.icon_url" :raised="raised" no-shadow />
|
||||
</nuxt-link>
|
||||
<nuxt-link v-else-if="user" :to="getUserLink(user)" tabindex="-1">
|
||||
<Avatar size="xs" :src="user.avatar_url" :raised="raised" no-shadow />
|
||||
</nuxt-link>
|
||||
<Avatar v-else size="xs" :raised="raised" no-shadow />
|
||||
</template>
|
||||
<template #secondary>
|
||||
<ScaleIcon
|
||||
v-if="type === 'moderator_message' || type === 'status_change'"
|
||||
class="moderation-color"
|
||||
/>
|
||||
<UserPlusIcon v-else-if="type === 'team_invite' && project" class="creator-color" />
|
||||
<UserPlusIcon
|
||||
v-else-if="type === 'organization_invite' && organization"
|
||||
class="creator-color"
|
||||
/>
|
||||
<VersionIcon v-else-if="type === 'project_update' && project && version" />
|
||||
<BellIcon v-else />
|
||||
</template>
|
||||
</DoubleIcon>
|
||||
<div class="notification__title">
|
||||
<template v-if="type === 'project_update' && project && version">
|
||||
A project you follow,
|
||||
<nuxt-link :to="getProjectLink(project)" class="title-link">{{ project.title }}</nuxt-link>
|
||||
, has been updated:
|
||||
</template>
|
||||
<template v-else-if="type === 'team_invite' && project">
|
||||
<nuxt-link
|
||||
:to="`/user/${invitedBy.username}`"
|
||||
class="iconified-link title-link inline-flex"
|
||||
>
|
||||
<Avatar
|
||||
:src="invitedBy.avatar_url"
|
||||
circle
|
||||
size="xxs"
|
||||
no-shadow
|
||||
:raised="raised"
|
||||
class="inline-flex"
|
||||
/>
|
||||
<span class="space"> </span>
|
||||
<span>{{ invitedBy.username }}</span>
|
||||
</nuxt-link>
|
||||
<span>
|
||||
has invited you to join
|
||||
<nuxt-link :to="getProjectLink(project)" class="title-link">
|
||||
{{ project.title }} </nuxt-link
|
||||
>.
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="type === 'organization_invite' && organization">
|
||||
<nuxt-link
|
||||
:to="`/user/${invitedBy.username}`"
|
||||
class="iconified-link title-link inline-flex"
|
||||
>
|
||||
<Avatar
|
||||
:src="invitedBy.avatar_url"
|
||||
circle
|
||||
size="xxs"
|
||||
no-shadow
|
||||
:raised="raised"
|
||||
class="inline-flex"
|
||||
/>
|
||||
<span class="space"> </span>
|
||||
<span>{{ invitedBy.username }}</span>
|
||||
</nuxt-link>
|
||||
<span>
|
||||
has invited you to join
|
||||
<nuxt-link :to="`/organization/${organization.slug}`" class="title-link">
|
||||
{{ organization.name }} </nuxt-link
|
||||
>.
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="type === 'status_change' && project">
|
||||
<nuxt-link :to="getProjectLink(project)" class="title-link">
|
||||
{{ project.title }}
|
||||
</nuxt-link>
|
||||
<template v-if="tags.rejectedStatuses.includes(notification.body.new_status)">
|
||||
has been
|
||||
<ProjectStatusBadge :status="notification.body.new_status" />
|
||||
</template>
|
||||
<template v-else>
|
||||
updated from
|
||||
<ProjectStatusBadge :status="notification.body.old_status" />
|
||||
to
|
||||
<ProjectStatusBadge :status="notification.body.new_status" />
|
||||
</template>
|
||||
by the moderators.
|
||||
</template>
|
||||
<template v-else-if="type === 'moderator_message' && thread && project && !report">
|
||||
Your project,
|
||||
<nuxt-link :to="getProjectLink(project)" class="title-link">{{ project.title }}</nuxt-link>
|
||||
, has received
|
||||
<template v-if="notification.grouped_notifs"> messages</template>
|
||||
<template v-else>a message</template>
|
||||
from the moderators.
|
||||
</template>
|
||||
<template v-else-if="type === 'moderator_message' && thread && report">
|
||||
A moderator replied to your report of
|
||||
<template v-if="version">
|
||||
version
|
||||
<nuxt-link :to="getVersionLink(project, version)" class="title-link">
|
||||
{{ version.name }}
|
||||
</nuxt-link>
|
||||
of project
|
||||
</template>
|
||||
<nuxt-link v-if="project" :to="getProjectLink(project)" class="title-link">
|
||||
{{ project.title }}
|
||||
</nuxt-link>
|
||||
<nuxt-link v-else-if="user" :to="getUserLink(user)" class="title-link">
|
||||
{{ user.username }}
|
||||
</nuxt-link>
|
||||
.
|
||||
</template>
|
||||
<nuxt-link v-else :to="notification.link" class="title-link">
|
||||
<span v-html="renderString(notification.title)" />
|
||||
</nuxt-link>
|
||||
<!-- <span v-else class="known-errors">Error reading notification.</span>-->
|
||||
</div>
|
||||
<div v-if="hasBody" class="notification__body">
|
||||
<ThreadSummary
|
||||
v-if="type === 'moderator_message' && thread"
|
||||
:thread="thread"
|
||||
:link="threadLink"
|
||||
:raised="raised"
|
||||
:messages="getMessages()"
|
||||
class="thread-summary"
|
||||
:auth="auth"
|
||||
/>
|
||||
<div v-else-if="type === 'project_update'" class="version-list">
|
||||
<template v-if="type === 'server_invite'">
|
||||
<div class="flex flex-col gap-4">
|
||||
<ModrinthServersIcon class="h-auto w-56 max-w-full text-[var(--color-heading)]" />
|
||||
<div
|
||||
v-for="notif in (notification.grouped_notifs
|
||||
? [notification, ...notification.grouped_notifs]
|
||||
: [notification]
|
||||
).filter((x) => x.extra_data.version)"
|
||||
:key="notif.id"
|
||||
class="version-link"
|
||||
class="flex flex-wrap items-center gap-x-1.5 gap-y-2 text-lg leading-tight text-[var(--color-heading)]"
|
||||
>
|
||||
<VersionIcon />
|
||||
<nuxt-link
|
||||
:to="getVersionLink(notif.extra_data.project, notif.extra_data.version)"
|
||||
class="text-link"
|
||||
v-if="invitedBy"
|
||||
:to="`/user/${invitedBy.username}`"
|
||||
class="inline-flex items-center font-bold text-[var(--color-heading)] hover:underline"
|
||||
>
|
||||
{{ notif.extra_data.version.name }}
|
||||
</nuxt-link>
|
||||
<span class="version-info">
|
||||
for
|
||||
<Categories
|
||||
:categories="getLoaderCategories(notif.extra_data.version)"
|
||||
:type="notif.extra_data.project.project_type"
|
||||
class="categories"
|
||||
<Avatar
|
||||
:src="invitedBy.avatar_url"
|
||||
circle
|
||||
size="xxs"
|
||||
no-shadow
|
||||
:raised="raised"
|
||||
class="mr-1.5 inline-flex"
|
||||
/>
|
||||
{{ $formatVersion(notif.extra_data.version.game_versions) }}
|
||||
<span v-tooltip="formatDateTime(notif.extra_data.version.date_published)" class="date">
|
||||
{{ formatRelativeTime(notif.extra_data.version.date_published) }}
|
||||
</span>
|
||||
</span>
|
||||
<span>{{ invitedBy.username }}</span>
|
||||
</nuxt-link>
|
||||
<span v-if="invitedBy">has invited you to manage</span>
|
||||
<span v-else>You have been invited to manage</span>
|
||||
<span
|
||||
><strong class="font-bold text-[var(--color-heading)]">{{
|
||||
notification.body.server_name
|
||||
}}</strong
|
||||
>.</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
{{ notification.text }}
|
||||
</template>
|
||||
</div>
|
||||
<span class="notification__date">
|
||||
<span v-if="notification.read" class="read-badge inline-flex">
|
||||
<CheckCircleIcon /> Read
|
||||
</span>
|
||||
<span v-tooltip="formatDateTime(notification.created)" class="inline-flex">
|
||||
<CalendarIcon class="mr-1" /> Received
|
||||
{{ formatRelativeTime(notification.created) }}
|
||||
</span>
|
||||
</span>
|
||||
<div v-if="compact" class="notification__actions">
|
||||
<template v-if="type === 'team_invite' || type === 'organization_invite'">
|
||||
<ButtonStyled circular color="brand" type="transparent">
|
||||
<button
|
||||
v-tooltip="`Accept`"
|
||||
@click="
|
||||
() => {
|
||||
acceptTeamInvite(notification.body.team_id)
|
||||
read()
|
||||
}
|
||||
"
|
||||
>
|
||||
<CheckIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular color="red" type="transparent">
|
||||
<button
|
||||
v-tooltip="`Decline`"
|
||||
@click="
|
||||
() => {
|
||||
removeSelfFromTeam(notification.body.team_id)
|
||||
read()
|
||||
}
|
||||
"
|
||||
>
|
||||
<XIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
<ButtonStyled v-else-if="!notification.read" circular type="transparent">
|
||||
<button v-tooltip="`Mark as read`" @click="read()">
|
||||
<XIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div v-else class="notification__actions">
|
||||
<div v-if="type !== null" class="input-group">
|
||||
<template
|
||||
v-if="(type === 'team_invite' || type === 'organization_invite') && !notification.read"
|
||||
<div
|
||||
v-if="!notification.read"
|
||||
class="flex flex-wrap items-center gap-3"
|
||||
:class="{ 'gap-2': compact }"
|
||||
>
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="performActionByTitle(notification, 'Accept')">
|
||||
<CheckIcon />
|
||||
Accept
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="red">
|
||||
<button @click="performActionByTitle(notification, 'Deny')">
|
||||
<XIcon />
|
||||
Decline
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div
|
||||
class="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-[var(--color-text-secondary)]"
|
||||
>
|
||||
<span
|
||||
v-if="notification.read"
|
||||
class="inline-flex items-center font-bold text-[var(--color-text)]"
|
||||
>
|
||||
<CheckCircleIcon /> Read
|
||||
</span>
|
||||
<span v-tooltip="formatDateTime(notification.created)" class="inline-flex items-center">
|
||||
<CalendarIcon class="mr-1" /> Received
|
||||
{{ formatRelativeTime(notification.created) }}
|
||||
</span>
|
||||
<CopyCode v-if="flags.developerMode" :text="notification.id" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<nuxt-link
|
||||
v-if="!type"
|
||||
:to="notification.link"
|
||||
class="notification__icon backed-svg"
|
||||
:class="{ raised: raised }"
|
||||
>
|
||||
<BellIcon />
|
||||
</nuxt-link>
|
||||
<DoubleIcon v-else class="notification__icon">
|
||||
<template #primary>
|
||||
<nuxt-link v-if="project" :to="getProjectLink(project)" tabindex="-1">
|
||||
<Avatar size="xs" :src="project.icon_url" :raised="raised" no-shadow />
|
||||
</nuxt-link>
|
||||
<nuxt-link
|
||||
v-else-if="organization"
|
||||
:to="`/organization/${organization.slug}`"
|
||||
tabindex="-1"
|
||||
>
|
||||
<Avatar size="xs" :src="organization.icon_url" :raised="raised" no-shadow />
|
||||
</nuxt-link>
|
||||
<nuxt-link v-else-if="user" :to="getUserLink(user)" tabindex="-1">
|
||||
<Avatar size="xs" :src="user.avatar_url" :raised="raised" no-shadow />
|
||||
</nuxt-link>
|
||||
<Avatar v-else size="xs" :raised="raised" no-shadow />
|
||||
</template>
|
||||
<template #secondary>
|
||||
<ScaleIcon
|
||||
v-if="type === 'moderator_message' || type === 'status_change'"
|
||||
class="moderation-color"
|
||||
/>
|
||||
<UserPlusIcon v-else-if="type === 'team_invite' && project" class="creator-color" />
|
||||
<UserPlusIcon
|
||||
v-else-if="type === 'organization_invite' && organization"
|
||||
class="creator-color"
|
||||
/>
|
||||
<VersionIcon v-else-if="type === 'project_update' && project && version" />
|
||||
<BellIcon v-else />
|
||||
</template>
|
||||
</DoubleIcon>
|
||||
<div class="notification__title">
|
||||
<template v-if="type === 'project_update' && project && version">
|
||||
A project you follow,
|
||||
<nuxt-link :to="getProjectLink(project)" class="title-link">{{
|
||||
project.title
|
||||
}}</nuxt-link>
|
||||
, has been updated:
|
||||
</template>
|
||||
<template v-else-if="type === 'team_invite' && project">
|
||||
<nuxt-link
|
||||
:to="`/user/${invitedBy.username}`"
|
||||
class="iconified-link title-link inline-flex"
|
||||
>
|
||||
<Avatar
|
||||
:src="invitedBy.avatar_url"
|
||||
circle
|
||||
size="xxs"
|
||||
no-shadow
|
||||
:raised="raised"
|
||||
class="inline-flex"
|
||||
/>
|
||||
<span class="space"> </span>
|
||||
<span>{{ invitedBy.username }}</span>
|
||||
</nuxt-link>
|
||||
<span>
|
||||
has invited you to join
|
||||
<nuxt-link :to="getProjectLink(project)" class="title-link">
|
||||
{{ project.title }} </nuxt-link
|
||||
>.
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="type === 'organization_invite' && organization">
|
||||
<nuxt-link
|
||||
:to="`/user/${invitedBy.username}`"
|
||||
class="iconified-link title-link inline-flex"
|
||||
>
|
||||
<Avatar
|
||||
:src="invitedBy.avatar_url"
|
||||
circle
|
||||
size="xxs"
|
||||
no-shadow
|
||||
:raised="raised"
|
||||
class="inline-flex"
|
||||
/>
|
||||
<span class="space"> </span>
|
||||
<span>{{ invitedBy.username }}</span>
|
||||
</nuxt-link>
|
||||
<span>
|
||||
has invited you to join
|
||||
<nuxt-link :to="`/organization/${organization.slug}`" class="title-link">
|
||||
{{ organization.name }} </nuxt-link
|
||||
>.
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="type === 'status_change' && project">
|
||||
<nuxt-link :to="getProjectLink(project)" class="title-link">
|
||||
{{ project.title }}
|
||||
</nuxt-link>
|
||||
<template v-if="tags.rejectedStatuses.includes(notification.body.new_status)">
|
||||
has been
|
||||
<ProjectStatusBadge :status="notification.body.new_status" />
|
||||
</template>
|
||||
<template v-else>
|
||||
updated from
|
||||
<ProjectStatusBadge :status="notification.body.old_status" />
|
||||
to
|
||||
<ProjectStatusBadge :status="notification.body.new_status" />
|
||||
</template>
|
||||
by the moderators.
|
||||
</template>
|
||||
<template v-else-if="type === 'moderator_message' && thread && project && !report">
|
||||
Your project,
|
||||
<nuxt-link :to="getProjectLink(project)" class="title-link">{{
|
||||
project.title
|
||||
}}</nuxt-link>
|
||||
, has received
|
||||
<template v-if="notification.grouped_notifs"> messages</template>
|
||||
<template v-else>a message</template>
|
||||
from the moderators.
|
||||
</template>
|
||||
<template v-else-if="type === 'moderator_message' && thread && report">
|
||||
A moderator replied to your report of
|
||||
<template v-if="version">
|
||||
version
|
||||
<nuxt-link :to="getVersionLink(project, version)" class="title-link">
|
||||
{{ version.name }}
|
||||
</nuxt-link>
|
||||
of project
|
||||
</template>
|
||||
<nuxt-link v-if="project" :to="getProjectLink(project)" class="title-link">
|
||||
{{ project.title }}
|
||||
</nuxt-link>
|
||||
<nuxt-link v-else-if="user" :to="getUserLink(user)" class="title-link">
|
||||
{{ user.username }}
|
||||
</nuxt-link>
|
||||
.
|
||||
</template>
|
||||
<nuxt-link v-else :to="notification.link" class="title-link">
|
||||
<span v-html="renderString(notification.title)" />
|
||||
</nuxt-link>
|
||||
<!-- <span v-else class="known-errors">Error reading notification.</span>-->
|
||||
</div>
|
||||
<div v-if="hasBody" class="notification__body">
|
||||
<ThreadSummary
|
||||
v-if="type === 'moderator_message' && thread"
|
||||
:thread="thread"
|
||||
:link="threadLink"
|
||||
:raised="raised"
|
||||
:messages="getMessages()"
|
||||
class="thread-summary"
|
||||
:auth="auth"
|
||||
/>
|
||||
<div v-else-if="type === 'project_update'" class="version-list">
|
||||
<div
|
||||
v-for="notif in (notification.grouped_notifs
|
||||
? [notification, ...notification.grouped_notifs]
|
||||
: [notification]
|
||||
).filter((x) => x.extra_data.version)"
|
||||
:key="notif.id"
|
||||
class="version-link"
|
||||
>
|
||||
<VersionIcon />
|
||||
<nuxt-link
|
||||
:to="getVersionLink(notif.extra_data.project, notif.extra_data.version)"
|
||||
class="text-link"
|
||||
>
|
||||
{{ notif.extra_data.version.name }}
|
||||
</nuxt-link>
|
||||
<span class="version-info">
|
||||
for
|
||||
<Categories
|
||||
:categories="getLoaderCategories(notif.extra_data.version)"
|
||||
:type="notif.extra_data.project.project_type"
|
||||
class="categories"
|
||||
/>
|
||||
{{ $formatVersion(notif.extra_data.version.game_versions) }}
|
||||
<span
|
||||
v-tooltip="formatDateTime(notif.extra_data.version.date_published)"
|
||||
class="date"
|
||||
>
|
||||
{{ formatRelativeTime(notif.extra_data.version.date_published) }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
{{ notification.text }}
|
||||
</template>
|
||||
</div>
|
||||
<span class="notification__date">
|
||||
<span v-if="notification.read" class="read-badge inline-flex">
|
||||
<CheckCircleIcon /> Read
|
||||
</span>
|
||||
<span v-tooltip="formatDateTime(notification.created)" class="inline-flex">
|
||||
<CalendarIcon class="mr-1" /> Received
|
||||
{{ formatRelativeTime(notification.created) }}
|
||||
</span>
|
||||
</span>
|
||||
<div v-if="compact" class="notification__actions">
|
||||
<template v-if="type === 'team_invite' || type === 'organization_invite'">
|
||||
<ButtonStyled circular color="brand" type="transparent">
|
||||
<button
|
||||
v-tooltip="`Accept`"
|
||||
@click="
|
||||
() => {
|
||||
acceptTeamInvite(notification.body.team_id)
|
||||
@@ -247,11 +287,11 @@
|
||||
"
|
||||
>
|
||||
<CheckIcon />
|
||||
Accept
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="red">
|
||||
<ButtonStyled circular color="red" type="transparent">
|
||||
<button
|
||||
v-tooltip="`Decline`"
|
||||
@click="
|
||||
() => {
|
||||
removeSelfFromTeam(notification.body.team_id)
|
||||
@@ -260,41 +300,79 @@
|
||||
"
|
||||
>
|
||||
<XIcon />
|
||||
Decline
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
<ButtonStyled v-else-if="!notification.read">
|
||||
<button @click="read()">
|
||||
<CheckIcon />
|
||||
Mark as read
|
||||
<ButtonStyled v-else-if="!notification.read" circular type="transparent">
|
||||
<button v-tooltip="`Mark as read`" @click="read()">
|
||||
<XIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<CopyCode v-if="flags.developerMode" :text="notification.id" />
|
||||
</div>
|
||||
<div v-else class="input-group">
|
||||
<ButtonStyled v-if="notification.link && notification.link !== '#'">
|
||||
<nuxt-link :to="notification.link" target="_blank">
|
||||
<ExternalIcon />
|
||||
Open link
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-for="(action, actionIndex) in notification.actions" :key="actionIndex">
|
||||
<button @click="performAction(notification, actionIndex)">
|
||||
<CheckIcon v-if="action.title === 'Accept'" />
|
||||
<XIcon v-else-if="action.title === 'Deny'" />
|
||||
{{ action.title }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="notification.actions.length === 0 && !notification.read">
|
||||
<button @click="performAction(notification, null)">
|
||||
<CheckIcon />
|
||||
Mark as read
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<CopyCode v-if="flags.developerMode" :text="notification.id" />
|
||||
<div v-else class="notification__actions">
|
||||
<div v-if="type !== null" class="input-group">
|
||||
<template
|
||||
v-if="(type === 'team_invite' || type === 'organization_invite') && !notification.read"
|
||||
>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
@click="
|
||||
() => {
|
||||
acceptTeamInvite(notification.body.team_id)
|
||||
read()
|
||||
}
|
||||
"
|
||||
>
|
||||
<CheckIcon />
|
||||
Accept
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="red">
|
||||
<button
|
||||
@click="
|
||||
() => {
|
||||
removeSelfFromTeam(notification.body.team_id)
|
||||
read()
|
||||
}
|
||||
"
|
||||
>
|
||||
<XIcon />
|
||||
Decline
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
<ButtonStyled v-else-if="!notification.read">
|
||||
<button @click="read()">
|
||||
<CheckIcon />
|
||||
Mark as read
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<CopyCode v-if="flags.developerMode" :text="notification.id" />
|
||||
</div>
|
||||
<div v-else class="input-group">
|
||||
<ButtonStyled v-if="notification.link && notification.link !== '#'">
|
||||
<nuxt-link :to="notification.link" target="_blank">
|
||||
<ExternalIcon />
|
||||
Open link
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-for="(action, actionIndex) in notification.actions" :key="actionIndex">
|
||||
<button @click="performAction(notification, actionIndex)">
|
||||
<CheckIcon v-if="action.title === 'Accept'" />
|
||||
<XIcon v-else-if="action.title === 'Deny'" />
|
||||
{{ action.title }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="notification.actions.length === 0 && !notification.read">
|
||||
<button @click="performAction(notification, null)">
|
||||
<CheckIcon />
|
||||
Mark as read
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<CopyCode v-if="flags.developerMode" :text="notification.id" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -328,11 +406,13 @@ import { markAsRead } from '~/helpers/platform-notifications'
|
||||
import { getProjectLink, getVersionLink } from '~/helpers/projects'
|
||||
import { acceptTeamInvite, removeSelfFromTeam } from '~/helpers/teams'
|
||||
|
||||
import ModrinthServersIcon from '../brand/ModrinthServersIcon.vue'
|
||||
import ThreadSummary from './thread/ThreadSummary.vue'
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const emit = defineEmits(['update:notifications'])
|
||||
const router = useRouter()
|
||||
const formatRelativeTime = useRelativeTime()
|
||||
const formatDateTime = useFormatDateTime({
|
||||
timeStyle: 'short',
|
||||
@@ -415,9 +495,29 @@ async function performAction(notification, actionIndex) {
|
||||
await read()
|
||||
|
||||
if (actionIndex !== null) {
|
||||
await useBaseFetch(`${notification.actions[actionIndex].action_route[1]}`, {
|
||||
method: notification.actions[actionIndex].action_route[0].toUpperCase(),
|
||||
})
|
||||
const action = notification.actions[actionIndex]
|
||||
|
||||
if (type.value === 'server_invite') {
|
||||
const actionName = action.title.toLowerCase()
|
||||
const inviteAction = actionName === 'accept' ? 'accept' : 'decline'
|
||||
const serverId = notification.body.server_id
|
||||
|
||||
await client.request(`/servers/${serverId}/invites/${inviteAction}`, {
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
if (inviteAction === 'accept') {
|
||||
await router.push(`/hosting/manage/${encodeURIComponent(serverId)}`)
|
||||
}
|
||||
} else {
|
||||
const [method, route] = action.action_route
|
||||
|
||||
await useBaseFetch(route, {
|
||||
method: method.toUpperCase(),
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
@@ -429,6 +529,20 @@ async function performAction(notification, actionIndex) {
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
function performActionByTitle(notification, title) {
|
||||
const actionIndex = notification.actions.findIndex((action) => action.title === title)
|
||||
if (actionIndex === -1) {
|
||||
addNotification({
|
||||
title: 'An error occurred',
|
||||
text: `Missing ${title.toLowerCase()} action for notification.`,
|
||||
type: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
return performAction(notification, actionIndex)
|
||||
}
|
||||
|
||||
function getMessages() {
|
||||
const messages = []
|
||||
if (props.notification.body.message_id) {
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import type { Archon } from '@modrinth/api-client'
|
||||
import { PlusIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Accordion,
|
||||
ButtonStyled,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
NewModal,
|
||||
ServerNotice,
|
||||
StyledInput,
|
||||
TagItem,
|
||||
} from '@modrinth/ui'
|
||||
import type { ServerNotice as ServerNoticeType } from '@modrinth/utils'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useServersFetch } from '~/composables/servers/servers-fetch.ts'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const client = injectModrinthClient()
|
||||
|
||||
type ServerNoticeType = Archon.Notices.v0.ListedNotice
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>()
|
||||
|
||||
@@ -32,28 +34,23 @@ const assignedNodes = computed(() => assigned.value.filter((n) => n.kind === 'no
|
||||
const inputField = ref('')
|
||||
|
||||
async function refresh() {
|
||||
await useServersFetch('notices').then((res) => {
|
||||
const notices = res as ServerNoticeType[]
|
||||
assigned.value = notices.find((n) => n.id === notice.value?.id)?.assigned ?? []
|
||||
})
|
||||
const notices = await client.archon.notices_v0.list()
|
||||
assigned.value = notices.find((n) => n.id === notice.value?.id)?.assigned ?? []
|
||||
}
|
||||
|
||||
async function assign(server: boolean = true) {
|
||||
const input = inputField.value.trim()
|
||||
|
||||
if (input !== '' && notice.value) {
|
||||
await useServersFetch(
|
||||
`notices/${notice.value.id}/assign?${server ? 'server' : 'node'}=${input}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
},
|
||||
).catch((err) => {
|
||||
addNotification({
|
||||
title: 'Error assigning notice',
|
||||
text: err,
|
||||
type: 'error',
|
||||
await client.archon.notices_v0
|
||||
.assign(notice.value.id, server ? { server: input } : { node: input })
|
||||
.catch((err) => {
|
||||
addNotification({
|
||||
title: 'Error assigning notice',
|
||||
text: err,
|
||||
type: 'error',
|
||||
})
|
||||
})
|
||||
})
|
||||
} else {
|
||||
addNotification({
|
||||
title: 'Error assigning notice',
|
||||
@@ -84,18 +81,15 @@ async function unassignDetect() {
|
||||
|
||||
async function unassign(id: string, server: boolean = true) {
|
||||
if (notice.value) {
|
||||
await useServersFetch(
|
||||
`notices/${notice.value.id}/unassign?${server ? 'server' : 'node'}=${id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
},
|
||||
).catch((err) => {
|
||||
addNotification({
|
||||
title: 'Error unassigning notice',
|
||||
text: err,
|
||||
type: 'error',
|
||||
await client.archon.notices_v0
|
||||
.unassign(notice.value.id, server ? { server: id } : { node: id })
|
||||
.catch((err) => {
|
||||
addNotification({
|
||||
title: 'Error unassigning notice',
|
||||
text: err,
|
||||
type: 'error',
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
await refresh()
|
||||
}
|
||||
@@ -125,7 +119,7 @@ defineExpose({ show, hide })
|
||||
:level="notice.level"
|
||||
:message="notice.message"
|
||||
:dismissable="notice.dismissable"
|
||||
:title="notice.title"
|
||||
:title="notice.title ?? undefined"
|
||||
preview
|
||||
/>
|
||||
<div class="flex flex-col gap-2">
|
||||
|
||||
@@ -133,6 +133,7 @@ import { CheckIcon, PlusIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Combobox,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
NewModal,
|
||||
StyledInput,
|
||||
@@ -143,9 +144,9 @@ import { DEFAULT_CREDIT_EMAIL_MESSAGE } from '@modrinth/utils/utils.ts'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useBaseFetch } from '#imports'
|
||||
import { useServersFetch } from '~/composables/servers/servers-fetch.ts'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const client = injectModrinthClient()
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>()
|
||||
|
||||
@@ -205,12 +206,12 @@ const applyDisabled = computed(() => {
|
||||
async function ensureOverview() {
|
||||
if (regions.value.length || nodeHostnames.value.length) return
|
||||
try {
|
||||
const data = await useServersFetch<any>('/nodes/overview', { version: 'internal' })
|
||||
regions.value = (data.regions || []).map((r: any) => ({
|
||||
const data = await client.archon.nodes_internal.overview()
|
||||
regions.value = data.regions.map((r) => ({
|
||||
value: r.key,
|
||||
label: `${r.display_name} (${r.key})`,
|
||||
}))
|
||||
nodeHostnames.value = data.node_hostnames || []
|
||||
nodeHostnames.value = data.node_hostnames
|
||||
if (!selectedRegion.value && regions.value.length) selectedRegion.value = regions.value[0].value
|
||||
} catch (err) {
|
||||
addNotification({ title: 'Failed to load nodes overview', text: String(err), type: 'error' })
|
||||
|
||||
@@ -198,6 +198,7 @@ import {
|
||||
ButtonStyled,
|
||||
Chips,
|
||||
Combobox,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
NewModal,
|
||||
StyledInput,
|
||||
@@ -207,13 +208,12 @@ import {
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useServersFetch } from '~/composables/servers/servers-fetch.ts'
|
||||
|
||||
const emit = defineEmits<{
|
||||
success: []
|
||||
}>()
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const client = injectModrinthClient()
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>()
|
||||
|
||||
@@ -341,12 +341,12 @@ const submitDisabled = computed(() => {
|
||||
async function ensureOverview() {
|
||||
if (regions.value.length || nodeHostnames.value.length) return
|
||||
try {
|
||||
const data = await useServersFetch<any>('/nodes/overview', { version: 'internal' })
|
||||
regions.value = (data.regions || []).map((r: any) => ({
|
||||
const data = await client.archon.nodes_internal.overview()
|
||||
regions.value = data.regions.map((r) => ({
|
||||
value: r.key,
|
||||
label: `${r.display_name} (${r.key})`,
|
||||
}))
|
||||
nodeHostnames.value = data.node_hostnames || []
|
||||
nodeHostnames.value = data.node_hostnames
|
||||
if (!selectedRegion.value && regions.value.length) {
|
||||
selectedRegion.value = regions.value[0].value
|
||||
}
|
||||
@@ -364,30 +364,22 @@ async function submit() {
|
||||
scheduleOption.value === 'now' ? undefined : dayjs(scheduledDate.value).toISOString()
|
||||
|
||||
if (mode.value === 'servers') {
|
||||
await useServersFetch('/transfers/schedule/servers', {
|
||||
version: 'internal',
|
||||
method: 'POST',
|
||||
body: {
|
||||
server_ids: parsedServerIds.value,
|
||||
scheduled_at: scheduledAt,
|
||||
target_region: selectedRegion.value || undefined,
|
||||
node_tags: selectedTags.value.length > 0 ? selectedTags.value : undefined,
|
||||
reason: reason.value.trim(),
|
||||
},
|
||||
await client.archon.transfers_internal.scheduleServers({
|
||||
server_ids: parsedServerIds.value,
|
||||
scheduled_at: scheduledAt,
|
||||
target_region: selectedRegion.value || undefined,
|
||||
node_tags: selectedTags.value.length > 0 ? selectedTags.value : undefined,
|
||||
reason: reason.value.trim(),
|
||||
})
|
||||
} else {
|
||||
await useServersFetch('/transfers/schedule/nodes', {
|
||||
version: 'internal',
|
||||
method: 'POST',
|
||||
body: {
|
||||
node_hostnames: selectedNodes.value.slice(),
|
||||
scheduled_at: scheduledAt,
|
||||
target_region: selectedRegion.value || undefined,
|
||||
node_tags: selectedTags.value.length > 0 ? selectedTags.value : undefined,
|
||||
reason: reason.value.trim(),
|
||||
cordon_nodes: cordonNodes.value,
|
||||
tag_nodes: tagNodes.value.trim() || undefined,
|
||||
},
|
||||
await client.archon.transfers_internal.scheduleNodes({
|
||||
node_hostnames: selectedNodes.value.slice(),
|
||||
scheduled_at: scheduledAt,
|
||||
target_region: selectedRegion.value || undefined,
|
||||
node_tags: selectedTags.value.length > 0 ? selectedTags.value : undefined,
|
||||
reason: reason.value.trim(),
|
||||
cordon_nodes: cordonNodes.value,
|
||||
tag_nodes: tagNodes.value.trim() || undefined,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,12 @@
|
||||
>{{ isIncome ? '' : '-' }}{{ formatMoney(transaction.amount) }}</span
|
||||
>
|
||||
<template v-if="transaction.type === 'withdrawal' && transaction.status === 'in-transit'">
|
||||
<Tooltip theme="dismissable-prompt" :triggers="['hover', 'focus']" no-auto-focus>
|
||||
<Tooltip
|
||||
theme="dismissable-prompt"
|
||||
class="inline-flex shrink-0"
|
||||
:triggers="['hover', 'focus']"
|
||||
no-auto-focus
|
||||
>
|
||||
<span class="my-auto align-middle"
|
||||
><ButtonStyled circular type="outlined" size="small">
|
||||
<button class="align-middle" @click="cancelPayout">
|
||||
|
||||
Reference in New Issue
Block a user