mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 03:55:59 +00:00
feat: invite management frontend
This commit is contained in:
@@ -1,31 +1,159 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="flex flex-col gap-8">
|
||||||
|
<section class="flex flex-col gap-4">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<h3 class="m-0 text-lg font-semibold text-contrast">
|
||||||
|
{{ formatMessage(messages.activeInvitesTitle) }}
|
||||||
|
</h3>
|
||||||
|
<p class="m-0 text-secondary">
|
||||||
|
{{ formatMessage(messages.activeInvitesDescription) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table :columns="inviteColumns" :data="activeInvites" row-key="id" table-min-width="36rem">
|
||||||
|
<template #empty-state>
|
||||||
|
<div class="flex h-40 items-center justify-center text-secondary">
|
||||||
|
{{ formatMessage(messages.noActiveInvites) }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #cell-id="{ row }">
|
||||||
|
<CopyCode
|
||||||
|
:text="`${config.siteUrl}/share/${encodeURIComponent(row.id)}`"
|
||||||
|
:display-text="row.id"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #cell-uses="{ row }">
|
||||||
|
<span class="font-medium text-primary">{{ row.uses }}</span>
|
||||||
|
<span> / {{ row.maxUses }}</span>
|
||||||
|
</template>
|
||||||
|
<template #cell-expiration="{ row }">
|
||||||
|
<span v-tooltip="formatDateTime(row.expiration)" class="whitespace-nowrap">
|
||||||
|
{{ formatRelativeTime(row.expiration) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template #cell-actions="{ row }">
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<ButtonStyled circular type="transparent">
|
||||||
|
<button
|
||||||
|
v-tooltip="formatMessage(messages.revokeInvite)"
|
||||||
|
:aria-label="
|
||||||
|
formatMessage(messages.revokeInviteWithCode, {
|
||||||
|
code: row.id,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
class="text-secondary hover:!filter-none hover:text-red focus-visible:!filter-none"
|
||||||
|
@click="revokeInviteModal?.show(row.id)"
|
||||||
|
>
|
||||||
|
<XIcon aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
</section>
|
||||||
|
|
||||||
<SharedInstanceInstallationSettingsControls
|
<SharedInstanceInstallationSettingsControls
|
||||||
can-unpublish
|
can-unpublish
|
||||||
:busy="isBusy"
|
:busy="isBusy"
|
||||||
:unpublishing="unpublishing"
|
:unpublishing="unpublishing"
|
||||||
:unpublish="unpublishSharedInstance"
|
:unpublish="unpublishSharedInstance"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ConfirmRevokeSharedInstanceInviteModal
|
||||||
|
ref="revokeInviteModal"
|
||||||
|
@revoke="revokeInvite"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { injectNotificationManager } from '@modrinth/ui'
|
import { XIcon } from '@modrinth/assets'
|
||||||
|
import {
|
||||||
|
ButtonStyled,
|
||||||
|
CopyCode,
|
||||||
|
defineMessages,
|
||||||
|
injectNotificationManager,
|
||||||
|
Table,
|
||||||
|
type TableColumn,
|
||||||
|
useFormatDateTime,
|
||||||
|
useRelativeTime,
|
||||||
|
useVIntl,
|
||||||
|
} from '@modrinth/ui'
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
import ConfirmRevokeSharedInstanceInviteModal from '@/components/ui/shared-instances/ConfirmRevokeSharedInstanceInviteModal.vue'
|
||||||
import SharedInstanceInstallationSettingsControls from '@/components/ui/shared-instances/SharedInstanceInstallationSettingsControls.vue'
|
import SharedInstanceInstallationSettingsControls from '@/components/ui/shared-instances/SharedInstanceInstallationSettingsControls.vue'
|
||||||
import { unpublish_shared_instance } from '@/helpers/instance'
|
import { config } from '@/config'
|
||||||
|
import { type SharedInstanceInvite, unpublish_shared_instance } from '@/helpers/instance'
|
||||||
import { injectInstanceSettings } from '@/providers/instance-settings'
|
import { injectInstanceSettings } from '@/providers/instance-settings'
|
||||||
|
|
||||||
const { instance, offline, onUnlinked } = injectInstanceSettings()
|
const { instance, offline, onUnlinked } = injectInstanceSettings()
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const unpublishing = ref(false)
|
const unpublishing = ref(false)
|
||||||
|
const revokeInviteModal = ref<InstanceType<typeof ConfirmRevokeSharedInstanceInviteModal>>()
|
||||||
|
const formatRelativeTime = useRelativeTime()
|
||||||
|
const formatDateTime = useFormatDateTime({ dateStyle: 'medium', timeStyle: 'short' })
|
||||||
const isBusy = computed(
|
const isBusy = computed(
|
||||||
() => instance.value.install_stage !== 'installed' || unpublishing.value || !!offline,
|
() => instance.value.install_stage !== 'installed' || unpublishing.value || !!offline,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type InviteTableColumn = 'id' | 'uses' | 'expiration' | 'actions'
|
||||||
|
|
||||||
|
const inviteColumns = computed<TableColumn<InviteTableColumn>[]>(() => [
|
||||||
|
{
|
||||||
|
key: 'id',
|
||||||
|
label: formatMessage(messages.inviteCodeLabel),
|
||||||
|
width: 'clamp(11rem, 34%, 19rem)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'uses',
|
||||||
|
label: formatMessage(messages.usesLabel),
|
||||||
|
width: 'clamp(7rem, 18%, 10rem)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'expiration',
|
||||||
|
label: formatMessage(messages.expiresLabel),
|
||||||
|
width: 'clamp(9rem, 28%, 14rem)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'actions',
|
||||||
|
label: formatMessage(messages.actionsLabel),
|
||||||
|
align: 'right',
|
||||||
|
width: '5.5rem',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
|
||||||
|
// TODO: Use actual endpoint
|
||||||
|
const activeInvites = ref<SharedInstanceInvite[]>([
|
||||||
|
{
|
||||||
|
id: 'wqHPxNagZr',
|
||||||
|
expiration: new Date(now + 6 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
maxUses: 10,
|
||||||
|
uses: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'GbRGfY7hbs',
|
||||||
|
expiration: new Date(now + 3 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
maxUses: 10,
|
||||||
|
uses: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'k9mvD2QxLc',
|
||||||
|
expiration: new Date(now + 90 * 60 * 1000).toISOString(),
|
||||||
|
maxUses: 5,
|
||||||
|
uses: 4,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
function revokeInvite(inviteId: string) {
|
||||||
|
activeInvites.value = activeInvites.value.filter((invite) => invite.id !== inviteId)
|
||||||
|
}
|
||||||
|
|
||||||
async function unpublishSharedInstance() {
|
async function unpublishSharedInstance() {
|
||||||
unpublishing.value = true
|
unpublishing.value = true
|
||||||
try {
|
try {
|
||||||
@@ -39,4 +167,43 @@ async function unpublishSharedInstance() {
|
|||||||
unpublishing.value = false
|
unpublishing.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
activeInvitesTitle: {
|
||||||
|
id: 'instance.settings.sharing.active-invites.title',
|
||||||
|
defaultMessage: 'Active invites',
|
||||||
|
},
|
||||||
|
activeInvitesDescription: {
|
||||||
|
id: 'instance.settings.sharing.active-invites.description',
|
||||||
|
defaultMessage: 'Anyone with one of these invite codes can join while it remains active.',
|
||||||
|
},
|
||||||
|
inviteCodeLabel: {
|
||||||
|
id: 'instance.settings.sharing.active-invites.code',
|
||||||
|
defaultMessage: 'Invite code',
|
||||||
|
},
|
||||||
|
usesLabel: {
|
||||||
|
id: 'instance.settings.sharing.active-invites.uses',
|
||||||
|
defaultMessage: 'Uses',
|
||||||
|
},
|
||||||
|
expiresLabel: {
|
||||||
|
id: 'instance.settings.sharing.active-invites.expires',
|
||||||
|
defaultMessage: 'Expires',
|
||||||
|
},
|
||||||
|
actionsLabel: {
|
||||||
|
id: 'instance.settings.sharing.active-invites.actions',
|
||||||
|
defaultMessage: 'Actions',
|
||||||
|
},
|
||||||
|
noActiveInvites: {
|
||||||
|
id: 'instance.settings.sharing.active-invites.empty',
|
||||||
|
defaultMessage: 'There are no active invites.',
|
||||||
|
},
|
||||||
|
revokeInvite: {
|
||||||
|
id: 'instance.settings.sharing.active-invites.revoke',
|
||||||
|
defaultMessage: 'Revoke invite',
|
||||||
|
},
|
||||||
|
revokeInviteWithCode: {
|
||||||
|
id: 'instance.settings.sharing.active-invites.revoke-with-code',
|
||||||
|
defaultMessage: 'Revoke invite {code}',
|
||||||
|
},
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+82
@@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<NewModal ref="modal" :header="formatMessage(messages.header)" fade="danger" max-width="500px">
|
||||||
|
<Admonition type="critical" :header="formatMessage(messages.admonitionHeader)">
|
||||||
|
<IntlFormatted :message-id="messages.admonitionBody" :values="{ code: inviteCode }">
|
||||||
|
<template #monospace="{ children }">
|
||||||
|
<code class="font-mono"><component :is="() => children" /></code>
|
||||||
|
</template>
|
||||||
|
</IntlFormatted>
|
||||||
|
</Admonition>
|
||||||
|
|
||||||
|
<template #actions>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<ButtonStyled type="outlined">
|
||||||
|
<button @click="modal?.hide()">
|
||||||
|
<XIcon />
|
||||||
|
{{ formatMessage(commonMessages.cancelButton) }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled color="red">
|
||||||
|
<button @click="confirm">
|
||||||
|
<XIcon />
|
||||||
|
{{ formatMessage(messages.revokeButton) }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</NewModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { XIcon } from '@modrinth/assets'
|
||||||
|
import {
|
||||||
|
Admonition,
|
||||||
|
ButtonStyled,
|
||||||
|
commonMessages,
|
||||||
|
defineMessages,
|
||||||
|
IntlFormatted,
|
||||||
|
NewModal,
|
||||||
|
useVIntl,
|
||||||
|
} from '@modrinth/ui'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
const modal = ref<InstanceType<typeof NewModal>>()
|
||||||
|
const inviteCode = ref('')
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
revoke: [inviteCode: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function show(code: string) {
|
||||||
|
inviteCode.value = code
|
||||||
|
modal.value?.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirm() {
|
||||||
|
modal.value?.hide()
|
||||||
|
emit('revoke', inviteCode.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
header: {
|
||||||
|
id: 'instance.settings.sharing.revoke-invite.header',
|
||||||
|
defaultMessage: 'Revoke invite',
|
||||||
|
},
|
||||||
|
admonitionHeader: {
|
||||||
|
id: 'instance.settings.sharing.revoke-invite.admonition-header',
|
||||||
|
defaultMessage: 'This action cannot be undone',
|
||||||
|
},
|
||||||
|
admonitionBody: {
|
||||||
|
id: 'instance.settings.sharing.revoke-invite.admonition-body',
|
||||||
|
defaultMessage:
|
||||||
|
'The invite code <monospace>{code}</monospace> will stop working immediately. People who already joined will keep access.',
|
||||||
|
},
|
||||||
|
revokeButton: {
|
||||||
|
id: 'instance.settings.sharing.revoke-invite.confirm',
|
||||||
|
defaultMessage: 'Revoke invite',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
defineExpose({ show })
|
||||||
|
</script>
|
||||||
@@ -365,6 +365,13 @@ export interface SharedInstanceInviteLink {
|
|||||||
maxUses: number
|
maxUses: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SharedInstanceInvite {
|
||||||
|
id: string
|
||||||
|
expiration: string
|
||||||
|
maxUses: number
|
||||||
|
uses: number
|
||||||
|
}
|
||||||
|
|
||||||
export async function can_current_user_use_shared_instances(): Promise<boolean> {
|
export async function can_current_user_use_shared_instances(): Promise<boolean> {
|
||||||
return await invoke('plugin:instance|instance_share_can_current_user_use')
|
return await invoke('plugin:instance|instance_share_can_current_user_use')
|
||||||
}
|
}
|
||||||
@@ -394,6 +401,19 @@ export async function create_shared_instance_invite_link(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function get_shared_instance_invites(
|
||||||
|
instanceId: string,
|
||||||
|
): Promise<SharedInstanceInvite[]> {
|
||||||
|
return await invoke('plugin:instance|instance_share_get_invites', { instanceId })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function revoke_shared_instance_invite(
|
||||||
|
instanceId: string,
|
||||||
|
inviteId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
return await invoke('plugin:instance|instance_share_revoke_invite', { instanceId, inviteId })
|
||||||
|
}
|
||||||
|
|
||||||
export async function remove_shared_instance_users(
|
export async function remove_shared_instance_users(
|
||||||
instanceId: string,
|
instanceId: string,
|
||||||
userIds: string[],
|
userIds: string[],
|
||||||
|
|||||||
@@ -47,8 +47,10 @@
|
|||||||
@state-change="publishState = $event"
|
@state-change="publishState = $event"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div v-if="membersTableLoading" class="h-64" aria-hidden="true" />
|
||||||
|
|
||||||
<SharedInstanceMembersTable
|
<SharedInstanceMembersTable
|
||||||
v-if="members.rows.value.length > 0"
|
v-else-if="showMembersTable"
|
||||||
:rows="members.rows.value"
|
:rows="members.rows.value"
|
||||||
:actions-locked="sharedInstanceActionsLocked"
|
:actions-locked="sharedInstanceActionsLocked"
|
||||||
:invite-disabled="!hasRemainingUserSlots"
|
:invite-disabled="!hasRemainingUserSlots"
|
||||||
@@ -256,6 +258,23 @@ const lockedActionButton = computed(() =>
|
|||||||
const sharedInstanceUnavailableReason = sharedInstanceState.unavailableReason
|
const sharedInstanceUnavailableReason = sharedInstanceState.unavailableReason
|
||||||
const sharedInstanceUnavailable = computed(() => !!sharedInstanceUnavailableReason.value)
|
const sharedInstanceUnavailable = computed(() => !!sharedInstanceUnavailableReason.value)
|
||||||
const sharedInstanceUnavailableManager = sharedInstanceState.unavailableManager
|
const sharedInstanceUnavailableManager = sharedInstanceState.unavailableManager
|
||||||
|
const membersTableLoading = computed(
|
||||||
|
() =>
|
||||||
|
members.rows.value.length === 0 &&
|
||||||
|
!!props.instance.shared_instance &&
|
||||||
|
(members.query.data.value === undefined || members.query.isFetching.value) &&
|
||||||
|
!sharedInstanceUnavailable.value &&
|
||||||
|
!sharedInstanceActionsLocked.value,
|
||||||
|
)
|
||||||
|
const showMembersTable = computed(
|
||||||
|
() =>
|
||||||
|
members.rows.value.length > 0 ||
|
||||||
|
(!!props.instance.shared_instance &&
|
||||||
|
members.query.data.value !== undefined &&
|
||||||
|
!members.query.isFetching.value &&
|
||||||
|
!sharedInstanceUnavailable.value &&
|
||||||
|
!sharedInstanceActionsLocked.value),
|
||||||
|
)
|
||||||
const requiresUnlink = computed(
|
const requiresUnlink = computed(
|
||||||
() =>
|
() =>
|
||||||
props.instance.link?.type === 'imported_modpack' &&
|
props.instance.link?.type === 'imported_modpack' &&
|
||||||
|
|||||||
@@ -67,7 +67,11 @@
|
|||||||
>
|
>
|
||||||
<template #empty-state
|
<template #empty-state
|
||||||
><div class="flex h-64 items-center justify-center text-secondary">
|
><div class="flex h-64 items-center justify-center text-secondary">
|
||||||
No users match your filters.
|
{{
|
||||||
|
formatMessage(
|
||||||
|
rows.length === 0 ? messages.noUsersJoined : messages.noUsersMatchFilters,
|
||||||
|
)
|
||||||
|
}}
|
||||||
</div></template
|
</div></template
|
||||||
>
|
>
|
||||||
<template #cell-username="{ row }">
|
<template #cell-username="{ row }">
|
||||||
@@ -300,6 +304,14 @@ const messages = defineMessages({
|
|||||||
id: 'app.instance.admonitions.shared-instance.publish-button',
|
id: 'app.instance.admonitions.shared-instance.publish-button',
|
||||||
defaultMessage: 'Push update',
|
defaultMessage: 'Push update',
|
||||||
},
|
},
|
||||||
|
noUsersJoined: {
|
||||||
|
id: 'app.instance.share.members.empty',
|
||||||
|
defaultMessage: 'No users have joined yet',
|
||||||
|
},
|
||||||
|
noUsersMatchFilters: {
|
||||||
|
id: 'app.instance.share.members.no-filter-results',
|
||||||
|
defaultMessage: 'No users match your filters.',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
function userProfileLink(username: string) {
|
function userProfileLink(username: string) {
|
||||||
return !username || username.includes('@')
|
return !username || username.includes('@')
|
||||||
|
|||||||
@@ -227,6 +227,8 @@ fn main() {
|
|||||||
"instance_share_get_users",
|
"instance_share_get_users",
|
||||||
"instance_share_invite_users",
|
"instance_share_invite_users",
|
||||||
"instance_share_create_invite_link",
|
"instance_share_create_invite_link",
|
||||||
|
"instance_share_get_invites",
|
||||||
|
"instance_share_revoke_invite",
|
||||||
"instance_share_remove_users",
|
"instance_share_remove_users",
|
||||||
"instance_share_get_publish_preview",
|
"instance_share_get_publish_preview",
|
||||||
"instance_share_publish",
|
"instance_share_publish",
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
|
|||||||
instance_share_get_users,
|
instance_share_get_users,
|
||||||
instance_share_invite_users,
|
instance_share_invite_users,
|
||||||
instance_share_create_invite_link,
|
instance_share_create_invite_link,
|
||||||
|
instance_share_get_invites,
|
||||||
|
instance_share_revoke_invite,
|
||||||
instance_share_remove_users,
|
instance_share_remove_users,
|
||||||
instance_share_get_publish_preview,
|
instance_share_get_publish_preview,
|
||||||
instance_share_publish,
|
instance_share_publish,
|
||||||
@@ -825,6 +827,27 @@ pub async fn instance_share_create_invite_link(
|
|||||||
.await?)
|
.await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn instance_share_get_invites(
|
||||||
|
instance_id: &str,
|
||||||
|
) -> Result<Vec<theseus::instance::SharedInstanceInvite>> {
|
||||||
|
Ok(theseus::instance::get_shared_instance_invites(instance_id).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn instance_share_revoke_invite(
|
||||||
|
instance_id: &str,
|
||||||
|
invite_id: String,
|
||||||
|
) -> Result<()> {
|
||||||
|
Ok(
|
||||||
|
theseus::instance::revoke_shared_instance_invite(
|
||||||
|
instance_id,
|
||||||
|
invite_id,
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn instance_share_remove_users(
|
pub async fn instance_share_remove_users(
|
||||||
instance_id: &str,
|
instance_id: &str,
|
||||||
|
|||||||
@@ -42,8 +42,9 @@ pub(crate) use self::shared::{
|
|||||||
};
|
};
|
||||||
pub use self::shared::{
|
pub use self::shared::{
|
||||||
SharedInstanceExternalFilePreview, SharedInstanceInstallPreview,
|
SharedInstanceExternalFilePreview, SharedInstanceInstallPreview,
|
||||||
SharedInstanceInviteInstallPreview, SharedInstanceInviteLink,
|
SharedInstanceInvite, SharedInstanceInviteInstallPreview,
|
||||||
SharedInstanceJoinType, SharedInstancePublishPreview,
|
SharedInstanceInviteLink, SharedInstanceJoinType,
|
||||||
|
SharedInstancePublishPreview,
|
||||||
SharedInstanceUpdateDiff, SharedInstanceUpdateDiffType,
|
SharedInstanceUpdateDiff, SharedInstanceUpdateDiffType,
|
||||||
SharedInstanceUpdatePreview, SharedInstanceUser, SharedInstanceUsers,
|
SharedInstanceUpdatePreview, SharedInstanceUser, SharedInstanceUsers,
|
||||||
accept_pending_shared_instance_invite,
|
accept_pending_shared_instance_invite,
|
||||||
@@ -51,8 +52,10 @@ pub use self::shared::{
|
|||||||
can_active_user_use_shared_instances, create_shared_instance_invite_link,
|
can_active_user_use_shared_instances, create_shared_instance_invite_link,
|
||||||
decline_pending_shared_instance_invite,
|
decline_pending_shared_instance_invite,
|
||||||
get_shared_instance_install_preview, get_shared_instance_publish_preview,
|
get_shared_instance_install_preview, get_shared_instance_publish_preview,
|
||||||
get_shared_instance_update_preview, get_shared_instance_users,
|
get_shared_instance_invites, get_shared_instance_update_preview,
|
||||||
install_shared_instance, invite_shared_instance_users,
|
get_shared_instance_users, install_shared_instance,
|
||||||
publish_shared_instance, remove_shared_instance_users,
|
invite_shared_instance_users, publish_shared_instance,
|
||||||
unlink_shared_instance, unpublish_shared_instance, update_shared_instance,
|
remove_shared_instance_users, revoke_shared_instance_invite,
|
||||||
|
unlink_shared_instance, unpublish_shared_instance,
|
||||||
|
update_shared_instance,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -73,6 +73,14 @@ pub(super) struct CreateInstanceInviteResponse {
|
|||||||
pub(super) id: String,
|
pub(super) id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub(super) struct InstanceInviteResponse {
|
||||||
|
pub(super) id: String,
|
||||||
|
pub(super) expiration: DateTime<Utc>,
|
||||||
|
pub(super) max_uses: i32,
|
||||||
|
pub(super) uses: i32,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub(super) struct BlacklistStatusResponse {
|
pub(super) struct BlacklistStatusResponse {
|
||||||
pub(super) blacklisted: bool,
|
pub(super) blacklisted: bool,
|
||||||
@@ -502,6 +510,20 @@ pub(super) async fn delete_remote_invite(
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) async fn get_remote_invites(
|
||||||
|
shared_instance_id: &str,
|
||||||
|
state: &State,
|
||||||
|
) -> crate::Result<Vec<InstanceInviteResponse>> {
|
||||||
|
request_json(
|
||||||
|
"get_instance_invites",
|
||||||
|
Method::GET,
|
||||||
|
&format!("/instances/{shared_instance_id}/invites"),
|
||||||
|
None,
|
||||||
|
state,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) async fn get_shared_instance_invite_info(
|
pub(super) async fn get_shared_instance_invite_info(
|
||||||
invite_id: &str,
|
invite_id: &str,
|
||||||
state: &State,
|
state: &State,
|
||||||
|
|||||||
@@ -124,6 +124,44 @@ pub async fn create_shared_instance_invite_link(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn get_shared_instance_invites(
|
||||||
|
instance_id: &str,
|
||||||
|
) -> crate::Result<Vec<SharedInstanceInvite>> {
|
||||||
|
let state = State::get().await?;
|
||||||
|
let Some(attachment) = shared_attachment(instance_id, &state).await? else {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
};
|
||||||
|
ensure_owner(&attachment)?;
|
||||||
|
|
||||||
|
Ok(get_remote_invites(&attachment.id, &state)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|invite| SharedInstanceInvite {
|
||||||
|
id: invite.id,
|
||||||
|
expiration: invite.expiration,
|
||||||
|
max_uses: invite.max_uses,
|
||||||
|
uses: invite.uses,
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(invite_id))]
|
||||||
|
pub async fn revoke_shared_instance_invite(
|
||||||
|
instance_id: &str,
|
||||||
|
invite_id: String,
|
||||||
|
) -> crate::Result<()> {
|
||||||
|
let state = State::get().await?;
|
||||||
|
let _shared_instance_lock = state.lock_shared_instance(instance_id).await;
|
||||||
|
let Some(attachment) = shared_attachment(instance_id, &state).await? else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
ensure_owner(&attachment)?;
|
||||||
|
|
||||||
|
delete_remote_invite(&attachment.id, &invite_id, &state).await?;
|
||||||
|
emit_instance(instance_id, InstancePayloadType::Edited).await
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
pub async fn remove_shared_instance_users(
|
pub async fn remove_shared_instance_users(
|
||||||
instance_id: &str,
|
instance_id: &str,
|
||||||
|
|||||||
@@ -110,8 +110,9 @@ pub use self::install::{
|
|||||||
};
|
};
|
||||||
pub use self::invites::{
|
pub use self::invites::{
|
||||||
accept_pending_shared_instance_invite, create_shared_instance_invite_link,
|
accept_pending_shared_instance_invite, create_shared_instance_invite_link,
|
||||||
decline_pending_shared_instance_invite, get_shared_instance_users,
|
decline_pending_shared_instance_invite, get_shared_instance_invites,
|
||||||
invite_shared_instance_users, remove_shared_instance_users,
|
get_shared_instance_users, invite_shared_instance_users,
|
||||||
|
remove_shared_instance_users, revoke_shared_instance_invite,
|
||||||
};
|
};
|
||||||
pub use self::publish::{
|
pub use self::publish::{
|
||||||
get_shared_instance_publish_preview, publish_shared_instance,
|
get_shared_instance_publish_preview, publish_shared_instance,
|
||||||
@@ -119,8 +120,9 @@ pub use self::publish::{
|
|||||||
};
|
};
|
||||||
pub use self::types::{
|
pub use self::types::{
|
||||||
SharedInstanceExternalFilePreview, SharedInstanceInstallPreview,
|
SharedInstanceExternalFilePreview, SharedInstanceInstallPreview,
|
||||||
SharedInstanceInviteInstallPreview, SharedInstanceInviteLink,
|
SharedInstanceInvite, SharedInstanceInviteInstallPreview,
|
||||||
SharedInstanceJoinType, SharedInstancePublishPreview,
|
SharedInstanceInviteLink, SharedInstanceJoinType,
|
||||||
|
SharedInstancePublishPreview,
|
||||||
SharedInstanceUpdateDiff, SharedInstanceUpdateDiffType,
|
SharedInstanceUpdateDiff, SharedInstanceUpdateDiffType,
|
||||||
SharedInstanceUpdatePreview, SharedInstanceUser, SharedInstanceUsers,
|
SharedInstanceUpdatePreview, SharedInstanceUser, SharedInstanceUsers,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -116,6 +116,15 @@ pub struct SharedInstanceInviteLink {
|
|||||||
pub max_uses: i32,
|
pub max_uses: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SharedInstanceInvite {
|
||||||
|
pub id: String,
|
||||||
|
pub expiration: DateTime<Utc>,
|
||||||
|
pub max_uses: i32,
|
||||||
|
pub uses: i32,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct SharedInstanceInviteInstallPreview {
|
pub struct SharedInstanceInviteInstallPreview {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
:title="formatMessage(copiedMessage)"
|
:title="formatMessage(copiedMessage)"
|
||||||
@click="copyText"
|
@click="copyText"
|
||||||
>
|
>
|
||||||
<span>{{ text }}</span>
|
<span>{{ displayText ?? text }}</span>
|
||||||
<CheckIcon v-if="copied" />
|
<CheckIcon v-if="copied" />
|
||||||
<ClipboardCopyIcon v-else />
|
<ClipboardCopyIcon v-else />
|
||||||
</button>
|
</button>
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { CheckIcon, ClipboardCopyIcon } from '@modrinth/assets'
|
import { CheckIcon, ClipboardCopyIcon } from '@modrinth/assets'
|
||||||
import { ref } from 'vue'
|
import { onBeforeUnmount, ref } from 'vue'
|
||||||
|
|
||||||
import { defineMessage, useVIntl } from '../../composables/i18n'
|
import { defineMessage, useVIntl } from '../../composables/i18n'
|
||||||
|
|
||||||
@@ -22,12 +22,22 @@ const copiedMessage = defineMessage({
|
|||||||
})
|
})
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const props = defineProps<{ text: string }>()
|
const props = defineProps<{
|
||||||
|
text: string
|
||||||
|
displayText?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
const copied = ref(false)
|
const copied = ref(false)
|
||||||
|
let copiedResetTimeout: ReturnType<typeof setTimeout> | undefined
|
||||||
|
|
||||||
async function copyText() {
|
async function copyText() {
|
||||||
await navigator.clipboard.writeText(props.text)
|
await navigator.clipboard.writeText(props.text)
|
||||||
copied.value = true
|
copied.value = true
|
||||||
|
clearTimeout(copiedResetTimeout)
|
||||||
|
copiedResetTimeout = setTimeout(() => {
|
||||||
|
copied.value = false
|
||||||
|
}, 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => clearTimeout(copiedResetTimeout))
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user