refactor: align files tab with content tab design (#5621)

* fix: files.vue bugs before styling changes

* feat: move files tab to shared layout structure

* fix: qa

* fix: qa

* fix: bugs

* fix: lint

* fix: admonition cleanup with progress + actions

* fix: cleanup

* fix: modals

* fix: admon title

* fix: i18n standard

* fix: lint + i18n pass

* fix: remove transition

* fix: type errors

* feat: files tab in app

* fix: qa

* fix: backup item minmax

* fix: use ContentPageHeader for server panel

* fix: lint

* fix: lint

* fix: lint

* feat: page leave safety

* fix: lint

* fix: cargo fmt fix

* fix: blank in prod

* fix: content card table stuff

* Revert "fix: blank in prod"

This reverts commit 74758fe185cf85a4a20355857f889cb091b97ace.

* fix: import

* feat: browse worlds/servers flow

* fix: worlds tab parity with content tab

* fix: perf bug + shader filter pill copy

* feat: singleplayer filter

* fix: ordering

* fix: breadcrumbs

* fix: lint

* fix: qa

* feat: store server proj id when adding to a non-linked instance

* fix: lint

* fix: i18n + qa

* fix: conflict

* qa: already installed modal + placeholders not server-specific

* fix: qa

* fix: add + edit server modals

* fix: qa

* fix: security

* fix: devin flags

* fix: lint

* chore: change file to break build cache

* fix: admon

* fix: import path stuff

* feat: qa

* fix: fmt fmt idiot

---------

Signed-off-by: Calum H. <calum@modrinth.com>
This commit is contained in:
Calum H.
2026-03-26 18:55:15 +00:00
committed by GitHub
parent 706eb800cb
commit 381ea51cce
170 changed files with 8052 additions and 4571 deletions

View File

@@ -1076,6 +1076,7 @@ import {
injectModrinthClient,
injectNotificationManager,
IntlFormatted,
NavTabs,
NewModal,
OpenInAppModal,
OverflowMenu,
@@ -1115,7 +1116,6 @@ import AutomaticAccordion from '~/components/ui/AutomaticAccordion.vue'
import CollectionCreateModal from '~/components/ui/create/CollectionCreateModal.vue'
import MessageBanner from '~/components/ui/MessageBanner.vue'
import ModerationChecklist from '~/components/ui/moderation/checklist/ModerationChecklist.vue'
import NavTabs from '~/components/ui/NavTabs.vue'
import ProjectMemberHeader from '~/components/ui/ProjectMemberHeader.vue'
import { saveFeatureFlags } from '~/composables/featureFlags.ts'
import { STALE_TIME, STALE_TIME_LONG } from '~/composables/queries/project'

View File

@@ -1,5 +1,6 @@
<template>
<div>
<ConfirmLeaveModal ref="confirmLeaveModal" />
<div class="universal-card">
<div class="markdown-disclaimer">
<h2>Description</h2>
@@ -41,9 +42,11 @@
import { TriangleAlertIcon } from '@modrinth/assets'
import { countText, MIN_DESCRIPTION_CHARS } from '@modrinth/moderation'
import {
ConfirmLeaveModal,
injectProjectPageContext,
MarkdownEditor,
UnsavedChangesPopup,
usePageLeaveSafety,
useSavable,
} from '@modrinth/ui'
import { TeamMemberPermission } from '@modrinth/utils'
@@ -53,13 +56,15 @@ import { useImageUpload } from '~/composables/image-upload.ts'
const { projectV2: project, currentMember, patchProject } = injectProjectPageContext()
const { saved, current, saving, reset, save } = useSavable(
const { saved, current, saving, hasChanges, reset, save } = useSavable(
() => ({ description: project.value.body }),
async ({ description }) => {
await patchProject({ body: description })
},
)
const { confirmLeaveModal } = usePageLeaveSafety(hasChanges)
const descriptionWarning = computed(() => {
const text = current.value.description?.trim() || ''
const charCount = countText(text)

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import {
ConfirmLeaveModal,
defineMessages,
IconSelect,
injectProjectPageContext,
@@ -7,6 +8,7 @@ import {
SettingsLabel,
StyledInput,
UnsavedChangesPopup,
usePageLeaveSafety,
useSavable,
useVIntl,
} from '@modrinth/ui'
@@ -15,7 +17,7 @@ const { formatMessage } = useVIntl()
const { projectV2: project, patchProject } = injectProjectPageContext()
const { saved, current, saving, reset, save } = useSavable(
const { saved, current, saving, hasChanges, reset, save } = useSavable(
() => ({
title: project.value.title,
tagline: project.value.description,
@@ -31,6 +33,8 @@ const { saved, current, saving, reset, save } = useSavable(
},
)
const { confirmLeaveModal } = usePageLeaveSafety(hasChanges)
const messages = defineMessages({
nameTitle: {
id: 'project.settings.general.name.title',
@@ -117,6 +121,7 @@ const placeholder = computed(() => placeholders[placeholderIndex.value] ?? place
</script>
<template>
<div>
<ConfirmLeaveModal ref="confirmLeaveModal" />
<UnsavedChangesPopup
:original="saved"
:modified="current"

View File

@@ -302,6 +302,7 @@
@reset="resetChanges"
@save="handleSave"
/>
<ConfirmLeaveModal ref="confirmLeaveModal" />
</div>
</template>
@@ -319,12 +320,14 @@ import { MIN_SUMMARY_CHARS } from '@modrinth/moderation'
import {
Avatar,
Combobox,
ConfirmLeaveModal,
ConfirmModal,
injectModrinthClient,
injectNotificationManager,
injectProjectPageContext,
StyledInput,
UnsavedChangesPopup,
usePageLeaveSafety,
} from '@modrinth/ui'
import { fileIsValid, formatProjectStatus, formatProjectType } from '@modrinth/utils'
@@ -480,6 +483,12 @@ const modified = computed(() => ({
deletedBanner: deletedBanner.value,
}))
const hasChanges = computed(() =>
Object.keys(modified.value).some((key) => original.value[key] !== modified.value[key]),
)
const { confirmLeaveModal } = usePageLeaveSafety(hasChanges)
function resetChanges() {
name.value = project.value.title
slug.value = project.value.slug

View File

@@ -1,5 +1,6 @@
<template>
<div>
<ConfirmLeaveModal ref="confirmLeaveModal" />
<section class="universal-card">
<h2 class="label__title size-card-header">License</h2>
<p class="label__description">
@@ -154,10 +155,12 @@
<script setup lang="ts">
import {
Checkbox,
ConfirmLeaveModal,
DropdownSelect,
injectProjectPageContext,
StyledInput,
UnsavedChangesPopup,
usePageLeaveSafety,
useSavable,
} from '@modrinth/ui'
import {
@@ -194,7 +197,7 @@ function getInitialLicense() {
)
}
const { saved, current, saving, reset, save } = useSavable(
const { saved, current, saving, hasChanges, reset, save } = useSavable(
() => ({
license: getInitialLicense(),
licenseUrl: project.value.license.url ?? '',
@@ -219,6 +222,8 @@ const { saved, current, saving, reset, save } = useSavable(
},
)
const { confirmLeaveModal } = usePageLeaveSafety(hasChanges)
const hasPermission = computed(() => {
return (currentMember.value?.permissions ?? 0) & TeamMemberPermission.EDIT_DETAILS
})

View File

@@ -1,5 +1,6 @@
<template>
<div>
<ConfirmLeaveModal ref="confirmLeaveModal" />
<section class="universal-card">
<div class="flex flex-col gap-6">
<div class="text-2xl font-semibold text-contrast">Server details</div>
@@ -161,6 +162,7 @@ import { InfoIcon, RefreshCwIcon, SpinnerIcon } from '@modrinth/assets'
import {
ButtonStyled,
Combobox,
ConfirmLeaveModal,
injectModrinthClient,
injectNotificationManager,
injectProjectPageContext,
@@ -169,6 +171,7 @@ import {
SERVER_REGIONS,
StyledInput,
UnsavedChangesPopup,
usePageLeaveSafety,
useVIntl,
} from '@modrinth/ui'
@@ -364,6 +367,19 @@ const modified = computed(() => ({
languages: languages.value,
}))
const hasChanges = computed(() =>
Object.keys(original.value).some((key) => {
const a = original.value[key]
const b = modified.value[key]
if (Array.isArray(a) && Array.isArray(b)) {
return a.length !== b.length || a.some((v, i) => v !== b[i])
}
return a !== b
}),
)
const { confirmLeaveModal } = usePageLeaveSafety(hasChanges)
function resetChanges() {
javaAddress.value = projectV3.value?.minecraft_java_server?.address ?? ''
bedrockAddress.value = projectV3.value?.minecraft_bedrock_server?.address ?? ''

View File

@@ -1,5 +1,6 @@
<template>
<div>
<ConfirmLeaveModal ref="confirmLeaveModal" />
<section class="universal-card">
<div class="label">
<h3>
@@ -143,11 +144,13 @@ import {
} from '@modrinth/assets'
import {
Checkbox,
ConfirmLeaveModal,
formatCategory,
formatCategoryHeader,
FormattedTag,
injectProjectPageContext,
UnsavedChangesPopup,
usePageLeaveSafety,
useSavable,
useVIntl,
} from '@modrinth/ui'
@@ -187,7 +190,7 @@ const matchesProjectType = (x: Category) => {
}
}
const { saved, current, saving, reset, save } = useSavable(
const { saved, current, saving, hasChanges, reset, save } = useSavable(
() => ({
selectedTags: sortedCategories(tags.value, formatCategoryName, locale.value).filter(
(x: Category) =>
@@ -237,6 +240,8 @@ const { saved, current, saving, reset, save } = useSavable(
},
)
const { confirmLeaveModal } = usePageLeaveSafety(hasChanges)
const categoryLists = computed(() => {
const lists: Record<string, Category[]> = {}
sortedCategories(tags.value, formatCategoryName, locale.value).forEach((x: Category) => {

View File

@@ -397,6 +397,7 @@ import {
injectModrinthClient,
injectNotificationManager,
IntlFormatted,
NavTabs,
NewModal,
normalizeChildren,
NormalPage,
@@ -417,7 +418,6 @@ import { useQuery } from '@tanstack/vue-query'
import dayjs from 'dayjs'
import AdPlaceholder from '~/components/ui/AdPlaceholder.vue'
import NavTabs from '~/components/ui/NavTabs.vue'
const { handleError } = injectNotificationManager()
const api = injectModrinthClient()

View File

@@ -1,7 +1,5 @@
<script setup lang="ts">
import { commonProjectTypeCategoryMessages, useVIntl } from '@modrinth/ui'
import NavTabs from '~/components/ui/NavTabs.vue'
import { commonProjectTypeCategoryMessages, NavTabs, useVIntl } from '@modrinth/ui'
const { formatMessage } = useVIntl()

View File

@@ -103,68 +103,89 @@
: `linear-gradient(180deg, rgba(153,153,153,1) 0%, rgba(87,87,87,1) 100%)`,
}"
>
<div class="border-0 border-b border-solid border-divider pb-4">
<NuxtLink to="/hosting/manage" class="breadcrumb goto-link flex w-fit items-center">
<LeftArrowIcon />
All servers
</NuxtLink>
<div class="flex w-full min-w-0 select-none flex-col items-center gap-4 pt-4 sm:flex-row">
<NuxtLink to="/hosting/manage" class="breadcrumb goto-link flex w-fit items-center">
<LeftArrowIcon />
All servers
</NuxtLink>
<ContentPageHeader>
<template #icon>
<ServerIcon
:image="
serverData.is_medal ? 'https://cdn-raw.modrinth.com/medal_icon.webp' : serverImage
"
class="drop-shadow-lg sm:drop-shadow-none"
/>
</template>
<template #title>
{{ serverData.name }}
</template>
<template #stats>
<div
class="flex min-w-0 flex-1 flex-col-reverse items-center gap-2 sm:flex-col sm:items-start"
v-if="serverData.flows?.intro"
class="flex items-center gap-2 font-semibold text-secondary"
>
<div class="flex w-full flex-col items-center gap-4 sm:flex-row">
<h1
class="m-0 w-screen flex-shrink gap-3 truncate px-3 text-center text-2xl font-bold text-contrast sm:w-full sm:p-0 sm:text-left"
>
{{ serverData.name }}
</h1>
<div
v-if="isConnected"
data-pyro-server-action-buttons
class="server-action-buttons-anim flex w-fit flex-shrink-0"
>
<PanelServerActionButton
v-if="!serverData.flows?.intro"
class="flex-shrink-0"
:is-online="isServerRunning"
:is-actioning="isActioning"
:is-installing="serverData.status === 'installing'"
:disabled="isActioning || !!error"
:server-name="serverData.name"
:server-data="serverData"
:uptime-seconds="uptimeSeconds"
:busy-reason="
busyReasons.length > 0 ? formatMessage(busyReasons[0].reason) : undefined
"
@action="sendPowerAction"
/>
</div>
<SettingsIcon /> Configuring server...
</div>
<div v-else class="flex flex-wrap items-center gap-2">
<div v-if="serverData.loader" class="flex items-center gap-2 font-medium capitalize">
<LoaderIcon :loader="serverData.loader" class="flex shrink-0 [&&]:size-5" />
{{ serverData.loader }} {{ serverData.mc_version }}
</div>
<div
v-if="serverData.flows?.intro"
class="flex items-center gap-2 font-semibold text-secondary"
v-if="serverData.loader && serverData.net?.domain"
class="h-1.5 w-1.5 rounded-full bg-surface-5"
></div>
<div
v-if="serverData.net?.domain"
v-tooltip="'Copy server address'"
class="flex cursor-pointer items-center gap-2 font-medium hover:underline"
@click="copyServerAddress"
>
<SettingsIcon /> Configuring server...
<LinkIcon class="flex size-5 shrink-0" />
{{ serverData.net.domain }}.modrinth.gg
</div>
<ServerInfoLabels
v-else
<div v-if="uptimeSeconds" class="h-1.5 w-1.5 rounded-full bg-surface-5"></div>
<div v-if="uptimeSeconds" class="flex items-center gap-2 font-medium">
<TimerIcon class="flex size-5 shrink-0" />
{{ formattedUptime }}
</div>
<div
v-if="serverProject && (serverData.loader || serverData.net?.domain || uptimeSeconds)"
class="h-1.5 w-1.5 rounded-full bg-surface-5"
></div>
<div v-if="serverProject" class="flex items-center gap-1.5 font-medium text-primary">
Linked to
<Avatar :src="serverProject.icon_url" :alt="serverProject.title" size="24px" />
<NuxtLink
:to="`/project/${serverProject.slug ?? serverProject.id}`"
class="truncate text-primary hover:underline"
>
{{ serverProject.title }}
</NuxtLink>
</div>
</div>
</template>
<template #actions>
<div v-if="isConnected && !serverData.flows?.intro" class="flex gap-2">
<PanelServerActionButton
:is-online="isServerRunning"
:is-actioning="isActioning"
:is-installing="serverData.status === 'installing'"
:disabled="isActioning || !!error"
:server-name="serverData.name"
:server-data="serverData"
:show-game-label="showGameLabel"
:show-loader-label="showLoaderLabel"
:uptime-seconds="uptimeSeconds"
:linked="true"
class="server-action-buttons-anim flex min-w-0 flex-col flex-wrap items-center gap-4 text-secondary *:hidden sm:flex-row sm:*:flex"
:busy-reason="busyReasons.length > 0 ? formatMessage(busyReasons[0].reason) : undefined"
@action="sendPowerAction"
/>
</div>
</div>
</div>
</template>
</ContentPageHeader>
<ServerOnboardingPanelPage v-if="serverData.flows?.intro" />
@@ -351,24 +372,29 @@ import {
IssuesIcon,
LayoutTemplateIcon,
LeftArrowIcon,
LinkIcon,
LockIcon,
RightArrowIcon,
SettingsIcon,
TimerIcon,
TransferIcon,
} from '@modrinth/assets'
import type { BusyReason } from '@modrinth/ui'
import {
Avatar,
BackupProgressAdmonitions,
ButtonStyled,
ContentPageHeader,
defineMessage,
ErrorInformationCard,
formatLoaderLabel,
injectModrinthClient,
injectNotificationManager,
InstallingBanner,
LoaderIcon,
NavTabs,
provideModrinthServerContext,
ServerIcon,
ServerInfoLabels,
ServerNotice,
ServerOnboardingPanelPage,
useDebugLogger,
@@ -381,7 +407,6 @@ import DOMPurify from 'dompurify'
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
import { reloadNuxtApp } from '#app'
import NavTabs from '~/components/ui/NavTabs.vue'
import PanelErrorIcon from '~/components/ui/servers/icons/PanelErrorIcon.vue'
import MedalServerCountdown from '~/components/ui/servers/marketing/MedalServerCountdown.vue'
import PanelServerActionButton from '~/components/ui/servers/PanelServerActionButton.vue'
@@ -589,6 +614,30 @@ provideModrinthServerContext({
})
const uptimeSeconds = ref(0)
const formattedUptime = computed(() => {
const days = Math.floor(uptimeSeconds.value / (24 * 3600))
const hours = Math.floor((uptimeSeconds.value % (24 * 3600)) / 3600)
const minutes = Math.floor((uptimeSeconds.value % 3600) / 60)
const seconds = uptimeSeconds.value % 60
let formatted = ''
if (days > 0) formatted += `${days}d `
if (hours > 0 || days > 0) formatted += `${hours}h `
formatted += `${minutes}m ${seconds}s`
return formatted.trim()
})
function copyServerAddress() {
if (!serverData.value?.net?.domain) return
navigator.clipboard.writeText(serverData.value.net.domain + '.modrinth.gg')
addNotification({
title: 'Server address copied',
text: "Your server's address has been copied to your clipboard.",
type: 'success',
})
}
const copied = ref(false)
const error = ref<Error | null>(null)
@@ -628,9 +677,6 @@ const stats = ref<Stats>({
},
})
const showGameLabel = computed(() => !!serverData.value?.game)
const showLoaderLabel = computed(() => !!serverData.value?.loader)
const navLinks = [
{
label: 'Overview',

View File

@@ -18,9 +18,7 @@
<script setup lang="ts">
import { FolderIcon, ReportIcon, ShieldCheckIcon } from '@modrinth/assets'
import { Chips, defineMessages, useVIntl } from '@modrinth/ui'
import NavTabs from '@/components/ui/NavTabs.vue'
import { Chips, defineMessages, NavTabs, useVIntl } from '@modrinth/ui'
definePageMeta({
middleware: 'auth',

View File

@@ -1,10 +1,8 @@
<script setup lang="ts">
import { getChangelog, type Product } from '@modrinth/blog'
import { ChangelogEntry } from '@modrinth/ui'
import { ChangelogEntry, NavTabs } from '@modrinth/ui'
import Timeline from '@modrinth/ui/src/components/base/Timeline.vue'
import NavTabs from '~/components/ui/NavTabs.vue'
const route = useRoute()
const filter = ref<Product | undefined>(undefined)

View File

@@ -299,6 +299,7 @@ import {
commonMessages,
ContentPageHeader,
injectModrinthClient,
NavTabs,
OverflowMenu,
ProjectCard,
ProjectCardList,
@@ -313,7 +314,6 @@ import UpToDate from '~/assets/images/illustrations/up_to_date.svg?component'
import AdPlaceholder from '~/components/ui/AdPlaceholder.vue'
import ModalCreation from '~/components/ui/create/ProjectCreateModal.vue'
import NavStack from '~/components/ui/NavStack.vue'
import NavTabs from '~/components/ui/NavTabs.vue'
import { acceptTeamInvite, removeTeamMember } from '~/helpers/teams.js'
import {
OrganizationContext,

View File

@@ -495,6 +495,7 @@ import {
injectModrinthClient,
injectNotificationManager,
IntlFormatted,
NavTabs,
NewModal,
OverflowMenu,
ProjectCard,
@@ -521,7 +522,6 @@ import UpToDate from '~/assets/images/illustrations/up_to_date.svg?component'
import AdPlaceholder from '~/components/ui/AdPlaceholder.vue'
import CollectionCreateModal from '~/components/ui/create/CollectionCreateModal.vue'
import ModalCreation from '~/components/ui/create/ProjectCreateModal.vue'
import NavTabs from '~/components/ui/NavTabs.vue'
import { reportUser } from '~/utils/report-helpers.ts'
const data = useNuxtApp()