feat: improve add dependency flow (#6075)

* fix: shadow on nav

* feat: improve add dependency flow

* feat: update suggested dependency style

* feat: update dependency rows to use version number and update styles

* feat: implement combobox select searched text on focus

* feat: add Tabs.vue

* feat: update nav tabs to use tabs

* feat: improve project search dropdown

* fix: dependency search not clearing inbound query

* fix: combobox no options open state bug

* feat: improve dependency project and version search
This commit is contained in:
Truman Gao
2026-05-12 02:46:23 +00:00
committed by GitHub
parent 612934bf34
commit e0056bfc40
18 changed files with 569 additions and 374 deletions
@@ -1,6 +1,7 @@
import type { Labrinth, UploadProgress } from '@modrinth/api-client'
import { SaveIcon, SpinnerIcon } from '@modrinth/assets'
import {
type ComboboxOption,
createContext,
injectModrinthClient,
injectNotificationManager,
@@ -49,16 +50,16 @@ export type VersionStage =
| 'add-loaders'
| 'add-mc-versions'
| 'add-environment'
| 'add-dependencies'
| 'add-changelog'
| 'from-details-loaders'
| 'from-details-mc-versions'
| 'from-details-environment'
| 'from-details-dependencies'
export type SuggestedDependency = Labrinth.Versions.v3.Dependency & {
name?: string
icon?: string
versionName?: string
versionNumber?: string
}
export interface PrimaryFile {
@@ -79,6 +80,10 @@ export interface ManageVersionContextValue {
projectsFetchLoading: Ref<boolean>
handlingNewFiles: Ref<boolean>
suggestedDependencies: Ref<SuggestedDependency[] | null>
newDependencyProjectId: Ref<string | undefined>
newDependencyType: Ref<Labrinth.Versions.v2.DependencyType>
newDependencyVersionId: Ref<string | null>
newDependencyVersions: Ref<ComboboxOption<string>[]>
visibleSuggestedDependencies: ComputedRef<SuggestedDependency[]>
primaryFile: ComputedRef<PrimaryFile | null>
@@ -105,6 +110,8 @@ export interface ManageVersionContextValue {
replacePrimaryFile: (file: File) => Promise<void>
getProject: (projectId: string) => Promise<Labrinth.Projects.v3.Project>
getVersion: (versionId: string) => Promise<Labrinth.Versions.v3.Version>
resetNewDependency: () => void
addNewDependency: () => boolean
// Submission methods
handleCreateVersion: () => Promise<void>
@@ -178,6 +185,10 @@ export function createManageVersionContext(
const dependencyVersions = ref<Record<string, Labrinth.Versions.v3.Version>>({})
const projectsFetchLoading = ref(false)
const suggestedDependencies = ref<SuggestedDependency[] | null>(null)
const newDependencyProjectId = ref<string>()
const newDependencyType = ref<Labrinth.Versions.v2.DependencyType>('required')
const newDependencyVersionId = ref<string | null>(null)
const newDependencyVersions = ref<ComboboxOption<string>[]>([])
const isSubmitting = ref(false)
const isUploading = ref(false)
@@ -254,6 +265,7 @@ export function createManageVersionContext(
filesToAdd.value = []
existingFilesToDelete.value = []
inferredVersionData.value = undefined
resetNewDependency()
}
async function handleNewFiles(newFiles: File[]) {
@@ -453,6 +465,44 @@ export function createManageVersionContext(
return version
}
function resetNewDependency() {
newDependencyProjectId.value = undefined
newDependencyVersionId.value = null
newDependencyType.value = 'required'
newDependencyVersions.value = []
}
function addNewDependency() {
if (!newDependencyProjectId.value) return false
if (!draftVersion.value.dependencies) draftVersion.value.dependencies = []
const dependency: Labrinth.Versions.v3.Dependency = {
project_id: newDependencyProjectId.value,
version_id: newDependencyVersionId.value || undefined,
dependency_type: newDependencyType.value,
}
const alreadyAdded = draftVersion.value.dependencies.some((existing) => {
if (existing.project_id !== dependency.project_id) return false
if (!existing.version_id && !dependency.version_id) return true
return existing.version_id === dependency.version_id
})
if (alreadyAdded) {
addNotification({
title: 'Dependency already added',
text: 'You cannot add the same dependency twice.',
type: 'error',
})
return false
}
projectsFetchLoading.value = true
draftVersion.value.dependencies.push(dependency)
resetNewDependency()
return true
}
// Primary file computed
const primaryFile = computed<PrimaryFile | null>(() => {
const existingPrimaryFile = draftVersion.value.existing_files?.[0]
@@ -537,6 +587,30 @@ export function createManageVersionContext(
{ immediate: true, deep: true },
)
watch(newDependencyProjectId, async () => {
newDependencyVersionId.value = null
newDependencyType.value = 'required'
if (!newDependencyProjectId.value) {
newDependencyVersions.value = []
return
}
try {
const versions = await labrinth.versions_v3.getProjectVersions(newDependencyProjectId.value)
newDependencyVersions.value = versions.map((version) => ({
label: version.version_number,
value: version.id,
}))
} catch (error: any) {
addNotification({
title: 'An error occurred',
text: error.data ? error.data.description : error,
type: 'error',
})
}
})
// Watch loaders to infer environment if not set
watch(
() => draftVersion.value.loaders,
@@ -603,7 +677,7 @@ export function createManageVersionContext(
if (dep.version_id) {
const version = await getVersion(dep.version_id)
dep.versionName = version.name
dep.versionNumber = version.version_number
}
} catch (error: any) {
console.error(`Failed to fetch project/version data for dependency:`, error)
@@ -766,8 +840,6 @@ export function createManageVersionContext(
return editingVersion.value ? 'Edit loaders' : 'Set loaders'
case 'add-mc-versions':
return editingVersion.value ? 'Edit game versions' : 'Set game versions'
case 'add-dependencies':
return editingVersion.value ? 'Edit dependencies' : 'Set dependencies'
case 'add-environment':
return editingVersion.value ? 'Edit environment' : 'Add environment'
case 'add-changelog':
@@ -801,6 +873,10 @@ export function createManageVersionContext(
handlingNewFiles,
projectsFetchLoading,
suggestedDependencies,
newDependencyProjectId,
newDependencyType,
newDependencyVersionId,
newDependencyVersions,
visibleSuggestedDependencies,
primaryFile,
@@ -826,6 +902,8 @@ export function createManageVersionContext(
replacePrimaryFile,
getProject,
getVersion,
resetNewDependency,
addNewDependency,
handleNewFiles,
handleCreateVersion,
handleSaveVersionEdits,
@@ -1,4 +1,4 @@
import { LeftArrowIcon, RightArrowIcon, XIcon } from '@modrinth/assets'
import { PlusIcon, XIcon } from '@modrinth/assets'
import type { StageConfigInput } from '@modrinth/ui'
import { markRaw } from 'vue'
@@ -6,54 +6,27 @@ import DependenciesStage from '~/components/ui/create-project-version/stages/Dep
import type { ManageVersionContextValue } from '../manage-version-modal'
export const stageConfig: StageConfigInput<ManageVersionContextValue> = {
id: 'add-dependencies',
stageContent: markRaw(DependenciesStage),
title: (ctx) => (ctx.editingVersion.value ? 'Edit version' : 'Dependencies'),
skip: (ctx) => ctx.suggestedDependencies.value != null || ctx.projectType.value === 'modpack',
leftButtonConfig: (ctx) =>
ctx.editingVersion.value
? {
label: 'Cancel',
icon: XIcon,
onClick: () => ctx.modal.value?.hide(),
}
: {
label: 'Back',
icon: LeftArrowIcon,
onClick: () => ctx.modal.value?.prevStage(),
},
rightButtonConfig: (ctx) =>
ctx.editingVersion.value
? ctx.saveButtonConfig()
: {
label: ctx.getNextLabel(),
icon: RightArrowIcon,
iconPosition: 'after',
onClick: () => ctx.modal.value?.nextStage(),
},
nonProgressStage: (ctx) => ctx.editingVersion.value,
}
export const fromDetailsStageConfig: StageConfigInput<ManageVersionContextValue> = {
id: 'from-details-dependencies',
stageContent: markRaw(DependenciesStage),
title: 'Edit version',
title: 'Add dependency',
nonProgressStage: true,
leftButtonConfig: (ctx) => ({
label: 'Back',
icon: LeftArrowIcon,
onClick: () => ctx.modal.value?.setStage('metadata'),
label: 'Cancel',
icon: XIcon,
onClick: () => {
ctx.resetNewDependency()
ctx.modal.value?.setStage('metadata')
},
}),
rightButtonConfig: (ctx) => ({
label: 'Add dependency',
icon: PlusIcon,
iconPosition: 'before',
color: 'green',
disabled: !ctx.newDependencyProjectId.value,
onClick: () => {
if (ctx.addNewDependency()) ctx.modal.value?.setStage('metadata')
},
}),
rightButtonConfig: (ctx) =>
ctx.editingVersion.value
? {
...ctx.saveButtonConfig(),
}
: {
label: 'Add details',
icon: RightArrowIcon,
iconPosition: 'after',
onClick: () => ctx.modal.value?.setStage('add-details'),
},
}
@@ -2,10 +2,7 @@ import {
fromDetailsStageConfig as fromDetailsFilesStageConfig,
stageConfig as addFilesStageConfig,
} from './add-files-stage'
import {
fromDetailsStageConfig as fromDetailsDependenciesStageConfig,
stageConfig as dependenciesStageConfig,
} from './dependencies-stage'
import { fromDetailsStageConfig as fromDetailsDependenciesStageConfig } from './dependencies-stage'
import { stageConfig as detailsStageConfig } from './details-stage'
import {
fromDetailsStageConfig as fromDetailsEnvironmentStageConfig,
@@ -26,11 +23,10 @@ export const stageConfigs = [
loadersStageConfig,
mcVersionsStageConfig,
environmentStageConfig,
dependenciesStageConfig,
metadataStageConfig,
detailsStageConfig,
// Non-progress stages for editing from details page
// Non-progress stages for editing from metadata/details pages
fromDetailsLoadersStageConfig,
fromDetailsMcVersionsStageConfig,
fromDetailsEnvironmentStageConfig,