Files
AstralRinth/apps/frontend/src/components/ui/create-project-version/components/McVersionPicker.vue
T
Truman Gao 9958600121 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>
2025-12-18 19:56:15 +00:00

209 lines
5.8 KiB
Vue

<template>
<div class="space-y-2.5">
<div class="flex items-center justify-between">
<span class="font-semibold text-contrast">
Minecraft versions <span class="text-red">*</span>
</span>
<Chips
v-model="versionType"
:items="['release', 'all']"
:never-empty="true"
:capitalize="true"
size="small"
/>
</div>
<div class="iconified-input w-full">
<SearchIcon aria-hidden="true" />
<input v-model="searchQuery" type="text" placeholder="Search versions" />
</div>
<div
class="flex h-72 select-none flex-col gap-3 overflow-y-auto rounded-xl border border-solid border-surface-5 p-3 py-4"
>
<div v-for="group in groupedGameVersions" :key="group.key" class="space-y-1.5">
<span class="font-semibold">{{ group.key }}</span>
<div class="flex flex-wrap gap-2 gap-x-1.5">
<ButtonStyled
v-for="version in group.versions"
:key="version"
:color="
holdingShift && version === anchorVersion
? 'purple'
: modelValue.includes(version)
? 'green'
: 'standard'
"
:highlighted="modelValue.includes(version)"
type="chip"
>
<button
class="!py-1.5 focus:outline-none"
:class="[
versionType === 'all' && !group.isReleaseGroup ? 'w-max' : 'w-16',
modelValue.includes(version) ? '!text-contrast' : '',
]"
@click="() => handleToggleVersion(version)"
@blur="
() => {
if (!holdingShift) anchorVersion = ''
}
"
>
{{ version }}
</button>
</ButtonStyled>
</div>
</div>
<span v-if="!filteredVersions.length">No versions found.</span>
</div>
<div>Hold shift and click to select range.</div>
</div>
</template>
<script lang="ts" setup>
import type { Labrinth } from '@modrinth/api-client'
import { SearchIcon } from '@modrinth/assets'
import { ButtonStyled, Chips } from '@modrinth/ui'
import { useMagicKeys } from '@vueuse/core'
import { computed, ref } from 'vue'
type GameVersion = Labrinth.Tags.v2.GameVersion
const props = defineProps<{
modelValue: string[]
gameVersions: Labrinth.Tags.v2.GameVersion[]
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: string[]): void
}>()
const keys = useMagicKeys()
const holdingShift = computed(() => keys.shift.value)
const versionType = ref<string | null>('release')
const searchQuery = ref('')
const filteredVersions = computed(() =>
props.gameVersions
.filter((v) => versionType.value === 'all' || v.version_type === versionType.value)
.filter(searchFilter),
)
const groupedGameVersions = computed(() => groupVersions(filteredVersions.value))
const allVersionsFlat = computed(() => groupedGameVersions.value.flatMap((group) => group.versions))
const anchorVersion = ref<string | null>(null)
const handleToggleVersion = (version: string) => {
const flat = allVersionsFlat.value
if (holdingShift.value && anchorVersion.value && flat.length) {
const anchorIdx = flat.indexOf(anchorVersion.value)
const targetIdx = flat.indexOf(version)
if (anchorIdx === -1 || targetIdx === -1) {
return toggleVersion(version)
}
const start = Math.min(anchorIdx, targetIdx)
const end = Math.max(anchorIdx, targetIdx)
const range = flat.slice(start, end + 1)
const isTargetSelected = props.modelValue.includes(version)
if (isTargetSelected) {
emit(
'update:modelValue',
props.modelValue.filter((v) => !range.includes(v)),
)
} else {
const newVersions = range.filter((v) => !props.modelValue.includes(v))
emit('update:modelValue', [...props.modelValue, ...newVersions])
}
anchorVersion.value = ''
return
}
toggleVersion(version)
anchorVersion.value = version
}
const toggleVersion = (version: string) => {
const isSelected = props.modelValue.includes(version)
const next = isSelected
? props.modelValue.filter((v) => v !== version)
: [...props.modelValue, version]
emit('update:modelValue', next)
}
const DEV_RELEASE_KEY = 'Snapshots'
function groupVersions(gameVersions: GameVersion[]) {
gameVersions = [...gameVersions].sort(
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(),
)
const getGroupKey = (v: string) => v.split('.').slice(0, 2).join('.')
const groups: Record<string, string[]> = {}
let currentGroupKey = getGroupKey(gameVersions.find((v) => v.major)?.version || '')
gameVersions.forEach((gameVersion) => {
if (gameVersion.version_type === 'release') {
currentGroupKey = getGroupKey(gameVersion.version)
if (!groups[currentGroupKey]) groups[currentGroupKey] = []
groups[currentGroupKey].push(gameVersion.version)
} else {
const key = `${currentGroupKey} ${DEV_RELEASE_KEY}`
if (!groups[key]) groups[key] = []
groups[key].push(gameVersion.version)
}
})
const sortedKeys = Object.keys(groups).sort(compareGroupKeys)
return sortedKeys.map((key) => ({
key,
versions: groups[key].sort((a, b) => compareVersions(b, a)),
isReleaseGroup: !key.includes(DEV_RELEASE_KEY),
}))
}
const getBaseVersion = (key: string) => key.split(' ')[0]
function compareVersions(a: string, b: string) {
const pa = a.split('.').map(Number)
const pb = b.split('.').map(Number)
for (let i = 0; i < 2; i++) {
const na = pa[i] || 0
const nb = pb[i] || 0
if (na > nb) return 1
if (na < nb) return -1
}
return 0
}
function compareGroupKeys(a: string, b: string) {
const aBase = getBaseVersion(a)
const bBase = getBaseVersion(b)
const versionSort = compareVersions(aBase, bBase)
if (versionSort !== 0) return -versionSort // descending
const isADev = a.includes(DEV_RELEASE_KEY)
const isBDev = b.includes(DEV_RELEASE_KEY)
if (isADev && !isBDev) return 1
if (!isADev && isBDev) return -1
return 0
}
function searchFilter(gameVersion: Labrinth.Tags.v2.GameVersion) {
return gameVersion.version.toLowerCase().includes(searchQuery.value.toLowerCase())
}
</script>