diff --git a/apps/frontend/src/components/ui/create-project-version/stages/DetailsStage.vue b/apps/frontend/src/components/ui/create-project-version/stages/DetailsStage.vue
index 6aa337c5..60cf4e0b 100644
--- a/apps/frontend/src/components/ui/create-project-version/stages/DetailsStage.vue
+++ b/apps/frontend/src/components/ui/create-project-version/stages/DetailsStage.vue
@@ -9,6 +9,7 @@
:items="['release', 'beta', 'alpha']"
:never-empty="true"
:capitalize="true"
+ :disabled="isUploading"
/>
@@ -18,6 +19,7 @@
@@ -44,6 +47,7 @@
v-model="draftVersion.changelog"
:on-image-upload="onImageUpload"
:min-height="150"
+ :disabled="isUploading"
/>
@@ -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' })
diff --git a/apps/frontend/src/providers/version/manage-version-modal.ts b/apps/frontend/src/providers/version/manage-version-modal.ts
index 1b448065..cf162ebc 100644
--- a/apps/frontend/src/providers/version/manage-version-modal.ts
+++ b/apps/frontend/src/providers/version/manage-version-modal.ts
@@ -78,7 +78,7 @@ export interface ManageVersionContextValue {
dependencyVersions: Ref>
projectsFetchLoading: Ref
handlingNewFiles: Ref
- suggestedDependencies: Ref
+ suggestedDependencies: Ref
visibleSuggestedDependencies: ComputedRef
primaryFile: ComputedRef
@@ -177,7 +177,7 @@ export function createManageVersionContext(
const dependencyProjects = ref>({})
const dependencyVersions = ref>({})
const projectsFetchLoading = ref(false)
- const suggestedDependencies = ref([])
+ const suggestedDependencies = ref(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)
diff --git a/apps/frontend/src/providers/version/stages/dependencies-stage.ts b/apps/frontend/src/providers/version/stages/dependencies-stage.ts
index 0f003431..de31a3e8 100644
--- a/apps/frontend/src/providers/version/stages/dependencies-stage.ts
+++ b/apps/frontend/src/providers/version/stages/dependencies-stage.ts
@@ -10,7 +10,7 @@ export const stageConfig: StageConfigInput = {
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
? {
diff --git a/apps/frontend/src/providers/version/stages/details-stage.ts b/apps/frontend/src/providers/version/stages/details-stage.ts
index ef80de6f..04910965 100644
--- a/apps/frontend/src/providers/version/stages/details-stage.ts
+++ b/apps/frontend/src/providers/version/stages/details-stage.ts
@@ -11,6 +11,7 @@ export const stageConfig: StageConfigInput = {
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 = {
: {
label: 'Back',
icon: LeftArrowIcon,
+ disabled: ctx.isUploading.value,
onClick: () => ctx.modal.value?.prevStage(),
},
rightButtonConfig: (ctx) => ({
@@ -29,7 +31,7 @@ export const stageConfig: StageConfigInput = {
: 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',
diff --git a/packages/ui/src/components/base/MultiStageModal.vue b/packages/ui/src/components/base/MultiStageModal.vue
index d5ee0dd0..296d1b75 100644
--- a/packages/ui/src/components/base/MultiStageModal.vue
+++ b/packages/ui/src/components/base/MultiStageModal.vue
@@ -7,6 +7,7 @@
:closable="true"
:close-on-click-outside="false"
:width="resolvedMaxWidth"
+ :disable-close="resolveCtxFn(currentStage.disableClose, context)"
>
{
hideStageInBreadcrumb?: MaybeCtxFn
nonProgressStage?: MaybeCtxFn
cannotNavigateForward?: MaybeCtxFn
+ disableClose?: MaybeCtxFn
leftButtonConfig: MaybeCtxFn
rightButtonConfig: MaybeCtxFn
/** Max width for the modal content and header defined in px (e.g., '460px', '600px'). Defaults to '460px'. */
diff --git a/packages/ui/src/components/modal/NewModal.vue b/packages/ui/src/components/modal/NewModal.vue
index 1243800a..aaaa274d 100644
--- a/packages/ui/src/components/modal/NewModal.vue
+++ b/packages/ui/src/components/modal/NewModal.vue
@@ -42,7 +42,7 @@
-
@@ -53,7 +53,7 @@
class="absolute top-4 right-4 z-10"
circular
>
-
+
@@ -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 = ''
diff --git a/packages/ui/src/components/project/ProjectSidebarCompatibility.vue b/packages/ui/src/components/project/ProjectSidebarCompatibility.vue
index 658e377a..a213cca6 100644
--- a/packages/ui/src/components/project/ProjectSidebarCompatibility.vue
+++ b/packages/ui/src/components/project/ProjectSidebarCompatibility.vue
@@ -29,9 +29,9 @@
{{ formatMessage(messages.environments) }}
-
+
- {{ formatMessage(tag.label) }}
+ {{ formatMessage(tag.message) }}
@@ -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(() =>
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({
diff --git a/packages/ui/src/components/project/settings/environment/environments.ts b/packages/ui/src/components/project/settings/environment/environments.ts
index f63ea4dd..4c34ebca 100644
--- a/packages/ui/src/components/project/settings/environment/environments.ts
+++ b/packages/ui/src/components/project/settings/environment/environments.ts
@@ -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':
diff --git a/packages/ui/src/locales/en-US/index.json b/packages/ui/src/locales/en-US/index.json
index 9346e240..a1d02758 100644
--- a/packages/ui/src/locales/en-US/index.json
+++ b/packages/ui/src/locales/en-US/index.json
@@ -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"
},