You've already forked AstralRinth
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:
@@ -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' })
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
? {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'. */
|
||||
|
||||
@@ -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 = ''
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user