feat: managing project versions (#4811)

* start modal, working show modal

* add stages and implement MultiModalStage component

* add project versions context and add file button

* implement add files stage

* export interfaces

* move MultiStageModal to /base

* small update to file input

* add version types to api-client

* wrap version namespace under v3

* implement add details stage fields and loaders component

* start create MC versions stage

* implement changelog stage and bring width into a per stage concern

* implement loader picker with grouping

* improve grouping and sorting for loader picker

* use chips component

* small updaets

* fix loader icon color

* componentize mc version picker

* initial version of shift click to select range

* use newModal for markdown editor

* start add dependencies stage with search

* implement showing mod options in search

* componentize modselect and add version/dependency relation select

* hide version and dependency relation when no project selected

* fix project facet search

* implement api-client versions requests

* fix search api request facet type to be string

* fix new modal outer container scroll

* implement add dependency stage

* fix parse error

* add placeholders

* fix types

* update dependency row styles

* small change

* fix the types on manage versions to be correct with labrinth request bodies

* fix create version file parts

* use draft version ref in flow and implement proper file handlling

* use draft version ref for mc versions select

* implement reactive modal state and conditionally disabled next buttons

* ensure all data is using draftVersion ref

* remove shift click to select range since it sucks

* fix up add dependencies stage state/types

* small fixes

* implement adding dependencies connected to api calls and make adding dependencies work

* add final create version button config

* start create version backend call and bring versions table to project settings

* set add files stage width

* remove version file upload in project page

* small fix

* fix create version api call

* implement error handling

* implement mc versions search

* implement showing all mc versions

* small fix

* implement prefill data

* add success notification

* add cancel button

* add new dropzone file input

* run pnpm run fix

* add tailwind preset in ui package

* polish file version row

* fix modal widths

* hide added versions when no versions added

* implement add loaders stage

* implement small chips and small fixes

* implement grouping for all releases

* implement new all releases grouping

* implement better shift click for version select

* small fixes

* fix search input style

* delete versions provider and start project type inferring

* implement getting project type

* add versions empty state, add folder up icon and pnpm run fix

* implement create version in project versions table

* update side nav

* implement dynamic create version flow depending on project type and detected data

* add id to stages and fix calling setStage not working

* move added loaded out of loader picker

* remove selected and detected MC versions

* add loading message to dependency search and fix dependency type always being "required"

* fix components in ref

* fix width on dropdown

* implement toggle all mc versions based on state of last in range

* fix mc version text colour

* do proper clean up

* update loaders to use tag item

* update UI to use TagItem and better match styles

* handle detected data when setting primary file

* add progress bar

* hide progress bar for non-progress stage

* add loading state on submit

* properly cache dependencies projects/versions

* pnpm run fix

* add dragover show purple border on dropzone file input

* better handle added dependencies

* move versions in side nav

* implement adding file type

* fix api body format for file type

* implement working edit existing version
- working add/remove file
- working edit version details

* a step towards proper versions refresh

* add gallery to project settings

* actually figured out refresh versions

* move checklist into settings page

* remove editing version from version page and add button to versions table in project settings

* remove edit and delete buttons from gallery in project page

* add empty state messages for project page

* add default scroll bar styles

* implement support for new file types

* remove edit from dropdown in project page versions table

* redirect to settings page

* move changelog to row with actions

* fix overflow on added dependencies

* fix redirect

* update scroll styles

* implement add environment stage (create and modify version not persisting environment to backend)

* small style fixes

* small spacing fix

* small style fixes

* add a flag for loading dependency projects

* address PR comments

* fix modrinth ui imports

* use magic keys instead of window.addeventlistener

* add spacing in bottom of settings page

* useDebounceFn from vue

* fix inconsistent stroke

* persist scroll through

* fix remove button

* fix api fields

* fix version file dropdown: hide primary option in edit mode and fix setting initial value

* fix links in nags

* implement skipped field for skipping steps instead of mutating stages array's elements

* implement suggested dependencies components

* implement suggested dependencies api call

* refactor cached get project and get version calls

* always hide environments

* update links

* set scroll in 10ms

* update links

* fix links pt2

* fix shadow

* fix progress bar

* dont include mc versions in suggested versions finder

* fix text overflow styles

* use tooltip

* fix change version name api

* implement set environment api call

* delete unused vue pages

* implement detected environment, edit environment step, and fix showing loaders in details for no loader projects

* small fix

* no loaders project wrong check

* fix not having 'minecraft' loader for resource pack

* implement updating existing files file type

* move add minecraft loader outside try catch

* add datapack to have environment

* fix being able to select duplicate MC versions

* remove datapack project from environment

* fix version fetch

* fix having detected environment not properly skipping step

* only add detected data when primary file changes

* fix unknown environemtn

* implement gallery and versions have moved admonition

* update project page for creator view

* small copy update

* merge fixes

* pnpm run fix

* fix checkmark squished

* fix version type can be deselected

* refactor: DI context + better typed MultistageModal

* fix type import

* Misc QA fixes

* fix allowed file types with no project type

* implement new add files stage

* fix versiosn header with new pagination

* hide buttons when no files for add file stage

* use prettier formatter

* allow signature file types

* add detecting primary file

* fix progress bar in firefox

* fix environment not correctly being hidden/shown

* remove environment missing nag

* temp bring back environment page

* remove delete version action from project page

* replace "continue" next button label with actual next step

* fix types

* pnpm run fix

* move supplementary files alert up and update border radius style on dropzone

* copy updates

* small update on version num placeholder

* update placeholder

* make timeout on upload routes 2 minutes

* fix lint issues

* run pnpm intl:extract

---------

Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
This commit is contained in:
Truman Gao
2025-12-18 11:56:15 -08:00
committed by GitHub
parent 9ad01723a2
commit 9958600121
69 changed files with 4954 additions and 585 deletions

View File

@@ -6,6 +6,7 @@ import { ArchonServersV0Module } from './archon/servers/v0'
import { ArchonServersV1Module } from './archon/servers/v1'
import { ISO3166Module } from './iso3166'
import { KyrosFilesV0Module } from './kyros/files/v0'
import { LabrinthVersionsV3Module } from './labrinth'
import { LabrinthBillingInternalModule } from './labrinth/billing/internal'
import { LabrinthCollectionsModule } from './labrinth/collections'
import { LabrinthProjectsV2Module } from './labrinth/projects/v2'
@@ -35,6 +36,7 @@ export const MODULE_REGISTRY = {
labrinth_projects_v2: LabrinthProjectsV2Module,
labrinth_projects_v3: LabrinthProjectsV3Module,
labrinth_state: LabrinthStateModule,
labrinth_versions_v3: LabrinthVersionsV3Module,
} as const satisfies Record<string, ModuleConstructor>
export type ModuleID = keyof typeof MODULE_REGISTRY

View File

@@ -3,3 +3,4 @@ export * from './collections'
export * from './projects/v2'
export * from './projects/v3'
export * from './state'
export * from './versions/v3'

View File

@@ -68,7 +68,10 @@ export class LabrinthProjectsV2Module extends AbstractModule {
api: 'labrinth',
version: 2,
method: 'GET',
params: params as Record<string, unknown>,
params: {
...params,
facets: params.facets ? JSON.stringify(params.facets) : undefined,
},
})
}

View File

@@ -172,6 +172,7 @@ export namespace Labrinth {
| 'shader'
| 'plugin'
| 'datapack'
| 'project'
export type GalleryImage = {
url: string
@@ -264,7 +265,7 @@ export namespace Labrinth {
export type ProjectSearchParams = {
query?: string
facets?: string[][]
facets?: string[][] // in the format of [["categories:forge"],["versions:1.17.1"]]
filters?: string
index?: 'relevance' | 'downloads' | 'follows' | 'newest' | 'updated'
offset?: number
@@ -420,23 +421,38 @@ export namespace Labrinth {
}
}
// TODO: consolidate duplicated types between v2 and v3 versions
export namespace v3 {
export type VersionType = 'release' | 'beta' | 'alpha'
export interface Dependency {
dependency_type: Labrinth.Versions.v2.DependencyType
project_id?: string
file_name?: string
version_id?: string
}
export type VersionStatus =
| 'listed'
| 'archived'
| 'draft'
| 'unlisted'
| 'scheduled'
export interface GetProjectVersionsParams {
game_versions?: string[]
loaders?: string[]
}
export type VersionChannel = 'release' | 'beta' | 'alpha'
export type FileType =
| 'required-resource-pack'
| 'optional-resource-pack'
| 'sources-jar'
| 'dev-jar'
| 'javadoc-jar'
| 'signature'
| 'unknown'
export type DependencyType = 'required' | 'optional' | 'incompatible' | 'embedded'
export interface VersionFileHash {
sha512: string
sha1: string
}
export type FileType = 'required-resource-pack' | 'optional-resource-pack' | 'unknown'
export type VersionFile = {
hashes: Record<string, string>
interface VersionFile {
hashes: VersionFileHash
url: string
filename: string
primary: boolean
@@ -444,35 +460,75 @@ export namespace Labrinth {
file_type?: FileType
}
export type Dependency = {
version_id?: string
project_id?: string
file_name?: string
dependency_type: DependencyType
}
export type Version = {
export interface Version {
name: string
version_number: string
changelog?: string
dependencies: Dependency[]
game_versions: string[]
version_type: VersionChannel
loaders: string[]
featured: boolean
status: Labrinth.Versions.v2.VersionStatus
id: string
project_id: string
author_id: string
featured: boolean
name: string
version_number: string
project_types: string[]
games: string[]
changelog: string
date_published: string
downloads: number
version_type: VersionType
status: VersionStatus
requested_status?: VersionStatus | null
files: VersionFile[]
dependencies: Dependency[]
environment?: Labrinth.Projects.v3.Environment
}
export interface DraftVersionFile {
fileType?: FileType
file: File
}
export type DraftVersion = Omit<
Labrinth.Versions.v3.CreateVersionRequest,
'file_parts' | 'primary_file' | 'file_types'
> & {
existing_files?: VersionFile[]
version_id?: string
environment?: Labrinth.Projects.v3.Environment
}
export interface CreateVersionRequest {
name: string
version_number: string
changelog: string
dependencies?: Array<{
version_id?: string
project_id?: string
file_name?: string
dependency_type: Labrinth.Versions.v2.DependencyType
}>
game_versions: string[]
version_type: 'release' | 'beta' | 'alpha'
loaders: string[]
ordering?: number | null
game_versions?: string[]
mrpack_loaders?: string[]
environment?: string
featured?: boolean
status?: 'listed' | 'archived' | 'draft' | 'unlisted' | 'scheduled' | 'unknown'
requested_status?: 'listed' | 'archived' | 'draft' | 'unlisted' | null
project_id: string
file_parts: string[]
primary_file?: string
file_types?: Record<string, Labrinth.Versions.v3.FileType | null>
environment?: Labrinth.Projects.v3.Environment
}
export type ModifyVersionRequest = Partial<
Omit<CreateVersionRequest, 'project_id' | 'file_parts' | 'primary_file' | 'file_types'>
> & {
file_types?: {
algorithm: string
hash: string
file_type: Labrinth.Versions.v3.FileType | null
}[]
}
export type AddFilesToVersionRequest = {
file_parts: string[]
file_types?: Record<string, Labrinth.Versions.v3.FileType | null>
}
}
}
@@ -566,7 +622,7 @@ export namespace Labrinth {
export interface GameVersion {
version: string
version_type: string
date: string // RFC 3339 DateTime
date: string
major: boolean
}

View File

@@ -0,0 +1,282 @@
import { AbstractModule } from '../../../core/abstract-module'
import type { Labrinth } from '../types'
export class LabrinthVersionsV3Module extends AbstractModule {
public getModuleID(): string {
return 'labrinth_versions_v3'
}
/**
* Get versions for a project (v3)
*
* @param id - Project ID or slug (e.g., 'sodium' or 'AANobbMI')
* @param options - Optional query parameters to filter versions
* @returns Promise resolving to an array of v3 versions
*
* @example
* ```typescript
* const versions = await client.labrinth.versions_v3.getProjectVersions('sodium')
* const filteredVersions = await client.labrinth.versions_v3.getProjectVersions('sodium', {
* game_versions: ['1.20.1'],
* loaders: ['fabric']
* })
* console.log(versions[0].version_number)
* ```
*/
public async getProjectVersions(
id: string,
options?: Labrinth.Versions.v3.GetProjectVersionsParams,
): Promise<Labrinth.Versions.v3.Version[]> {
const params: Record<string, string> = {}
if (options?.game_versions?.length) {
params.game_versions = JSON.stringify(options.game_versions)
}
if (options?.loaders?.length) {
params.loaders = JSON.stringify(options.loaders)
}
return this.client.request<Labrinth.Versions.v3.Version[]>(`/project/${id}/version`, {
api: 'labrinth',
version: 2, // TODO: move this to a versions v2 module to keep api-client clean and organized
method: 'GET',
params: Object.keys(params).length > 0 ? params : undefined,
})
}
/**
* Get a specific version by ID (v3)
*
* @param id - Version ID
* @returns Promise resolving to the v3 version data
*
* @example
* ```typescript
* const version = await client.labrinth.versions_v3.getVersion('DXtmvS8i')
* console.log(version.version_number)
* ```
*/
public async getVersion(id: string): Promise<Labrinth.Versions.v3.Version> {
return this.client.request<Labrinth.Versions.v3.Version>(`/version/${id}`, {
api: 'labrinth',
version: 3,
method: 'GET',
})
}
/**
* Get multiple versions by IDs (v3)
*
* @param ids - Array of version IDs
* @returns Promise resolving to an array of v3 versions
*
* @example
* ```typescript
* const versions = await client.labrinth.versions_v3.getVersions(['DXtmvS8i', 'abc123'])
* console.log(versions[0].version_number)
* ```
*/
public async getVersions(ids: string[]): Promise<Labrinth.Versions.v3.Version[]> {
return this.client.request<Labrinth.Versions.v3.Version[]>(`/versions`, {
api: 'labrinth',
version: 3,
method: 'GET',
params: { ids: JSON.stringify(ids) },
})
}
/**
* Get a version from a project by version ID or number (v3)
*
* @param projectId - Project ID or slug
* @param versionId - Version ID or version number
* @returns Promise resolving to the v3 version data
*
* @example
* ```typescript
* const version = await client.labrinth.versions_v3.getVersionFromIdOrNumber('sodium', 'DXtmvS8i')
* const versionByNumber = await client.labrinth.versions_v3.getVersionFromIdOrNumber('sodium', '0.4.12')
* ```
*/
public async getVersionFromIdOrNumber(
projectId: string,
versionId: string,
): Promise<Labrinth.Versions.v3.Version> {
return this.client.request<Labrinth.Versions.v3.Version>(
`/project/${projectId}/version/${versionId}`,
{
api: 'labrinth',
version: 3,
method: 'GET',
},
)
}
/**
* Create a new version for a project (v3)
*
* Creates a new version on an existing project. At least one file must be
* attached unless the version is created as a draft.
*
* @param data - JSON metadata payload for the version (must include file_parts)
* @param files - Array of uploaded files, in the same order as `data.file_parts`
*
* @returns A promise resolving to the newly created version data
*
* @example
* ```ts
* const version = await client.labrinth.versions_v3.createVersion('sodium', {
* name: 'v0.5.0',
* version_number: '0.5.0',
* version_type: 'release',
* loaders: ['fabric'],
* game_versions: ['1.20.1'],
* project_id: 'sodium',
* file_parts: ['primary']
* }, [fileObject])
* ```
*/
public async createVersion(
draftVersion: Labrinth.Versions.v3.DraftVersion,
versionFiles: Labrinth.Versions.v3.DraftVersionFile[],
): Promise<Labrinth.Versions.v3.Version> {
const formData = new FormData()
const files = versionFiles.map((vf) => vf.file)
const fileTypes = versionFiles.map((vf) => vf.fileType || null)
const fileParts = files.map((file, i) => {
return `${file.name}-${i === 0 ? 'primary' : i}`
})
const fileTypeMap = fileParts.reduce<Record<string, Labrinth.Versions.v3.FileType | null>>(
(acc, key, i) => {
acc[key] = fileTypes[i]
return acc
},
{},
)
const data: Labrinth.Versions.v3.CreateVersionRequest = {
project_id: draftVersion.project_id,
version_number: draftVersion.version_number,
name: draftVersion.name || draftVersion.version_number,
changelog: draftVersion.changelog,
dependencies: draftVersion.dependencies || [],
game_versions: draftVersion.game_versions,
loaders: draftVersion.loaders,
version_type: draftVersion.version_type,
featured: !!draftVersion.featured,
file_parts: fileParts,
file_types: fileTypeMap,
primary_file: fileParts[0],
environment: draftVersion.environment,
}
formData.append('data', JSON.stringify(data))
files.forEach((file, i) => {
formData.append(fileParts[i], new Blob([file]), file.name)
})
return this.client.request<Labrinth.Versions.v3.Version>(`/version`, {
api: 'labrinth',
version: 3,
method: 'POST',
body: formData,
timeout: 120000,
headers: {
'Content-Type': '',
},
})
}
/**
* Modify an existing version by ID (v3)
*
* Partially updates a versions metadata. Only JSON fields may be modified.
* To update files, use the separate "Add files to version" endpoint.
*
* @param versionId - The version ID to update
* @param data - PATCH metadata for this version (all fields optional)
*
* @returns A promise resolving to the updated version data
*
* @example
* ```ts
* const updated = await client.labrinth.versions_v3.modifyVersion('DXtmvS8i', {
* name: 'v1.0.1',
* changelog: 'Updated changelog',
* featured: true,
* status: 'listed'
* })
* ```
*/
public async modifyVersion(
versionId: string,
data: Labrinth.Versions.v3.ModifyVersionRequest,
): Promise<Labrinth.Versions.v3.Version> {
return this.client.request<Labrinth.Versions.v3.Version>(`/version/${versionId}`, {
api: 'labrinth',
version: 3,
method: 'PATCH',
body: data,
})
}
/**
* Delete a version by ID (v3)
*
* @param versionId - Version ID
*
* @example
* ```typescript
* await client.labrinth.versions_v3.deleteVersion('DXtmvS8i')
* ```
*/
public async deleteVersion(versionId: string): Promise<void> {
return this.client.request(`/version/${versionId}`, {
api: 'labrinth',
version: 2,
method: 'DELETE',
})
}
public async addFilesToVersion(
versionId: string,
versionFiles: Labrinth.Versions.v3.DraftVersionFile[],
): Promise<Labrinth.Versions.v3.Version> {
const formData = new FormData()
const files = versionFiles.map((vf) => vf.file)
const fileTypes = versionFiles.map((vf) => vf.fileType || null)
const fileParts = files.map((file, i) => `${file.name}-${i}`)
const fileTypeMap = fileParts.reduce<Record<string, Labrinth.Versions.v3.FileType | null>>(
(acc, key, i) => {
acc[key] = fileTypes[i]
return acc
},
{},
)
formData.append('data', JSON.stringify({ file_types: fileTypeMap }))
files.forEach((file, i) => {
formData.append(fileParts[i], new Blob([file]), file.name)
})
return this.client.request<Labrinth.Versions.v3.Version>(`/version/${versionId}/file`, {
api: 'labrinth',
version: 2,
method: 'POST',
body: formData,
timeout: 120000,
headers: {
'Content-Type': '',
},
})
}
}