feat: manage project versions v2 (#5049)

* update add files copy and go to next step on just one file

* rename and reorder stages

* add metadata stage and update details stage

* implement files inside metadata stage

* use regular prettier instead of prettier eslint

* remove changelog stage config

* save button on details stage

* update edit buttons in versions table

* add collapse environment selector

* implement dependencies list in metadata step

* move dependencies into provider

* add suggested dependencies to metadata stage

* pnpm prepr

* fix unused var

* Revert "add collapse environment selector"

This reverts commit f90fabc7a57ff201f26e1b628eeced8e6ef75865.

* hide resource pack loader only when its the only loader

* fix no dependencies for modpack

* add breadcrumbs with hide breadcrumb option

* wider stages

* add proper horizonal scroll breadcrumbs

* fix titles

* handle save version in version page

* remove box shadow

* add notification provider to storybook

* add drop area for versions to drop file right into page

* fix mobile versions table buttons overflowing

* pnpm prepr

* fix drop file opening modal in wrong stage

* implement invalid file for dropping files

* allow horizontal scroll on breadcrumbs

* update infer.js as best as possible

* add create version button uploading version state

* add extractVersionFromFilename for resource pack and datapack

* allow jars for datapack project

* detect multiple loaders when possible

* iris means compatible with optifine too

* infer environment on loader change as well

* add tooltip

* prevent navigate forward when cannot go to next step

* larger breadcrumb click targets

* hide loaders and mc versions stage until files added

* fix max width in header

* fix add files from metadata step jumping steps

* define width in NewModal instead

* disable remove dependency in metadata stage

* switch metadata and details buttons positions

* fix remove button spacing

* do not allow duplicate suggested dependencies

* fix version detection for fabric minecraft version semvar

* better verion number detection based on filename

* show resource pack loader but uneditable

* remove vanilla shader detection

* refactor: break up large infer.js into ts and modules

* remove duplicated types

* add fill missing from file name step

* pnpm prepr

* fix neoforge loader parse failing and not adding neoforge loader

* add missing pack formats

* handle new pack format

* pnpm prepr

* add another regex where it is version in anywhere in filename

* only show resource pack or data pack options for filetype on datapack project

* add redundant zip folder check

* reject RP and DP if has redundant folder

* fix hide stage in breadcrumb

* add snapshot group key in case no release version. brings out 26.1 snapshots

* pnpm prepr

* open in group if has something selected

* fix resource pack loader uneditable if accidentally selected on different project type

* add new environment tags

* add unknown and not applicable environment tags

* pnpm prepr

* use shared constant on labels

* use ref for timeout

* remove console logs

* remove box shadow only for cm-content

* feat: xhr upload + fix wrangler prettierignore

* fix: upload content type fix

* fix dependencies version width

* fix already added dependencies logic

* add changelog minheight

* set progress percentage on button

* add legacy fabric detection logic

* lint

* small update on create version button label

---------

Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
Truman Gao
2026-01-12 12:41:14 -07:00
committed by GitHub
parent b46f6d0141
commit 61c8cd75cd
64 changed files with 3185 additions and 1709 deletions

View File

@@ -1,4 +1,4 @@
import type { Labrinth } from '@modrinth/api-client'
import type { Labrinth, UploadProgress } from '@modrinth/api-client'
import { SaveIcon, SpinnerIcon } from '@modrinth/assets'
import {
createContext,
@@ -55,6 +55,18 @@ export type VersionStage =
| 'from-details-mc-versions'
| 'from-details-environment'
export type SuggestedDependency = Labrinth.Versions.v3.Dependency & {
name?: string
icon?: string
versionName?: string
}
export interface PrimaryFile {
name: string
fileType?: string
existing?: boolean
}
export interface ManageVersionContextValue {
// State
draftVersion: Ref<Labrinth.Versions.v3.DraftVersion>
@@ -64,16 +76,23 @@ export interface ManageVersionContextValue {
projectType: Ref<Labrinth.Projects.v2.ProjectType | undefined>
dependencyProjects: Ref<Record<string, Labrinth.Projects.v3.Project>>
dependencyVersions: Ref<Record<string, Labrinth.Versions.v3.Version>>
projectsFetchLoading: Ref<boolean>
handlingNewFiles: Ref<boolean>
suggestedDependencies: Ref<SuggestedDependency[]>
visibleSuggestedDependencies: ComputedRef<SuggestedDependency[]>
primaryFile: ComputedRef<PrimaryFile | null>
// Stage management
stageConfigs: StageConfigInput<ManageVersionContextValue>[]
isSubmitting: Ref<boolean>
isUploading: Ref<boolean>
uploadProgress: Ref<UploadProgress>
modal: ShallowRef<ComponentExposed<typeof MultiStageModal> | null>
// Computed state
editingVersion: ComputedRef<boolean>
noLoadersProject: ComputedRef<boolean>
noEnvironmentProject: ComputedRef<boolean>
noDependenciesProject: ComputedRef<boolean>
// Stage helpers
getNextLabel: (currentIndex?: number | null) => string
@@ -81,11 +100,9 @@ export interface ManageVersionContextValue {
// Version methods
newDraftVersion: (projectId: string, version?: Labrinth.Versions.v3.DraftVersion | null) => void
setPrimaryFile: (index: number) => void
setInferredVersionData: (
file: File,
project: Labrinth.Projects.v2.Project,
) => Promise<InferredVersionInfo>
handleNewFiles: (newFiles: File[]) => Promise<void>
swapPrimaryFile: (index: number) => void
replacePrimaryFile: (file: File) => Promise<void>
getProject: (projectId: string) => Promise<Labrinth.Projects.v3.Project>
getVersion: (versionId: string) => Promise<Labrinth.Versions.v3.Version>
@@ -129,24 +146,42 @@ const PROJECT_TYPE_LOADERS: Record<string, readonly string[]> = {
modpack: ['mrpack'],
} as const
export const fileTypeLabels: Record<Labrinth.Versions.v3.FileType | 'primary', string> = {
primary: 'Primary',
unknown: 'Other',
'required-resource-pack': 'Required RP',
'optional-resource-pack': 'Optional RP',
'sources-jar': 'Sources JAR',
'dev-jar': 'Dev JAR',
'javadoc-jar': 'Javadoc JAR',
signature: 'Signature',
}
export const [injectManageVersionContext, provideManageVersionContext] =
createContext<ManageVersionContextValue>('CreateProjectVersionModal')
export function createManageVersionContext(
modal: ShallowRef<ComponentExposed<typeof MultiStageModal> | null>,
onSave?: () => void,
): ManageVersionContextValue {
const { labrinth } = injectModrinthClient()
const { addNotification } = injectNotificationManager()
const { refreshVersions } = injectProjectPageContext()
const { refreshVersions, projectV2 } = injectProjectPageContext()
// State
const draftVersion = ref<Labrinth.Versions.v3.DraftVersion>(structuredClone(EMPTY_DRAFT_VERSION))
const filesToAdd = ref<Labrinth.Versions.v3.DraftVersionFile[]>([])
const existingFilesToDelete = ref<Labrinth.Versions.v3.VersionFileHash['sha1'][]>([])
const handlingNewFiles = ref(false)
const inferredVersionData = ref<InferredVersionInfo>()
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 isSubmitting = ref(false)
const isUploading = ref(false)
const uploadProgress = ref<UploadProgress>({ loaded: 0, total: 0, progress: 0 })
const projectType = computed<Labrinth.Projects.v2.ProjectType>(() => {
const primaryFile = filesToAdd.value[0]?.file
@@ -166,7 +201,7 @@ export function createManageVersionContext(
if (loaders.some((loader) => PROJECT_TYPE_LOADERS.datapack.includes(loader))) {
return 'datapack'
}
if (loaders.some((loader) => PROJECT_TYPE_LOADERS.resourcepack.includes(loader))) {
if (loaders.length === 1 && loaders[0] === 'minecraft') {
return 'resourcepack'
}
if (loaders.some((loader) => PROJECT_TYPE_LOADERS.shader.includes(loader))) {
@@ -185,6 +220,30 @@ export function createManageVersionContext(
// Computed state
const editingVersion = computed(() => Boolean(draftVersion.value.version_id))
const visibleSuggestedDependencies = computed<SuggestedDependency[]>(() => {
const existingDeps = draftVersion.value.dependencies ?? []
const seenKeys = new Set<string>()
const isDuplicateSuggestion = (dep: SuggestedDependency) => {
const key = `${dep.project_id ?? ''}:${dep.version_id ?? ''}`
if (seenKeys.has(key)) return true
seenKeys.add(key)
return false
}
const isAlreadyAdded = (dep: SuggestedDependency) =>
existingDeps.some((existing) => {
if (existing.project_id !== dep.project_id) return false
if (!existing.version_id && !dep.version_id) return true
return existing.version_id === dep.version_id
})
return suggestedDependencies.value
.filter((dep) => !isDuplicateSuggestion(dep))
.filter((dep) => !isAlreadyAdded(dep))
.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''))
})
// Version management methods
function newDraftVersion(
projectId: string,
@@ -195,15 +254,50 @@ export function createManageVersionContext(
filesToAdd.value = []
existingFilesToDelete.value = []
inferredVersionData.value = undefined
// projectType.value = undefined
}
function setPrimaryFile(index: number) {
async function handleNewFiles(newFiles: File[]) {
handlingNewFiles.value = true
// detect primary file if no primary file is set
const primaryFileIndex = primaryFile.value ? null : detectPrimaryFileIndex(newFiles)
newFiles.forEach((file) => filesToAdd.value.push({ file }))
if (primaryFileIndex !== null) {
if (primaryFileIndex) swapPrimaryFile(primaryFileIndex)
}
if (
filesToAdd.value.length === 1 &&
!editingVersion.value &&
modal.value?.currentStageIndex === 0
) {
if (await rejectOnRedundantWrappedZip(filesToAdd.value[0].file)) return
await addDetectedData()
modal.value?.nextStage()
}
handlingNewFiles.value = false
}
async function replacePrimaryFile(file: File) {
if (file && !editingVersion.value) {
filesToAdd.value[0] = { file }
}
if (await rejectOnRedundantWrappedZip(file)) return
await addDetectedData()
}
async function swapPrimaryFile(index: number) {
const files = filesToAdd.value
if (index <= 0 || index >= files.length) return
files[0].fileType = 'unknown'
files[index].fileType = 'unknown'
;[files[0], files[index]] = [files[index], files[0]]
if (await rejectOnRedundantWrappedZip(files[0].file)) return
await addDetectedData()
}
const tags = useGeneratedState()
@@ -241,6 +335,65 @@ export function createManageVersionContext(
}
}
async function checkRedundantWrappedZip(file: File): Promise<boolean> {
const fileName = file.name.toLowerCase()
if (!fileName.endsWith('.zip')) return false
const zip = await JSZip.loadAsync(file)
const entries = Object.keys(zip.files).map((e) => e.toLowerCase())
const filtered = entries.filter((e) => !e.startsWith('__macosx/') && !e.endsWith('.ds_store'))
const hasRootEntries = filtered.some((e) => !e.includes('/'))
if (hasRootEntries) return false
const topLevelFolders = new Set(filtered.map((e) => e.split('/')[0]).filter(Boolean))
if (topLevelFolders.size !== 1) return false
const [folderName] = [...topLevelFolders]
// Check if the inner folder contents indicate a datapack or resource pack
const innerEntries = filtered.map((e) => e.substring(folderName.length + 1))
const hasPackMcmeta = hasFile(innerEntries, 'pack.mcmeta')
const hasAssets = hasDir(innerEntries, 'assets')
const hasData = hasDir(innerEntries, 'data')
return hasPackMcmeta && (hasAssets || hasData)
}
async function rejectOnRedundantWrappedZip(file: File): Promise<boolean> {
if (await checkRedundantWrappedZip(file)) {
newDraftVersion(projectV2.value.id)
modal.value?.setStage('add-files')
addNotification({
title: 'Invalid ZIP structure',
text: `The uploaded ZIP file "${file.name}" contains a redundant top-level folder. Please re-zip the contents directly without the extra folder layer.`,
type: 'error',
})
return true
}
return false
}
async function inferEnvironmentFromVersions(
projectId: string,
loaders: string[],
): Promise<Labrinth.Projects.v3.Environment | undefined> {
try {
const versions = await labrinth.versions_v3.getProjectVersions(projectId, {
loaders,
})
if (versions.length > 0) {
const mostRecentVersion = versions[0]
const version = await labrinth.versions_v3.getVersion(mostRecentVersion.id)
return version.environment !== 'unknown' ? version.environment : undefined
}
} catch (error) {
console.error('Error fetching versions for environment inference:', error)
}
return undefined
}
async function setInferredVersionData(
file: File,
project: Labrinth.Projects.v2.Project,
@@ -251,19 +404,7 @@ export function createManageVersionContext(
tags.value.gameVersions,
)) as InferredVersionInfo
try {
const versions = await labrinth.versions_v3.getProjectVersions(project.id, {
loaders: inferred.loaders ?? [],
})
if (versions.length > 0) {
const mostRecentVersion = versions[0]
const version = await labrinth.versions_v3.getVersion(mostRecentVersion.id)
inferred.environment = version.environment !== 'unknown' ? version.environment : undefined
}
} catch (error) {
console.error('Error fetching versions for environment inference:', error)
}
inferred.environment = await inferEnvironmentFromVersions(project.id, inferred.loaders ?? [])
const noLoaders = !inferred.loaders?.length
@@ -284,6 +425,12 @@ export function createManageVersionContext(
return inferred
}
// Stage visibility computeds (inlined)
const noEnvironmentProject = computed(
() => projectType.value !== 'mod' && projectType.value !== 'modpack',
)
const noDependenciesProject = computed(() => projectType.value === 'modpack')
const getProject = async (projectId: string) => {
if (dependencyProjects.value[projectId]) {
return dependencyProjects.value[projectId]
@@ -302,16 +449,198 @@ export function createManageVersionContext(
return version
}
// Primary file computed
const primaryFile = computed<PrimaryFile | null>(() => {
const existingPrimaryFile = draftVersion.value.existing_files?.[0]
if (existingPrimaryFile) {
return {
name: existingPrimaryFile.filename,
fileType: existingPrimaryFile.file_type,
existing: true,
}
}
const addedPrimaryFile = filesToAdd.value[0]
if (addedPrimaryFile) {
return {
name: addedPrimaryFile.file.name,
fileType: addedPrimaryFile.fileType,
existing: false,
}
}
return null
})
// File handling helpers
function detectPrimaryFileIndex(files: File[]): number {
const extensionPriority = ['.jar', '.zip', '.litemod', '.mrpack', '.mrpack-primary']
for (const ext of extensionPriority) {
const matches = files.filter((file) => file.name.toLowerCase().endsWith(ext))
if (matches.length > 0) {
const shortest = matches.reduce((a, b) => (a.name.length < b.name.length ? a : b))
return files.indexOf(shortest)
}
}
return 0
}
const addDetectedData = async () => {
if (editingVersion.value) return
const primaryFileData = filesToAdd.value[0]?.file
if (!primaryFileData) return
try {
const inferredData = await setInferredVersionData(primaryFileData, projectV2.value)
const mappedInferredData: Partial<Labrinth.Versions.v3.DraftVersion> = {
...inferredData,
name: inferredData.name || '',
}
draftVersion.value = {
...draftVersion.value,
...mappedInferredData,
}
} catch (err) {
console.error('Error parsing version file data', err)
}
}
// Watch draft version dependencies to fetch project/version data
watch(
draftVersion,
async (version) => {
if (noDependenciesProject.value) return
const deps = version.dependencies || []
for (const dep of deps) {
try {
if (dep?.project_id) await getProject(dep.project_id)
if (dep?.version_id) await getVersion(dep.version_id)
} catch (error: any) {
addNotification({
title: 'Could not fetch dependency data',
text: error.data ? error.data.description : error,
type: 'error',
})
}
}
projectsFetchLoading.value = false
},
{ immediate: true, deep: true },
)
// Watch loaders to infer environment if not set
watch(
() => draftVersion.value.loaders,
async (loaders) => {
if (noEnvironmentProject.value) return
if (draftVersion.value.environment) return
if (!loaders?.length) return
const projectId = draftVersion.value.project_id
if (!projectId) return
const environment = await inferEnvironmentFromVersions(projectId, loaders)
if (environment && !draftVersion.value.environment) {
draftVersion.value.environment = environment
inferredVersionData.value = { ...inferredVersionData.value, environment }
}
},
)
// Watch loaders to fetch suggested dependencies
// Gets the most recent version that matches loaders and suggests its dependencies
watch(
() => draftVersion.value.loaders,
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, {
loaders,
})
// Get the most recent matching version and extract its dependencies
if (versions.length > 0) {
const mostRecentVersion = versions[0]
for (const dep of mostRecentVersion.dependencies) {
suggestedDependencies.value.push({
project_id: dep.project_id,
version_id: dep.version_id,
dependency_type: dep.dependency_type,
file_name: dep.file_name,
})
}
}
} catch (error: any) {
console.error(`Failed to get versions for project ${projectId}:`, error)
}
for (const dep of suggestedDependencies.value) {
try {
if (dep.project_id) {
const proj = await getProject(dep.project_id)
dep.name = proj.name
dep.icon = proj.icon_url
}
if (dep.version_id) {
const version = await getVersion(dep.version_id)
dep.versionName = version.name
}
} catch (error: any) {
console.error(`Failed to fetch project/version data for dependency:`, error)
}
}
} catch (error: any) {
addNotification({
title: 'Could not fetch suggested dependencies',
text: error.data ? error.data.description : error,
type: 'error',
})
}
},
{ immediate: true },
)
// Submission handlers
async function handleCreateVersion() {
const version = toRaw(draftVersion.value)
const files = toRaw(filesToAdd.value)
isSubmitting.value = true
isUploading.value = true
// Reset progress and navigate to uploading stage
uploadProgress.value = { loaded: 0, total: 0, progress: 0 }
if (noEnvironmentProject.value) version.environment = undefined
try {
await labrinth.versions_v3.createVersion(version, files, projectType.value ?? null)
const uploadHandle = labrinth.versions_v3.createVersion(
version,
files,
projectType.value ?? null,
)
// Subscribe to progress updates
uploadHandle.onProgress((progress) => {
uploadProgress.value = progress
})
// Wait for upload to complete
await uploadHandle.promise
modal.value?.hide()
addNotification({
title: 'Project version created',
@@ -319,13 +648,15 @@ export function createManageVersionContext(
type: 'success',
})
await refreshVersions()
onSave?.()
} catch (err: any) {
addNotification({
title: 'An error occurred',
title: 'Could not create project version',
text: err.data ? err.data.description : err,
type: 'error',
})
}
isUploading.value = false
isSubmitting.value = false
}
@@ -336,6 +667,12 @@ export function createManageVersionContext(
isSubmitting.value = true
// Reset progress if we have files to upload
if (files.length > 0) {
isUploading.value = true
uploadProgress.value = { loaded: 0, total: 0, progress: 0 }
}
if (noEnvironmentProject.value) version.environment = undefined
try {
@@ -362,7 +699,13 @@ export function createManageVersionContext(
await labrinth.versions_v3.modifyVersion(version.version_id, data)
if (files.length > 0) {
await labrinth.versions_v3.addFilesToVersion(version.version_id, files)
const uploadHandle = labrinth.versions_v3.addFilesToVersion(version.version_id, files)
uploadHandle.onProgress((progress) => {
uploadProgress.value = progress
})
await uploadHandle.promise
}
// Delete files that were marked for deletion
@@ -379,6 +722,7 @@ export function createManageVersionContext(
type: 'success',
})
await refreshVersions()
onSave?.()
} catch (err: any) {
addNotification({
title: 'An error occurred',
@@ -386,15 +730,10 @@ export function createManageVersionContext(
type: 'error',
})
}
isUploading.value = false
isSubmitting.value = false
}
// Stage visibility computeds (inlined)
const noLoadersProject = computed(() => projectType.value === 'resourcepack')
const noEnvironmentProject = computed(
() => projectType.value !== 'mod' && projectType.value !== 'modpack',
)
// Dynamic next button label
function getNextLabel(currentIndex: number | null = null) {
const currentStageIndex = currentIndex ? currentIndex : modal.value?.currentStageIndex || 0
@@ -424,6 +763,8 @@ export function createManageVersionContext(
return editingVersion.value ? 'Edit environment' : 'Add environment'
case 'add-changelog':
return editingVersion.value ? 'Edit changelog' : 'Add changelog'
case 'metadata':
return 'Edit metadata'
default:
return 'Next'
}
@@ -448,16 +789,23 @@ export function createManageVersionContext(
projectType,
dependencyProjects,
dependencyVersions,
handlingNewFiles,
projectsFetchLoading,
suggestedDependencies,
visibleSuggestedDependencies,
primaryFile,
// Stage management
stageConfigs,
isSubmitting,
isUploading,
uploadProgress,
modal,
// Computed
editingVersion,
noLoadersProject,
noEnvironmentProject,
noDependenciesProject,
// Stage helpers
getNextLabel,
@@ -465,10 +813,11 @@ export function createManageVersionContext(
// Methods
newDraftVersion,
setPrimaryFile,
setInferredVersionData,
swapPrimaryFile,
replacePrimaryFile,
getProject,
getVersion,
handleNewFiles,
handleCreateVersion,
handleSaveVersionEdits,
}

View File

@@ -0,0 +1,100 @@
import { LeftArrowIcon, RightArrowIcon, XIcon } from '@modrinth/assets'
import type { StageConfigInput } from '@modrinth/ui'
import { markRaw } from 'vue'
import AddFilesStage from '~/components/ui/create-project-version/stages/AddFilesStage.vue'
import type { ManageVersionContextValue } from '../manage-version-modal'
export const stageConfig: StageConfigInput<ManageVersionContextValue> = {
id: 'add-files',
stageContent: markRaw(AddFilesStage),
title: (ctx) => (ctx.editingVersion.value ? 'Edit files' : 'Files'),
nonProgressStage: (ctx) => ctx.editingVersion.value,
cannotNavigateForward: (ctx) => {
const hasFiles =
ctx.filesToAdd.value.length !== 0 ||
(ctx.draftVersion.value.existing_files?.length ?? 0) !== 0
return !hasFiles || ctx.handlingNewFiles.value
},
leftButtonConfig: (ctx) => {
const hasFiles =
ctx.filesToAdd.value.length !== 0 ||
(ctx.draftVersion.value.existing_files?.length ?? 0) !== 0
if (ctx.editingVersion.value)
return {
label: 'Cancel',
icon: XIcon,
onClick: () => ctx.modal.value?.hide(),
}
if (!hasFiles || ctx.handlingNewFiles.value) return null
return {
label: 'Cancel',
icon: XIcon,
onClick: () => ctx.modal.value?.hide(),
}
},
rightButtonConfig: (ctx) => {
const hasFiles =
ctx.filesToAdd.value.length !== 0 ||
(ctx.draftVersion.value.existing_files?.length ?? 0) !== 0
if (ctx.editingVersion.value)
return {
...ctx.saveButtonConfig(),
label: 'Save files',
disabled: ctx.isSubmitting.value,
}
if (!hasFiles || ctx.handlingNewFiles.value) return null
return {
label: ctx.getNextLabel(),
icon: RightArrowIcon,
iconPosition: 'after',
disabled: !hasFiles,
onClick: () => ctx.modal.value?.nextStage(),
}
},
}
export const fromDetailsStageConfig: StageConfigInput<ManageVersionContextValue> = {
id: 'from-details-files',
stageContent: markRaw(AddFilesStage),
title: 'Edit files',
nonProgressStage: true,
leftButtonConfig: (ctx) => {
const hasFiles =
ctx.filesToAdd.value.length !== 0 ||
(ctx.draftVersion.value.existing_files?.length ?? 0) !== 0
return {
label: 'Back',
icon: LeftArrowIcon,
disabled: !hasFiles,
onClick: () => ctx.modal.value?.setStage('metadata'),
}
},
rightButtonConfig: (ctx) => {
const hasFiles =
ctx.filesToAdd.value.length !== 0 ||
(ctx.draftVersion.value.existing_files?.length ?? 0) !== 0
return ctx.editingVersion.value
? {
...ctx.saveButtonConfig(),
label: 'Save files',
disabled: !hasFiles || ctx.isSubmitting.value,
}
: {
label: 'Add details',
icon: RightArrowIcon,
iconPosition: 'after',
disabled: !hasFiles,
onClick: () => ctx.modal.value?.setStage('add-details'),
}
},
}

View File

@@ -1,56 +0,0 @@
import { RightArrowIcon, XIcon } from '@modrinth/assets'
import type { StageConfigInput } from '@modrinth/ui'
import { markRaw } from 'vue'
import AddFilesStage from '~/components/ui/create-project-version/stages/AddFilesStage.vue'
import type { ManageVersionContextValue } from '../manage-version-modal'
export const stageConfig: StageConfigInput<ManageVersionContextValue> = {
id: 'add-files',
stageContent: markRaw(AddFilesStage),
title: (ctx) => (ctx.editingVersion.value ? 'Edit files' : 'Add files'),
leftButtonConfig: (ctx) => {
const hasFiles =
ctx.filesToAdd.value.length !== 0 ||
(ctx.draftVersion.value.existing_files?.length ?? 0) !== 0
if (ctx.editingVersion.value)
return {
label: 'Cancel',
icon: XIcon,
onClick: () => ctx.modal.value?.hide(),
}
if (!hasFiles) return null
return {
label: 'Cancel',
icon: XIcon,
onClick: () => ctx.modal.value?.hide(),
}
},
rightButtonConfig: (ctx) => {
const hasFiles =
ctx.filesToAdd.value.length !== 0 ||
(ctx.draftVersion.value.existing_files?.length ?? 0) !== 0
if (ctx.editingVersion.value)
return {
...ctx.saveButtonConfig(),
label: 'Save files',
disabled: ctx.isSubmitting.value,
}
if (!hasFiles) return null
return {
label: ctx.getNextLabel(),
icon: RightArrowIcon,
iconPosition: 'after',
disabled: !hasFiles,
onClick: () => ctx.modal.value?.nextStage(),
}
},
nonProgressStage: (ctx) => ctx.editingVersion.value,
}

View File

@@ -2,14 +2,15 @@ import { LeftArrowIcon, RightArrowIcon, XIcon } from '@modrinth/assets'
import type { StageConfigInput } from '@modrinth/ui'
import { markRaw } from 'vue'
import AddDetailsStage from '~/components/ui/create-project-version/stages/AddDetailsStage.vue'
import DependenciesStage from '~/components/ui/create-project-version/stages/DependenciesStage.vue'
import type { ManageVersionContextValue } from '../manage-version-modal'
export const stageConfig: StageConfigInput<ManageVersionContextValue> = {
id: 'add-details',
stageContent: markRaw(AddDetailsStage),
title: (ctx) => (ctx.editingVersion.value ? 'Edit details' : 'Add details'),
id: 'add-dependencies',
stageContent: markRaw(DependenciesStage),
title: (ctx) => (ctx.editingVersion.value ? 'Edit dependencies' : 'Dependencies'),
skip: true,
leftButtonConfig: (ctx) =>
ctx.editingVersion.value
? {
@@ -24,17 +25,35 @@ export const stageConfig: StageConfigInput<ManageVersionContextValue> = {
},
rightButtonConfig: (ctx) =>
ctx.editingVersion.value
? {
...ctx.saveButtonConfig(),
disabled:
ctx.draftVersion.value.version_number.trim().length === 0 || ctx.isSubmitting.value,
}
? ctx.saveButtonConfig()
: {
label: ctx.getNextLabel(),
icon: RightArrowIcon,
iconPosition: 'after',
disabled: ctx.draftVersion.value.version_number.trim().length === 0,
onClick: () => ctx.modal.value?.nextStage(),
},
nonProgressStage: (ctx) => ctx.editingVersion.value,
}
export const fromDetailsStageConfig: StageConfigInput<ManageVersionContextValue> = {
id: 'from-details-dependencies',
stageContent: markRaw(DependenciesStage),
title: 'Edit dependencies',
nonProgressStage: true,
leftButtonConfig: (ctx) => ({
label: 'Back',
icon: LeftArrowIcon,
onClick: () => 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'),
},
}

View File

@@ -2,14 +2,15 @@ import { LeftArrowIcon, PlusIcon, SaveIcon, SpinnerIcon, XIcon } from '@modrinth
import type { StageConfigInput } from '@modrinth/ui'
import { markRaw } from 'vue'
import AddChangelogStage from '~/components/ui/create-project-version/stages/AddChangelogStage.vue'
import DetailsStage from '~/components/ui/create-project-version/stages/DetailsStage.vue'
import type { ManageVersionContextValue } from '../manage-version-modal'
export const stageConfig: StageConfigInput<ManageVersionContextValue> = {
id: 'add-changelog',
stageContent: markRaw(AddChangelogStage),
title: (ctx) => (ctx.editingVersion.value ? 'Edit changelog' : 'Add changelog'),
id: 'add-details',
stageContent: markRaw(DetailsStage),
title: (ctx) => (ctx.editingVersion.value ? 'Edit details' : 'Details'),
maxWidth: '744px',
leftButtonConfig: (ctx) =>
ctx.editingVersion.value
? {
@@ -23,7 +24,13 @@ export const stageConfig: StageConfigInput<ManageVersionContextValue> = {
onClick: () => ctx.modal.value?.prevStage(),
},
rightButtonConfig: (ctx) => ({
label: ctx.editingVersion.value ? 'Save changes' : 'Create version',
label: ctx.editingVersion.value
? 'Save changes'
: ctx.isUploading.value
? ctx.uploadProgress.value.progress >= 1
? 'Creating version'
: `Uploading version ${Math.round(ctx.uploadProgress.value.progress * 100)}%`
: 'Create version',
icon: ctx.isSubmitting.value ? SpinnerIcon : ctx.editingVersion.value ? SaveIcon : PlusIcon,
iconPosition: 'before',
iconClass: ctx.isSubmitting.value ? 'animate-spin' : undefined,

View File

@@ -2,18 +2,20 @@ import { LeftArrowIcon, RightArrowIcon } from '@modrinth/assets'
import type { StageConfigInput } from '@modrinth/ui'
import { markRaw } from 'vue'
import AddEnvironmentStage from '~/components/ui/create-project-version/stages/AddEnvironmentStage.vue'
import EnvironmentStage from '~/components/ui/create-project-version/stages/EnvironmentStage.vue'
import type { ManageVersionContextValue } from '../manage-version-modal'
export const stageConfig: StageConfigInput<ManageVersionContextValue> = {
id: 'add-environment',
stageContent: markRaw(AddEnvironmentStage),
title: (ctx) => (ctx.editingVersion.value ? 'Edit environment' : 'Add environment'),
stageContent: markRaw(EnvironmentStage),
title: (ctx) => (ctx.editingVersion.value ? 'Edit environment' : 'Environment'),
skip: (ctx) =>
ctx.noEnvironmentProject.value ||
(!ctx.editingVersion.value && !!ctx.inferredVersionData.value?.environment) ||
(ctx.editingVersion.value && !!ctx.draftVersion.value.environment),
hideStageInBreadcrumb: (ctx) => !ctx.primaryFile.value || ctx.handlingNewFiles.value,
cannotNavigateForward: (ctx) => !ctx.draftVersion.value.environment,
leftButtonConfig: (ctx) => ({
label: 'Back',
icon: LeftArrowIcon,
@@ -30,14 +32,14 @@ export const stageConfig: StageConfigInput<ManageVersionContextValue> = {
export const fromDetailsStageConfig: StageConfigInput<ManageVersionContextValue> = {
id: 'from-details-environment',
stageContent: markRaw(AddEnvironmentStage),
stageContent: markRaw(EnvironmentStage),
title: 'Edit environment',
nonProgressStage: true,
leftButtonConfig: (ctx) => ({
label: 'Back',
icon: LeftArrowIcon,
disabled: !ctx.draftVersion.value.environment,
onClick: () => ctx.modal.value?.setStage('add-details'),
onClick: () => ctx.modal.value?.setStage('metadata'),
}),
rightButtonConfig: (ctx) =>
ctx.editingVersion.value
@@ -46,10 +48,10 @@ export const fromDetailsStageConfig: StageConfigInput<ManageVersionContextValue>
disabled: !ctx.draftVersion.value.environment,
}
: {
label: ctx.getNextLabel(2),
label: 'Add details',
icon: RightArrowIcon,
iconPosition: 'after',
disabled: !ctx.draftVersion.value.environment,
onClick: () => ctx.modal.value?.setStage(2),
onClick: () => ctx.modal.value?.setStage('add-details'),
},
}

View File

@@ -1,30 +1,39 @@
import { stageConfig as addChangelogStageConfig } from './add-changelog'
import { stageConfig as addDependenciesStageConfig } from './add-dependencies'
import { stageConfig as addDetailsStageConfig } from './add-details'
import {
fromDetailsStageConfig as editEnvironmentStageConfig,
stageConfig as addEnvironmentStageConfig,
} from './add-environment'
import { stageConfig as addFilesStageConfig } from './add-files'
fromDetailsStageConfig as fromDetailsFilesStageConfig,
stageConfig as addFilesStageConfig,
} from './add-files-stage'
import {
fromDetailsStageConfig as editLoadersStageConfig,
stageConfig as addLoadersStageConfig,
} from './add-loaders'
fromDetailsStageConfig as fromDetailsDependenciesStageConfig,
stageConfig as dependenciesStageConfig,
} from './dependencies-stage'
import { stageConfig as detailsStageConfig } from './details-stage'
import {
fromDetailsStageConfig as editMcVersionsStageConfig,
stageConfig as addMcVersionsStageConfig,
} from './add-mc-versions'
fromDetailsStageConfig as fromDetailsEnvironmentStageConfig,
stageConfig as environmentStageConfig,
} from './environment-stage'
import {
fromDetailsStageConfig as fromDetailsLoadersStageConfig,
stageConfig as loadersStageConfig,
} from './loaders-stage'
import {
fromDetailsStageConfig as fromDetailsMcVersionsStageConfig,
stageConfig as mcVersionsStageConfig,
} from './mc-versions-stage'
import { stageConfig as metadataStageConfig } from './metadata-stage'
export const stageConfigs = [
addFilesStageConfig,
addDetailsStageConfig,
addLoadersStageConfig,
addMcVersionsStageConfig,
addEnvironmentStageConfig,
addDependenciesStageConfig,
addChangelogStageConfig,
loadersStageConfig,
mcVersionsStageConfig,
environmentStageConfig,
dependenciesStageConfig,
metadataStageConfig,
detailsStageConfig,
// Non-progress stages for editing from details page
editLoadersStageConfig,
editMcVersionsStageConfig,
editEnvironmentStageConfig,
fromDetailsLoadersStageConfig,
fromDetailsMcVersionsStageConfig,
fromDetailsEnvironmentStageConfig,
fromDetailsFilesStageConfig,
fromDetailsDependenciesStageConfig,
]

View File

@@ -2,18 +2,18 @@ import { LeftArrowIcon, RightArrowIcon } from '@modrinth/assets'
import type { StageConfigInput } from '@modrinth/ui'
import { markRaw } from 'vue'
import AddLoadersStage from '~/components/ui/create-project-version/stages/AddLoadersStage.vue'
import LoadersStage from '~/components/ui/create-project-version/stages/LoadersStage.vue'
import type { ManageVersionContextValue } from '../manage-version-modal'
export const stageConfig: StageConfigInput<ManageVersionContextValue> = {
id: 'add-loaders',
stageContent: markRaw(AddLoadersStage),
title: (ctx) => (ctx.editingVersion.value ? 'Edit loaders' : 'Add loaders'),
stageContent: markRaw(LoadersStage),
title: (ctx) => (ctx.editingVersion.value ? 'Edit loaders' : 'Loaders'),
skip: (ctx) =>
ctx.noLoadersProject.value ||
(ctx.inferredVersionData.value?.loaders?.length ?? 0) > 0 ||
ctx.editingVersion.value,
(ctx.inferredVersionData.value?.loaders?.length ?? 0) > 0 || ctx.editingVersion.value,
hideStageInBreadcrumb: (ctx) => !ctx.primaryFile.value || ctx.handlingNewFiles.value,
cannotNavigateForward: (ctx) => ctx.draftVersion.value.loaders.length === 0,
leftButtonConfig: (ctx) => ({
label: 'Back',
icon: LeftArrowIcon,
@@ -30,14 +30,14 @@ export const stageConfig: StageConfigInput<ManageVersionContextValue> = {
export const fromDetailsStageConfig: StageConfigInput<ManageVersionContextValue> = {
id: 'from-details-loaders',
stageContent: markRaw(AddLoadersStage),
stageContent: markRaw(LoadersStage),
title: 'Edit loaders',
nonProgressStage: true,
leftButtonConfig: (ctx) => ({
label: 'Back',
icon: LeftArrowIcon,
disabled: ctx.draftVersion.value.loaders.length === 0,
onClick: () => ctx.modal.value?.setStage('add-details'),
onClick: () => ctx.modal.value?.setStage('metadata'),
}),
rightButtonConfig: (ctx) =>
ctx.editingVersion.value
@@ -46,10 +46,10 @@ export const fromDetailsStageConfig: StageConfigInput<ManageVersionContextValue>
disabled: ctx.draftVersion.value.loaders.length === 0,
}
: {
label: ctx.getNextLabel(2),
label: 'Add details',
icon: RightArrowIcon,
iconPosition: 'after',
disabled: ctx.draftVersion.value.loaders.length === 0,
onClick: () => ctx.modal.value?.setStage(2),
onClick: () => ctx.modal.value?.setStage('add-details'),
},
}

View File

@@ -2,16 +2,19 @@ import { LeftArrowIcon, RightArrowIcon } from '@modrinth/assets'
import type { StageConfigInput } from '@modrinth/ui'
import { markRaw } from 'vue'
import AddMcVersionsStage from '~/components/ui/create-project-version/stages/AddMcVersionsStage.vue'
import McVersionsStage from '~/components/ui/create-project-version/stages/McVersionsStage.vue'
import type { ManageVersionContextValue } from '../manage-version-modal'
export const stageConfig: StageConfigInput<ManageVersionContextValue> = {
id: 'add-mc-versions',
stageContent: markRaw(AddMcVersionsStage),
title: (ctx) => (ctx.editingVersion.value ? 'Edit game versions' : 'Add game versions'),
stageContent: markRaw(McVersionsStage),
title: (ctx) => (ctx.editingVersion.value ? 'Edit game versions' : 'Game versions'),
skip: (ctx) =>
(ctx.inferredVersionData.value?.game_versions?.length ?? 0) > 0 || ctx.editingVersion.value,
(ctx.inferredVersionData.value?.game_versions?.length ?? 0) > 0 || !ctx.primaryFile.value,
hideStageInBreadcrumb: (ctx) => !ctx.primaryFile.value || ctx.handlingNewFiles.value,
cannotNavigateForward: (ctx) => ctx.draftVersion.value.game_versions.length === 0,
leftButtonConfig: (ctx) => ({
label: 'Back',
icon: LeftArrowIcon,
@@ -28,14 +31,14 @@ export const stageConfig: StageConfigInput<ManageVersionContextValue> = {
export const fromDetailsStageConfig: StageConfigInput<ManageVersionContextValue> = {
id: 'from-details-mc-versions',
stageContent: markRaw(AddMcVersionsStage),
stageContent: markRaw(McVersionsStage),
title: 'Edit game versions',
nonProgressStage: true,
leftButtonConfig: (ctx) => ({
label: 'Back',
icon: LeftArrowIcon,
disabled: ctx.draftVersion.value.game_versions.length === 0,
onClick: () => ctx.modal.value?.setStage('add-details'),
onClick: () => ctx.modal.value?.setStage('metadata'),
}),
rightButtonConfig: (ctx) =>
ctx.editingVersion.value
@@ -44,10 +47,10 @@ export const fromDetailsStageConfig: StageConfigInput<ManageVersionContextValue>
disabled: ctx.draftVersion.value.game_versions.length === 0,
}
: {
label: ctx.getNextLabel(2),
label: 'Add details',
icon: RightArrowIcon,
iconPosition: 'after',
disabled: ctx.draftVersion.value.game_versions.length === 0,
onClick: () => ctx.modal.value?.setStage(2),
onClick: () => ctx.modal.value?.setStage('add-details'),
},
}

View File

@@ -2,15 +2,14 @@ import { LeftArrowIcon, RightArrowIcon, XIcon } from '@modrinth/assets'
import type { StageConfigInput } from '@modrinth/ui'
import { markRaw } from 'vue'
import AddDependenciesStage from '~/components/ui/create-project-version/stages/AddDependenciesStage.vue'
import MetadataStage from '~/components/ui/create-project-version/stages/MetadataStage.vue'
import type { ManageVersionContextValue } from '../manage-version-modal'
export const stageConfig: StageConfigInput<ManageVersionContextValue> = {
id: 'add-dependencies',
stageContent: markRaw(AddDependenciesStage),
title: (ctx) => (ctx.editingVersion.value ? 'Edit dependencies' : 'Add dependencies'),
skip: (ctx) => ctx.projectType.value === 'modpack',
id: 'metadata',
stageContent: markRaw(MetadataStage),
title: 'Metadata',
leftButtonConfig: (ctx) =>
ctx.editingVersion.value
? {
@@ -25,12 +24,13 @@ export const stageConfig: StageConfigInput<ManageVersionContextValue> = {
},
rightButtonConfig: (ctx) =>
ctx.editingVersion.value
? ctx.saveButtonConfig()
? {
...ctx.saveButtonConfig(),
}
: {
label: ctx.getNextLabel(),
icon: RightArrowIcon,
iconPosition: 'after',
onClick: () => ctx.modal.value?.nextStage(),
},
nonProgressStage: (ctx) => ctx.editingVersion.value,
}