Files
Modrinth-plus/apps/frontend/src/components/ui/project-settings/CompatibilityCard.vue
Calum H. 7d92e4ec7f feat: content tab rewrite for worlds (#5136)
* feat: base content card component

* fix: tooltips + colors

* feat: fix orgs

* feat: base content tab internals rewrite

* feat: fix invalidmodal

* feat: add ContentModpackCard

* fix: extract types

* draft: layout

* feat: unlink modal

* feat: impl content tab

* fix: lint

* fix: toggling

* temp: disable updating stuff

* feat: selection v-model

* feat: bulk selection

* feat: mods tab rough draft

* feat: use fuse.js

* feat: add project combobox

* clean up project combobox

* feat: start install to play modal

* fix: events

* feat: use v-on

* feat: bulk actions + fix floating action bar width

* feat: figma alignments

* feat: migrate toggle to tailwind

* fix: row borders

* feat: disabled state

* feat: virtual list impl for card table based on window scroll

* fix: lint

* feat: virtualization + smaller contentcard items

* feat: use ContentCardTable + ContentCardItems

* feat: fix gap + border issues on last elm

* feat: cleanup + use proper searching

* fix: use TeleportOverflowMenu

* fix: fallback to svg if src is invalid on avatar component

* fix: storybook

* feat: start on updater modal

* feat: finish content updater modal

* feat: i18n pass

* feat: impl modal

* feat(app): backend changes for content tab refactor (#5237)

* feat: include_changelog=false for updater modal

* fix: hash overrides

* feat: update checking for modpack

* feat: qa

* feat: modpack content modal

* fix: padding in table to match modals + tightness

* fix: lint

* feat: delete modal

* feat: fix toggle bugs

* fix: prepr

* fix: duplicate messages

* qa: full width search

* qa: use bg-surface-1.5

* qa: animation for filter pills

* qa: standardize hover colors

* fix: border-[1px] is border

* qa: mass de-select actually mass selecting

* qa: match figma designs for floating action bar

* qa: modal fixes

* q: modal fixes x2

* fix: table border

* qa: confirm modals

* qa: modal alignment

* qa: re-add stuck heading + dedupe logic

* qa: dedupe virtual scrolling + remove dead components

* qa: responsiveness for content table + link fixes

* qa: version column link, tooltips + lint fixes

* qa: instance busy protections

* fix: installation freeze bug

* chore: remove old mods page

* refactor: deduplicate layout

* chore: delete old content page(s)

* qa

* qa

* qa

* feat: sort btn - to iterate

* fix: ml

* feat: date added

* fix: lint

* fix: formatting.ts removal

* feat: get_dependencies_as_content_items

* qa: final QA changes

* refactor: deduplicate + polish content.rs

* feat: hook up content.vue with v1

* feat: hide v1 content api behind frontend feature flag

* fix: query keys + copy on empty state

* chore: i18n pass

* feat: reimpl unlink + upload endpoint

* feat: use bulk endpoints v1

* fix: lint

* fix: flags

* fix: responsiveness via container queries

* fix: lint

* qa: 1

* qa: fixes

* qa: fix ssr issues with browse content

* qa: header page divider

* qa: modals

* fix: prepr

* fix: issues

* fix: lint

* fix: toggle v1 ff

* qa: 5

* qa: delete modal copy

* feat: creation flow modals (#5383)

* refactor: delete content v0 usages + impl

* feat: qa + fixes

* feat: installing banner using state event

* feat: fix modpack card bugs + filtering issues

* refactor: delete backups v0 api module

* feat: v1 servers GET endpoint

* fix: backups

* feat: swap to kyros upload v1 addon

* fix: use tanstack for loader.vue

* feat: finish install from discovery modal

* qa: bug fixes

* feat: set up installation settings

* fix: lint

* fix: typos

* fix: bugs

* fix: disable inline content

* feat: content tab improvements — upload UX, installation settings, and client-only indicators

   Upload cancellation and navigation guard:
   - Add ConfirmLeaveModal that prompts when navigating away during upload
   - Cancel in-flight XHR uploads when user confirms leaving the page
   - Add beforeunload handler to warn on browser/tab close during upload
   - Track uploadedBytes/totalBytes in UploadState for progress display
   - Replace Collapsible with Transition for upload progress admonition
   - Show byte progress and percentage in upload banner
   - Clamp upload progress to prevent exceeding 100%

   Installation settings (server.properties):
   - Add KnownPropertiesFields and PropertiesFields types to Archon types
   - Add buildProperties() to creation flow context to collect gamemode,
     difficulty, seed, world type, structures, and generator settings
   - Pass properties through installContent on onboarding, discovery, and
     ServerSetupModal flows

   Server setup and discovery flow improvements:
   - Migrate ServerSetupModal from servers_v0.reinstall to content_v1.installContent
   - Replace loaderApiNames lookup with toApiLoader() helper
   - Remove eraseDataOnInstall toggle — always use soft_override: false
   - Simplify modpack install on discovery page to use first available version
     and route through creation flow modal for both onboarding and non-onboarding
   - Differentiate post-install navigation: content page for onboarding,
     loader options for existing servers

   Modpack update flow:
   - Replace updateModpack() call with installContent() using soft_override: true
     to support version selection in the content updater modal

   Client-only mod indicators:
   - Add environment field to AddonVersion (reuses Labrinth.Projects.v3.Environment)
   - Add environment to ContentItem and isClientOnly to ContentCardTableItem
   - Show orange TriangleAlertIcon with tooltip on client-only mods in content table
   - Add "Client-only" filter pill to content filtering (controlled via
     showClientOnlyFilter on ContentManagerContext)
   - Apply client-only indicators in both ContentPageLayout and ModpackContentModal

   Misc:
   - Add CLAUDE.md note about using prepr commands for lint checks
   - Export ConfirmLeaveModal from instances barrel

* fix: piping

* fix: switch content disable for linked server instances

* feat: client only filter

* fix: prepr

* feat: hasUpdate shape update

* feat: bulk update endpoint impl for content in panel

* feat: websocket state impl again with new phases

* fix: ws

* fix: use timeout fn for sync admon + fix content card layout scroll for browsers with overflow anchor bug

* fix: qa bugs

* fix: lint, a11y and i18n

* refactor: set up layouts folder properly

* fix: linked data cache stuff + lint

* feat: move installationsettings to shared layout

* fix: lint

* fix: issues

* feat: temp fuck staging up

* fix: lockfile

* fix: data sync issues on loader.vue

* fix: lint

* Hide shader configuration files from content list (#5499)

* feat: workaround search problem + split out reset

* fix: qa

* fix: changelog not showing on first open

* fix: qa + optimistic updating improvements

* fix: prepr+lint

* fix: qa

* feat: qa

* fix: lint

* fix: lint

* fix: build

* fix: build

* fix: type errors

* fix: fade and JAVA_HOME passthrough

* feat: qa

* feat: impl diff shit

* fix: qa

* fix: app qa

* feat: update diff modal

* fix: endpoint

* fix: qa

* fix: qa

* fix: use bulk in modpack modal

* feat: abort signal impl + fix issues

* fix: diff modal trunc

* feat: qa

* fix: qa

* feat: tooltip content tab

* fix: prepr

* fix: dismiss on settings btn

* feat: qa

* feat: dont clear handlers on disconnect

* fix: lint

* fix: wrangler + introduce staging-archon env file

---------

Signed-off-by: Calum H. <calum@modrinth.com>
Co-authored-by: tdgao <mr.trumgao@gmail.com>
Co-authored-by: Artyom Ezri <61311568+Artezon@users.noreply.github.com>
2026-03-12 13:24:32 -07:00

317 lines
9.7 KiB
Vue

<template>
<div>
<div class="flex flex-col gap-3">
<div class="flex items-start justify-between gap-4">
<div class="flex flex-col gap-1">
<div class="text-xl font-semibold text-contrast">Server compatibility</div>
<div v-if="!content" class="text-sm text-secondary">
Select whether your server is vanilla or modded and which versions it supports. The
Modrinth App uses this when a player joins.
</div>
<div v-else>
<div v-if="content.kind === 'vanilla'" class="flex items-center gap-1.5">
<div
class="flex h-6 w-6 shrink-0 items-center justify-center rounded-md border border-solid border-surface-5 bg-surface-4 font-medium"
>
<BoxIcon class="h-4 w-4 shrink-0 text-secondary" />
</div>
Vanilla server
</div>
<div
v-else-if="content.kind === 'modpack' && !usingCustomMrpack"
class="flex items-center gap-1.5"
>
<div
class="flex h-6 w-6 shrink-0 items-center justify-center rounded-md border border-solid border-surface-5 bg-surface-4 font-medium"
>
<PackageIcon class="h-4 w-4 shrink-0 text-secondary" />
</div>
Published modpack
</div>
<div v-else class="flex items-center gap-1.5">
<div
class="flex h-6 w-6 shrink-0 items-center justify-center rounded-md border border-solid border-surface-5 bg-surface-4 font-medium"
>
<PackagePlusIcon class="h-4 w-4 shrink-0 text-secondary" />
</div>
Custom modpack
</div>
</div>
</div>
<ButtonStyled v-if="content" type="outlined">
<button
class="!border-[1px]"
:disabled="!hasPermission"
@click="handleSwitchCompatibility"
>
<ArrowLeftRightIcon />
Switch type
</button>
</ButtonStyled>
<ButtonStyled v-else>
<button :disabled="!hasPermission" @click="handleSetCompatibility">
<ComponentIcon />
Set compatibility
</button>
</ButtonStyled>
</div>
<div
v-if="content"
class="flex justify-between gap-4 rounded-2xl border border-solid border-surface-5 p-4"
>
<!-- kind = vanilla -->
<div
v-if="content?.kind === 'vanilla'"
class="flex flex-col items-start justify-between gap-2.5"
>
<div class="flex flex-col gap-2">
<div class="font-medium text-secondary">Recommended version</div>
<div class="text-2xl font-semibold text-contrast">
{{ content.recommended_game_version ?? '—' }}
</div>
</div>
<div class="flex flex-col gap-2">
<div class="font-medium text-secondary">Supported versions</div>
<div class="flex flex-wrap gap-1.5">
<TagItem
v-for="v in formatVersionsForDisplay(
content.supported_game_versions,
tags.gameVersions,
)"
:key="v"
>
{{ v }}
</TagItem>
<div v-if="!content.supported_game_versions.length">—</div>
</div>
</div>
</div>
<!-- kind = modpack -->
<div
v-if="content?.kind === 'modpack' && modpackProject"
class="flex w-full max-w-[500px] flex-col items-start justify-between gap-4"
>
<div class="flex w-full flex-col gap-2">
<div class="font-medium text-secondary">Required modpack</div>
<div class="w-fullitems-center flex gap-3 rounded-2xl bg-surface-1 p-3">
<Avatar
v-if="!usingCustomMrpack"
:src="modpackProject.icon_url"
size="56px"
:tint-by="modpackProject.name"
/>
<div
v-else
class="flex h-14 w-14 shrink-0 items-center justify-center rounded-xl border border-solid border-surface-5 bg-surface-3"
>
<PackagePlusIcon class="h-10 w-10 shrink-0 text-secondary" />
</div>
<div class="flex flex-col justify-center gap-1">
<div class="font-semibold text-contrast">
<NuxtLink
v-if="!usingCustomMrpack"
:to="`/modpack/${modpackProject.slug}`"
class="hover:underline"
>
{{ modpackProject.name }}
</NuxtLink>
<span v-else>{{ modpackFileName }}</span>
</div>
<div class="flex h-6 items-center gap-1.5 text-secondary">
<NuxtLink
v-if="modpackOrg?.name"
:to="`/organization/${modpackOrg.slug}`"
class="flex items-center gap-1 hover:underline"
>
<Avatar
v-if="modpackOrg?.icon_url"
:src="modpackOrg.icon_url"
size="24px"
class="rounded-2xl"
/>
{{ modpackOrg.name }}
</NuxtLink>
<div
v-if="modpackOrg?.name && modpackVersion"
class="h-1.5 w-1.5 rounded-full bg-surface-5"
></div>
<NuxtLink
v-if="modpackVersion && !usingCustomMrpack"
:to="`/modpack/${modpackProject?.slug}/version/${modpackVersion.id}`"
class="hover:underline"
>
v{{ modpackVersion.version_number }}
</NuxtLink>
<div v-else-if="modpackVersion" class="flex items-center gap-1.5">
<div>v{{ modpackVersion.version_number }}</div>
<div class="h-1.5 w-1.5 rounded-full bg-surface-5"></div>
<a
v-if="modpackVersion.files?.length"
:href="
modpackVersion.files.find((f) => f.primary)?.url ??
modpackVersion.files[0]?.url
"
class="flex items-center gap-0.5 hover:underline"
>
<DownloadIcon />
Download
</a>
</div>
</div>
</div>
</div>
</div>
<div v-if="modpackVersion" class="flex flex-col gap-2">
<div class="font-medium text-secondary">Required version</div>
<div class="flex flex-wrap gap-1.5">
<TagItem v-for="gv in modpackVersion.game_versions" :key="gv">
{{ gv }}
</TagItem>
<TagItem
v-for="loader in modpackVersion.mrpack_loaders"
:key="loader"
:style="`--_color: var(--color-platform-${loader})`"
>
<component
:is="getLoaderIcon(loader)"
v-if="getLoaderIcon(loader)"
class="h-4 w-4"
/>
<FormattedTag :tag="loader" enforce-type="loader" />
</TagItem>
</div>
</div>
</div>
<ButtonStyled v-if="content">
<button
class="!w-full !max-w-[160px]"
:disabled="!hasPermission"
@click="handleUpdateContent"
>
<RefreshCwIcon />
Update
</button>
</ButtonStyled>
</div>
</div>
<ServerCompatibilityModal v-if="hasPermission" ref="serverCompatibilityModal" />
</div>
</template>
<script setup lang="ts">
import {
ArrowLeftRightIcon,
BoxIcon,
ComponentIcon,
DownloadIcon,
getLoaderIcon,
PackageIcon,
PackagePlusIcon,
RefreshCwIcon,
} from '@modrinth/assets'
import {
Avatar,
ButtonStyled,
FormattedTag,
injectModrinthClient,
injectProjectPageContext,
TagItem,
} from '@modrinth/ui'
import { formatVersionsForDisplay } from '@modrinth/utils'
import { useQuery } from '@tanstack/vue-query'
import { useGeneratedState } from '~/composables/generated'
import ServerCompatibilityModal from './ServerCompatibilityModal/ServerCompatibilityModal.vue'
const serverCompatibilityModal = useTemplateRef<InstanceType<typeof ServerCompatibilityModal>>(
'serverCompatibilityModal',
)
const { projectV3, currentMember } = injectProjectPageContext()
const { labrinth } = injectModrinthClient()
const tags = useGeneratedState()
const hasPermission = computed(() => {
const EDIT_DETAILS = 1 << 2
return ((currentMember.value?.permissions ?? 0) & EDIT_DETAILS) === EDIT_DETAILS
})
const content = computed(() => {
if (!projectV3.value) return null
const content = projectV3.value.minecraft_java_server?.content
if (!content) return null
if (content?.kind === 'vanilla' && !content.recommended_game_version) {
return null
}
return content
})
const modpackVersionId = computed(() => {
if (content.value?.kind === 'modpack') return content.value.version_id
return null
})
const { data: modpackVersion } = useQuery({
queryKey: computed(() => ['versions', 'detail', modpackVersionId.value]),
queryFn: () => labrinth.versions_v3.getVersion(modpackVersionId.value!),
enabled: computed(() => !!modpackVersionId.value),
})
const modpackProjectId = computed(() => modpackVersion.value?.project_id ?? null)
const { data: modpackProject } = useQuery({
queryKey: computed(() => ['project', 'v3', modpackProjectId.value]),
queryFn: () => labrinth.projects_v3.get(modpackProjectId.value!),
enabled: computed(() => !!modpackProjectId.value),
})
const { data: modpackOrg } = useQuery({
queryKey: computed(() => ['project', 'org', modpackProjectId.value]),
queryFn: () => labrinth.projects_v3.getOrganization(modpackProjectId.value!),
enabled: computed(() => !!modpackProjectId.value && !!modpackProject.value?.organization),
})
const usingCustomMrpack = computed(() => modpackVersion.value?.project_id === projectV3.value?.id)
const modpackFileName = computed(() => {
if (!modpackVersion.value?.files?.length) return null
const primary = modpackVersion.value.files.find((f) => f.primary)
return (primary ?? modpackVersion.value.files[0]).filename
})
function handleSetCompatibility() {
serverCompatibilityModal.value?.show()
}
function handleSwitchCompatibility() {
serverCompatibilityModal.value?.show({ isSwitchingCompatibilityType: true })
}
function handleUpdateContent() {
if (!content.value?.kind) return
switch (content.value.kind) {
case 'vanilla':
serverCompatibilityModal.value?.show({ updateContentKind: 'vanilla' })
break
case 'modpack':
if (usingCustomMrpack.value) {
serverCompatibilityModal.value?.show({ updateContentKind: 'custom-modpack' })
} else {
serverCompatibilityModal.value?.show({ updateContentKind: 'published-modpack' })
}
break
default:
break
}
}
</script>