This commit is contained in:
Calum H. (IMB11)
2026-06-26 17:09:23 +01:00
parent 417c6964f6
commit 4986bc194d
13 changed files with 99 additions and 216 deletions
@@ -17,6 +17,7 @@ const props = defineProps<{
ariaLabel?: string
belowModal?: boolean
hideWhenModalOpen?: boolean
toolbarMaxWidth?: string
}>()
const INTERCOM_BUBBLE_GAP = 8
@@ -46,6 +47,11 @@ const barStyle = computed(() => ({
'--floating-action-bar-left-offset': leftOffset.value,
'--floating-action-bar-right-offset': rightOffset.value,
}))
const toolbarStyle = computed(() =>
props.toolbarMaxWidth
? { '--floating-action-bar-toolbar-max-width': props.toolbarMaxWidth }
: undefined,
)
function checkCompact() {
const el = toolbarEl.value
@@ -203,8 +209,9 @@ onUnmounted(() => {
ref="toolbarEl"
role="toolbar"
:aria-label="ariaLabel"
class="relative overflow-clip flex items-center gap-1.5 rounded-[20px] bg-surface-3 border border-surface-5 border-solid mx-auto md:max-w-[60vw] px-3 py-2.5 shadow-[0px_1px_3px_0px_rgba(0,0,0,0.3),0px_6px_10px_0px_rgba(0,0,0,0.15)]"
class="floating-action-toolbar relative overflow-clip flex items-center gap-1.5 rounded-[20px] bg-surface-3 border border-surface-5 border-solid mx-auto px-3 py-2.5 shadow-[0px_1px_3px_0px_rgba(0,0,0,0.3),0px_6px_10px_0px_rgba(0,0,0,0.15)]"
:class="{ 'bar-compact': compact }"
:style="toolbarStyle"
>
<slot />
</div>
@@ -220,6 +227,12 @@ onUnmounted(() => {
transition: bottom 0.25s ease-in-out;
}
@media (min-width: 768px) {
.floating-action-toolbar {
max-width: var(--floating-action-bar-toolbar-max-width, 60vw);
}
}
.floating-action-bar-enter-active {
transition:
transform 0.25s cubic-bezier(0.15, 1.4, 0.64, 0.96),
@@ -5,7 +5,7 @@
class="flex flex-col gap-2"
>
<span class="font-semibold text-contrast">
{{ formatMessage(messages.worldNameLabel) }} <span class="text-red">*</span>
{{ formatMessage(messages.worldNameLabel) }}
</span>
<StyledInput
v-model="worldName"
@@ -31,42 +31,6 @@
row-key="id"
:row-transition-name="rowTransitionName"
>
<template #header-world="{ column }">
<span class="inline-flex min-w-0 max-w-full items-center gap-1 font-semibold">
<span class="min-w-0 truncate">{{ column.label }}</span>
<Tooltip
theme="dismissable-prompt"
class="inline-flex shrink-0"
:triggers="['hover', 'focus']"
:popper-triggers="['hover', 'focus']"
popper-class="v-popper--interactive"
placement="top"
:delay="{ show: 200, hide: 100 }"
no-auto-focus
>
<button
type="button"
:aria-label="formatMessage(messages.instanceTooltipTitle)"
class="inline-flex cursor-help items-center justify-center border-0 bg-transparent p-0 text-secondary transition-colors hover:text-contrast"
>
<UnknownIcon class="size-4" aria-hidden="true" />
</button>
<template #popper>
<div class="grid !w-64 gap-1">
<h3 class="m-0 whitespace-nowrap text-base w-full font-bold text-contrast">
{{ formatMessage(messages.instanceTooltipTitle) }}
</h3>
<p
class="m-0 text-wrap text-sm w-full font-medium leading-tight text-secondary"
>
{{ formatMessage(messages.instanceTooltipDescription) }}
</p>
</div>
</template>
</Tooltip>
</span>
</template>
<template #cell-user="{ row: entry }">
<AutoLink
v-tooltip="actorName(entry)"
@@ -171,7 +135,7 @@
class="hidden min-h-14 bg-surface-3 @[800px]:grid @[800px]:h-14"
:class="
showWorldColumn
? '@[800px]:grid-cols-[18%_52%_20%_10%]'
? '@[800px]:grid-cols-[18%_46%_22%_14%]'
: '@[800px]:grid-cols-[26%_58%_16%]'
"
>
@@ -185,37 +149,7 @@
v-if="showWorldColumn"
class="hidden items-center px-2 font-semibold text-secondary @[800px]:flex"
>
<span class="inline-flex min-w-0 max-w-full items-center gap-1 font-semibold">
<span class="min-w-0 truncate">{{ formatMessage(messages.worldColumn) }}</span>
<Tooltip
theme="dismissable-prompt"
class="inline-flex shrink-0"
:triggers="['hover', 'focus']"
:popper-triggers="['hover', 'focus']"
popper-class="v-popper--interactive"
placement="top"
:delay="{ show: 200, hide: 100 }"
no-auto-focus
>
<button
type="button"
:aria-label="formatMessage(messages.instanceTooltipTitle)"
class="inline-flex cursor-help items-center justify-center border-0 bg-transparent p-0 text-secondary transition-colors hover:text-contrast"
>
<UnknownIcon class="size-4" aria-hidden="true" />
</button>
<template #popper>
<div class="grid !w-64 gap-1">
<h3 class="m-0 whitespace-nowrap text-base font-bold text-contrast">
{{ formatMessage(messages.instanceTooltipTitle) }}
</h3>
<p class="m-0 text-wrap text-sm font-medium leading-tight text-secondary">
{{ formatMessage(messages.instanceTooltipDescription) }}
</p>
</div>
</template>
</Tooltip>
</span>
<span class="min-w-0 truncate">{{ formatMessage(messages.worldColumn) }}</span>
</div>
<div
class="hidden items-center justify-end pl-2 pr-4 font-semibold text-secondary @[800px]:flex"
@@ -250,8 +184,7 @@
</template>
<script setup lang="ts">
import { IntercomBubbleIcon, UnknownIcon } from '@modrinth/assets'
import { Tooltip } from 'floating-vue'
import { IntercomBubbleIcon } from '@modrinth/assets'
import { computed, nextTick, onBeforeUnmount, onMounted, ref, useSlots, watch } from 'vue'
import { useFormatDateTime, useRelativeTime } from '../../../composables'
@@ -324,15 +257,6 @@ const messages = defineMessages({
id: 'servers.audit-log.column.world',
defaultMessage: 'Instance',
},
instanceTooltipTitle: {
id: 'servers.audit-log.column.world.tooltip-title',
defaultMessage: 'Coming soon!',
},
instanceTooltipDescription: {
id: 'servers.audit-log.column.world.tooltip-description',
defaultMessage:
'Server instances are contained environments with their own installed content and world files.',
},
eventColumn: {
id: 'servers.audit-log.column.event',
defaultMessage: 'Actions',
@@ -417,7 +341,7 @@ const columns = computed<TableColumn<AuditLogTableColumn>[]>(() => {
{
key: 'event',
label: formatMessage(messages.eventColumn),
width: showWorldColumn.value ? '52%' : '58%',
width: showWorldColumn.value ? '46%' : '58%',
},
]
@@ -425,7 +349,7 @@ const columns = computed<TableColumn<AuditLogTableColumn>[]>(() => {
tableColumns.push({
key: 'world',
label: formatMessage(messages.worldColumn),
width: '20%',
width: '22%',
})
}
@@ -434,7 +358,7 @@ const columns = computed<TableColumn<AuditLogTableColumn>[]>(() => {
label: formatMessage(messages.timeColumn),
align: 'right',
enableSorting: true,
width: showWorldColumn.value ? '10%' : '16%',
width: showWorldColumn.value ? '14%' : '16%',
})
return tableColumns
@@ -112,10 +112,6 @@ const props = withDefaults(
worlds?: HeaderWorld[]
powerDisabled?: boolean
settingsLabel?: string
showSettingsHint?: boolean
settingsHintTitle?: string
settingsHintDescription?: string
settingsHintDismissLabel?: string
actions?: HeaderAction[]
}>(),
{
@@ -131,17 +127,12 @@ const props = withDefaults(
worlds: () => [],
powerDisabled: false,
settingsLabel: 'Server settings',
showSettingsHint: false,
settingsHintTitle: '',
settingsHintDescription: '',
settingsHintDismissLabel: "Don't show again",
actions: () => [],
},
)
const emit = defineEmits<{
openSettings: []
dismissSettingsHint: []
}>()
const client = injectModrinthClient()
@@ -408,16 +399,8 @@ const settingsAction = computed<HeaderAction>(() => ({
label: props.settingsLabel,
icon: SettingsIcon,
labelHidden: true,
tooltip: props.showSettingsHint ? undefined : props.settingsLabel,
tooltip: props.settingsLabel,
onClick: () => emit('openSettings'),
prompt: {
title: props.settingsHintTitle,
description: props.settingsHintDescription,
dismissLabel: props.settingsHintDismissLabel,
shown: props.showSettingsHint,
placement: 'bottom-end',
onDismiss: () => emit('dismissSettingsHint'),
},
}))
const headerActions = computed<HeaderAction[]>(() => [
@@ -2,6 +2,7 @@
<FloatingActionBar
:shown="shown"
:aria-label="formatMessage(messages.ariaLabel)"
toolbar-max-width="min(1152px, calc(100vw - 3rem))"
hide-when-modal-open
>
<div class="flex min-w-0 items-center gap-0.5">
@@ -170,16 +170,16 @@ const messages = defineMessages({
},
resetWorldFilesButton: {
id: 'server.instance-settings.general.reset-world-files.button',
defaultMessage: 'Reset world data',
defaultMessage: 'Reset world',
},
resetWorldFilesDescription: {
id: 'server.instance-settings.general.reset-world-files.description',
defaultMessage:
'Delete the current world data and generate a new one. Your content and files will stay the same.',
'Create a fresh world for this instance. Content, files, settings, and backups stay the same.',
},
resetWorldFilesModalTitle: {
id: 'server.instance-settings.general.reset-world-files.modal.title',
defaultMessage: 'Reset world data',
defaultMessage: 'Reset world',
},
resetWorldFilesModalDescription: {
id: 'server.instance-settings.general.reset-world-files.modal.description',
@@ -200,12 +200,12 @@ const messages = defineMessages({
},
resetEverythingButton: {
id: 'server.instance-settings.general.reset-everything.button',
defaultMessage: 'Reset everything',
defaultMessage: 'Reset instance',
},
resetEverythingDescription: {
id: 'server.instance-settings.general.reset-everything.description',
defaultMessage:
'Reset your instance completely. This removes world data, content, and any configuration. A backup of the previous instance will remain available.',
'Recreate this instance from scratch. World data, content, files, and settings are removed. Previous backups will remain available.',
},
deleteInstanceButton: {
id: 'server.instance-settings.general.delete-instance.button',
@@ -214,7 +214,7 @@ const messages = defineMessages({
deleteInstanceDescription: {
id: 'server.instance-settings.general.delete-instance.description',
defaultMessage:
'Permanently delete this instance, including its content, files, settings, and backups.',
'Permanently delete this instance. World data, content, files, settings, and backups will be removed.',
},
deleteInstanceModalTitle: {
id: 'server.instance-settings.general.delete-instance.modal.title',
@@ -63,7 +63,7 @@
:has-more="hasMoreActionLogEntries"
:loading="isActionLogFiltering"
:loading-more="isLoadingMoreActionLogEntries"
:show-world-column="showAuditLogInstances"
show-world-column
:suppress-row-transitions="isActionLogSortTransitioning"
@load-more="loadMoreActionLogEntries"
>
@@ -145,14 +145,10 @@ type RoleFilter = ServerAccessRole | 'all'
const props = withDefaults(
defineProps<{
showAuditLogInstances?: boolean
userProfileLink?: (username: string) => ServerAccessUserProfileLink
}>(),
{
showAuditLogInstances: false,
},
{},
)
const showAuditLogInstances = computed(() => props.showAuditLogInstances)
const INVITE_RESEND_COOLDOWN_SECONDS = 2 * 60
@@ -294,7 +290,6 @@ const {
client,
serverId,
serverFull,
showAuditLogInstances,
addNotification,
})
@@ -36,7 +36,6 @@ type UseAccessAuditLogOptions = {
client: AbstractModrinthClient
serverId: string
serverFull: ComputedRef<Archon.Servers.v1.ServerFull | null>
showAuditLogInstances: ComputedRef<boolean>
addNotification: AbstractWebNotificationManager['addNotification']
}
@@ -47,7 +46,6 @@ export function useAccessAuditLog({
client,
serverId,
serverFull,
showAuditLogInstances,
addNotification,
}: UseAccessAuditLogOptions) {
const { formatMessage } = useVIntl()
@@ -68,9 +66,7 @@ export function useAccessAuditLog({
const worldOptions = computed(
() => serverFull.value?.worlds.map((world) => ({ id: world.id, name: world.name })) ?? [],
)
const isAuditLogWorldFilterVisible = computed(
() => showAuditLogInstances.value && worldOptions.value.length > 0,
)
const isAuditLogWorldFilterVisible = computed(() => worldOptions.value.length > 0)
const worldById = computed(
() => new Map(worldOptions.value.map((world) => [world.id, world] as const)),
@@ -130,12 +130,7 @@
:worlds="serverFull?.worlds ?? []"
:power-disabled="!!installError"
:settings-label="formatMessage(messages.serverSettings)"
:show-settings-hint="showSettingsHint"
:settings-hint-title="formatMessage(settingsHintMessages.title)"
:settings-hint-description="formatMessage(settingsHintMessages.description)"
:settings-hint-dismiss-label="formatMessage(settingsHintMessages.dismiss)"
@open-settings="openServerSettingsModal()"
@dismiss-settings-hint="dismissSettingsHint"
/>
<ServerOnboardingPanelPage v-if="isOnboarding" :browse-modpacks="handleBrowseModpacks" />
@@ -443,21 +438,6 @@ const leaveMessages = defineMessages({
},
})
const settingsHintMessages = defineMessages({
title: {
id: 'servers.manage.settings-hint.title',
defaultMessage: 'Your server settings have moved',
},
description: {
id: 'servers.manage.settings-hint.description',
defaultMessage: 'They can now be found here!',
},
dismiss: {
id: 'servers.manage.settings-hint.dismiss',
defaultMessage: "Don't show again",
},
})
const instancesHintMessages = defineMessages({
title: {
id: 'servers.manage.instances-hint.title',
@@ -712,14 +692,6 @@ const errorLog = ref('')
const errorLogFile = ref('')
const isOnboarding = computed(() => serverData.value?.flows?.intro)
const SETTINGS_HINT_KEY = 'server-panel-settings-hint-dismissed'
const settingsHintDismissed = useStorage(SETTINGS_HINT_KEY, false)
const showSettingsHint = ref(!settingsHintDismissed.value)
function dismissSettingsHint() {
showSettingsHint.value = false
settingsHintDismissed.value = true
}
const INSTANCES_HINT_KEY = 'server-panel-instances-hint-dismissed'
const instancesHintDismissed = useStorage(INSTANCES_HINT_KEY, false)
const showInstancesHint = ref(!instancesHintDismissed.value)
@@ -201,9 +201,13 @@ async function loadContentSummary(
index: number,
): Promise<ContentSummary> {
try {
const content = await client.archon.content_v1.getAddons(serverId, world.id, {
addons: true,
updates: false,
const content = await queryClient.fetchQuery({
queryKey: ['content', 'list', 'v1', serverId, world.id],
queryFn: () =>
client.archon.content_v1.getAddons(serverId, world.id, {
from_modpack: false,
}),
staleTime: 0,
})
return {
@@ -211,13 +215,35 @@ async function loadContentSummary(
loader: content.modloader ?? world.content?.modloader ?? null,
loaderVersion: content.modloader_version ?? world.content?.modloader_version ?? null,
linkedModpack: getLinkedModpack(content.modpack),
installedContentCount: content.addons?.length ?? 0,
installedContentCount: await getInstalledContentCount(world.id, content),
}
} catch {
return createDummyContentSummary(world, index)
}
}
async function getInstalledContentCount(
worldId: string,
content: Archon.Content.v1.Addons,
): Promise<number> {
const addonCount = content.addons?.length ?? 0
if (!content.modpack) return addonCount
try {
const modpackContent = await queryClient.fetchQuery({
queryKey: ['content', 'list', 'v1', serverId, worldId, 'modpack'],
queryFn: () =>
client.archon.content_v1.getAddons(serverId, worldId, {
from_modpack: true,
}),
staleTime: 0,
})
return addonCount + (modpackContent.addons?.length ?? 0)
} catch {
return addonCount
}
}
function toWorldSlot(world: Archon.Servers.v1.WorldFull, content: ContentSummary): WorldSlot {
return {
type: 'world',