fix: versions v2 fixes (#5106)

* update dependencies step to show when cannot detect suggested dependencies

* rollback environment to previous copy

* implement disable close when uploading in modal

* pnpm prepr
This commit is contained in:
Truman Gao
2026-01-12 17:12:10 -07:00
committed by GitHub
parent 8d72a42be5
commit 40f5db64d8
9 changed files with 162 additions and 65 deletions

View File

@@ -9,6 +9,7 @@
:items="['release', 'beta', 'alpha']"
:never-empty="true"
:capitalize="true"
:disabled="isUploading"
/>
</div>
<div class="flex flex-col gap-2">
@@ -18,6 +19,7 @@
<input
id="version-number"
v-model="draftVersion.version_number"
:disabled="isUploading"
placeholder="Enter version number, e.g. 1.2.3-alpha.1"
type="text"
autocomplete="off"
@@ -34,6 +36,7 @@
type="text"
autocomplete="off"
maxlength="256"
:disabled="isUploading"
/>
</div>
<div class="flex flex-col gap-2">
@@ -44,6 +47,7 @@
v-model="draftVersion.changelog"
:on-image-upload="onImageUpload"
:min-height="150"
:disabled="isUploading"
/>
</div>
</div>
@@ -56,7 +60,7 @@ import { Chips, MarkdownEditor } from '@modrinth/ui'
import { useImageUpload } from '~/composables/image-upload.ts'
import { injectManageVersionContext } from '~/providers/version/manage-version-modal'
const { draftVersion } = injectManageVersionContext()
const { draftVersion, isUploading } = injectManageVersionContext()
async function onImageUpload(file: File) {
const response = await useImageUpload(file, { context: 'version' })

View File

@@ -78,7 +78,7 @@ export interface ManageVersionContextValue {
dependencyVersions: Ref<Record<string, Labrinth.Versions.v3.Version>>
projectsFetchLoading: Ref<boolean>
handlingNewFiles: Ref<boolean>
suggestedDependencies: Ref<SuggestedDependency[]>
suggestedDependencies: Ref<SuggestedDependency[] | null>
visibleSuggestedDependencies: ComputedRef<SuggestedDependency[]>
primaryFile: ComputedRef<PrimaryFile | null>
@@ -177,7 +177,7 @@ export function createManageVersionContext(
const dependencyProjects = ref<Record<string, Labrinth.Projects.v3.Project>>({})
const dependencyVersions = ref<Record<string, Labrinth.Versions.v3.Version>>({})
const projectsFetchLoading = ref(false)
const suggestedDependencies = ref<SuggestedDependency[]>([])
const suggestedDependencies = ref<SuggestedDependency[] | null>(null)
const isSubmitting = ref(false)
const isUploading = ref(false)
@@ -238,7 +238,7 @@ export function createManageVersionContext(
return existing.version_id === dep.version_id
})
return suggestedDependencies.value
return (suggestedDependencies.value ?? [])
.filter((dep) => !isDuplicateSuggestion(dep))
.filter((dep) => !isAlreadyAdded(dep))
.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''))
@@ -559,20 +559,20 @@ export function createManageVersionContext(
async (loaders) => {
if (noDependenciesProject.value) return
try {
suggestedDependencies.value = []
if (!loaders?.length) return
const projectId = draftVersion.value.project_id
if (!projectId) return
try {
const versions = await labrinth.versions_v3.getProjectVersions(projectId, {
let versions = await labrinth.versions_v3.getProjectVersions(projectId, {
loaders,
})
if (!versions || versions.length === 0) {
versions = await labrinth.versions_v3.getProjectVersions(projectId)
}
// Get the most recent matching version and extract its dependencies
if (versions.length > 0) {
suggestedDependencies.value = []
const mostRecentVersion = versions[0]
for (const dep of mostRecentVersion.dependencies) {
suggestedDependencies.value.push({
@@ -582,12 +582,14 @@ export function createManageVersionContext(
file_name: dep.file_name,
})
}
} else {
suggestedDependencies.value = null
}
} catch (error: any) {
console.error(`Failed to get versions for project ${projectId}:`, error)
}
for (const dep of suggestedDependencies.value) {
for (const dep of suggestedDependencies.value ?? []) {
try {
if (dep.project_id) {
const proj = await getProject(dep.project_id)

View File

@@ -10,7 +10,7 @@ export const stageConfig: StageConfigInput<ManageVersionContextValue> = {
id: 'add-dependencies',
stageContent: markRaw(DependenciesStage),
title: (ctx) => (ctx.editingVersion.value ? 'Edit dependencies' : 'Dependencies'),
skip: true,
skip: (ctx) => ctx.suggestedDependencies.value != null,
leftButtonConfig: (ctx) =>
ctx.editingVersion.value
? {

View File

@@ -11,6 +11,7 @@ export const stageConfig: StageConfigInput<ManageVersionContextValue> = {
stageContent: markRaw(DetailsStage),
title: (ctx) => (ctx.editingVersion.value ? 'Edit details' : 'Details'),
maxWidth: '744px',
disableClose: (ctx) => ctx.isUploading.value,
leftButtonConfig: (ctx) =>
ctx.editingVersion.value
? {
@@ -21,6 +22,7 @@ export const stageConfig: StageConfigInput<ManageVersionContextValue> = {
: {
label: 'Back',
icon: LeftArrowIcon,
disabled: ctx.isUploading.value,
onClick: () => ctx.modal.value?.prevStage(),
},
rightButtonConfig: (ctx) => ({
@@ -29,7 +31,7 @@ export const stageConfig: StageConfigInput<ManageVersionContextValue> = {
: ctx.isUploading.value
? ctx.uploadProgress.value.progress >= 1
? 'Creating version'
: `Uploading version ${Math.round(ctx.uploadProgress.value.progress * 100)}%`
: `Uploading ${Math.round(ctx.uploadProgress.value.progress * 100)}%`
: 'Create version',
icon: ctx.isSubmitting.value ? SpinnerIcon : ctx.editingVersion.value ? SaveIcon : PlusIcon,
iconPosition: 'before',

View File

@@ -7,6 +7,7 @@
:closable="true"
:close-on-click-outside="false"
:width="resolvedMaxWidth"
:disable-close="resolveCtxFn(currentStage.disableClose, context)"
>
<template #title>
<div
@@ -126,6 +127,7 @@ export interface StageConfigInput<T> {
hideStageInBreadcrumb?: MaybeCtxFn<T, boolean>
nonProgressStage?: MaybeCtxFn<T, boolean>
cannotNavigateForward?: MaybeCtxFn<T, boolean>
disableClose?: MaybeCtxFn<T, boolean>
leftButtonConfig: MaybeCtxFn<T, StageButtonConfig | null>
rightButtonConfig: MaybeCtxFn<T, StageButtonConfig | null>
/** Max width for the modal content and header defined in px (e.g., '460px', '600px'). Defaults to '460px'. */

View File

@@ -42,7 +42,7 @@
</slot>
</div>
<ButtonStyled v-if="closable" circular>
<button v-tooltip="'Close'" aria-label="Close" @click="hide">
<button v-tooltip="'Close'" aria-label="Close" :disabled="disableClose" @click="hide">
<XIcon aria-hidden="true" />
</button>
</ButtonStyled>
@@ -53,7 +53,7 @@
class="absolute top-4 right-4 z-10"
circular
>
<button v-tooltip="'Close'" aria-label="Close" @click="hide">
<button v-tooltip="'Close'" aria-label="Close" :disabled="disableClose" @click="hide">
<XIcon aria-hidden="true" />
</button>
</ButtonStyled>
@@ -141,6 +141,8 @@ const props = withDefaults(
maxWidth?: string
/** Width for the modal body (e.g., '460px', '600px'). */
width?: string
/** Disables all close actions (close button, ESC key, click outside). */
disableClose?: boolean
}>(),
{
type: true,
@@ -160,6 +162,7 @@ const props = withDefaults(
maxContentHeight: '70vh',
maxWidth: undefined,
width: undefined,
disableClose: false,
},
)
@@ -194,6 +197,7 @@ function show(event?: MouseEvent) {
}
function hide() {
if (props.disableClose) return
props.onHide?.()
visible.value = false
document.body.style.overflow = ''

View File

@@ -29,9 +29,9 @@
<section v-if="showEnvironments" class="flex flex-col gap-2">
<h3 class="text-primary text-base m-0">{{ formatMessage(messages.environments) }}</h3>
<div class="flex flex-wrap gap-1">
<TagItem v-for="(tag, tagIdx) in primaryEnvironmentTags" :key="`environment-tag-${tagIdx}`">
<TagItem v-for="tag in primaryEnvironmentTags" :key="`environment-tag-${tag.message.id}`">
<component :is="tag.icon" />
{{ formatMessage(tag.label) }}
{{ formatMessage(tag.message) }}
</TagItem>
</div>
</section>
@@ -88,12 +88,16 @@
import { ClientIcon, MonitorSmartphoneIcon, ServerIcon, UserIcon } from '@modrinth/assets'
import type { EnvironmentV3, GameVersionTag, PlatformTag, ProjectV3Partial } from '@modrinth/utils'
import { formatCategory, getVersionsToDisplay } from '@modrinth/utils'
import { computed } from 'vue'
import { type Component, computed } from 'vue'
import { useRouter } from 'vue-router'
import { defineMessages, useVIntl } from '../../composables/i18n'
import {
defineMessage,
defineMessages,
type MessageDescriptor,
useVIntl,
} from '../../composables/i18n'
import TagItem from '../base/TagItem.vue'
import { getEnvironmentTags } from './settings/environment/environments'
const { formatMessage } = useVIntl()
const router = useRouter()
@@ -129,8 +133,82 @@ const primaryEnvironment = computed<EnvironmentV3 | undefined>(() =>
props.v3Metadata?.environment?.find((x) => x !== 'unknown'),
)
type EnvironmentTag = {
icon: Component
message: MessageDescriptor
environments: EnvironmentV3[]
}
const environmentTags: EnvironmentTag[] = [
{
icon: ClientIcon,
message: defineMessage({
id: `project.about.compatibility.environments.client-side`,
defaultMessage: 'Client-side',
}),
environments: [
'client_only',
'client_only_server_optional',
'client_or_server',
'client_or_server_prefers_both',
],
},
{
icon: ServerIcon,
message: defineMessage({
id: `project.about.compatibility.environments.server-side`,
defaultMessage: 'Server-side',
}),
environments: [
'server_only',
'server_only_client_optional',
'client_or_server',
'client_or_server_prefers_both',
],
},
{
icon: ServerIcon,
message: defineMessage({
id: `project.about.compatibility.environments.dedicated-servers-only`,
defaultMessage: 'Dedicated servers only',
}),
environments: ['dedicated_server_only'],
},
{
icon: UserIcon,
message: defineMessage({
id: `project.about.compatibility.environments.singleplayer-only`,
defaultMessage: 'Singleplayer only',
}),
environments: ['singleplayer_only'],
},
{
icon: UserIcon,
message: defineMessage({
id: `project.about.compatibility.environments.singleplayer`,
defaultMessage: 'Singleplayer',
}),
environments: ['server_only'],
},
{
icon: MonitorSmartphoneIcon,
message: defineMessage({
id: `project.about.compatibility.environments.client-and-server`,
defaultMessage: 'Client and server',
}),
environments: [
'client_and_server',
'client_only_server_optional',
'server_only_client_optional',
'client_or_server_prefers_both',
],
},
]
const primaryEnvironmentTags = computed(() => {
return getEnvironmentTags(primaryEnvironment.value)
return primaryEnvironment.value
? environmentTags.filter((x) => x.environments.includes(primaryEnvironment.value ?? 'unknown'))
: []
})
const messages = defineMessages({

View File

@@ -123,25 +123,29 @@ export const ENVIRONMENTS_COPY: Record<
}
export const ENVIRONMENT_TAG_LABELS = {
client: defineMessage({
id: 'project.environment.tag.client',
defaultMessage: 'Client',
clientSide: defineMessage({
id: 'project.about.compatibility.environments.client-side',
defaultMessage: 'Client-side',
}),
server: defineMessage({
id: 'project.environment.tag.server',
defaultMessage: 'Server',
serverSide: defineMessage({
id: 'project.about.compatibility.environments.server-side',
defaultMessage: 'Server-side',
}),
dedicatedServersOnly: defineMessage({
id: 'project.about.compatibility.environments.dedicated-servers-only',
defaultMessage: 'Dedicated servers only',
}),
singleplayerOnly: defineMessage({
id: 'project.about.compatibility.environments.singleplayer-only',
defaultMessage: 'Singleplayer only',
}),
singleplayer: defineMessage({
id: 'project.environment.tag.singleplayer',
id: 'project.about.compatibility.environments.singleplayer',
defaultMessage: 'Singleplayer',
}),
clientOptional: defineMessage({
id: 'project.environment.tag.client-optional',
defaultMessage: 'Client optional',
}),
serverOptional: defineMessage({
id: 'project.environment.tag.server-optional',
defaultMessage: 'Server optional',
clientAndServer: defineMessage({
id: 'project.about.compatibility.environments.client-and-server',
defaultMessage: 'Client and server',
}),
unknown: defineMessage({
id: 'project.environment.tag.unknown',
@@ -158,48 +162,46 @@ export function getEnvironmentTags(
): Array<{ icon: Component | null; label: MessageDescriptor }> {
switch (environment) {
case 'client_only':
return [{ icon: ClientIcon, label: ENVIRONMENT_TAG_LABELS.client }]
return [{ icon: ClientIcon, label: ENVIRONMENT_TAG_LABELS.clientSide }]
case 'server_only':
return [
{ icon: ServerIcon, label: ENVIRONMENT_TAG_LABELS.server },
{ icon: ServerIcon, label: ENVIRONMENT_TAG_LABELS.serverSide },
{ icon: UserIcon, label: ENVIRONMENT_TAG_LABELS.singleplayer },
]
case 'singleplayer_only':
return [{ icon: UserIcon, label: ENVIRONMENT_TAG_LABELS.singleplayer }]
return [{ icon: UserIcon, label: ENVIRONMENT_TAG_LABELS.singleplayerOnly }]
case 'dedicated_server_only':
return [{ icon: ServerIcon, label: ENVIRONMENT_TAG_LABELS.server }]
return [{ icon: ServerIcon, label: ENVIRONMENT_TAG_LABELS.dedicatedServersOnly }]
case 'client_and_server':
return [
{ icon: ClientIcon, label: ENVIRONMENT_TAG_LABELS.client },
{ icon: ServerIcon, label: ENVIRONMENT_TAG_LABELS.server },
]
return [{ icon: ClientIcon, label: ENVIRONMENT_TAG_LABELS.clientAndServer }]
case 'client_only_server_optional':
return [
{ icon: ClientIcon, label: ENVIRONMENT_TAG_LABELS.client },
{ icon: ServerIcon, label: ENVIRONMENT_TAG_LABELS.serverOptional },
{ icon: ClientIcon, label: ENVIRONMENT_TAG_LABELS.clientSide },
{ icon: ClientIcon, label: ENVIRONMENT_TAG_LABELS.clientAndServer },
]
case 'server_only_client_optional':
return [
{ icon: ServerIcon, label: ENVIRONMENT_TAG_LABELS.server },
{ icon: ClientIcon, label: ENVIRONMENT_TAG_LABELS.clientOptional },
{ icon: ServerIcon, label: ENVIRONMENT_TAG_LABELS.serverSide },
{ icon: ClientIcon, label: ENVIRONMENT_TAG_LABELS.clientAndServer },
]
case 'client_or_server':
return [
{ icon: ClientIcon, label: ENVIRONMENT_TAG_LABELS.clientOptional },
{ icon: ServerIcon, label: ENVIRONMENT_TAG_LABELS.serverOptional },
{ icon: ClientIcon, label: ENVIRONMENT_TAG_LABELS.clientSide },
{ icon: ServerIcon, label: ENVIRONMENT_TAG_LABELS.serverSide },
]
case 'client_or_server_prefers_both':
return [
{ icon: ClientIcon, label: ENVIRONMENT_TAG_LABELS.clientOptional },
{ icon: ServerIcon, label: ENVIRONMENT_TAG_LABELS.serverOptional },
{ icon: ClientIcon, label: ENVIRONMENT_TAG_LABELS.clientSide },
{ icon: ServerIcon, label: ENVIRONMENT_TAG_LABELS.serverSide },
{ icon: ClientIcon, label: ENVIRONMENT_TAG_LABELS.clientAndServer },
]
case 'unknown':

View File

@@ -593,6 +593,24 @@
"project.about.compatibility.environments": {
"defaultMessage": "Supported environments"
},
"project.about.compatibility.environments.client-and-server": {
"defaultMessage": "Client and server"
},
"project.about.compatibility.environments.client-side": {
"defaultMessage": "Client-side"
},
"project.about.compatibility.environments.dedicated-servers-only": {
"defaultMessage": "Dedicated servers only"
},
"project.about.compatibility.environments.server-side": {
"defaultMessage": "Server-side"
},
"project.about.compatibility.environments.singleplayer": {
"defaultMessage": "Singleplayer"
},
"project.about.compatibility.environments.singleplayer-only": {
"defaultMessage": "Singleplayer only"
},
"project.about.compatibility.game.minecraftJava": {
"defaultMessage": "Minecraft: Java Edition"
},
@@ -713,24 +731,9 @@
"project.environment.singleplayer-only.title": {
"defaultMessage": "Singleplayer only"
},
"project.environment.tag.client": {
"defaultMessage": "Client"
},
"project.environment.tag.client-optional": {
"defaultMessage": "Client optional"
},
"project.environment.tag.not-applicable": {
"defaultMessage": "N/A"
},
"project.environment.tag.server": {
"defaultMessage": "Server"
},
"project.environment.tag.server-optional": {
"defaultMessage": "Server optional"
},
"project.environment.tag.singleplayer": {
"defaultMessage": "Singleplayer"
},
"project.environment.tag.unknown": {
"defaultMessage": "Unknown"
},