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

@@ -3,8 +3,12 @@
<Button
v-for="item in items"
:key="formatLabel(item)"
class="btn"
:class="{ selected: selected === item, capitalize: capitalize }"
class="btn !brightness-100 hover:!brightness-125"
:class="{
selected: selected === item,
capitalize: capitalize,
'!px-2.5 !py-1.5': size === 'small',
}"
@click="toggleItem(item)"
>
<CheckIcon v-if="selected === item" />
@@ -24,14 +28,17 @@ const props = withDefaults(
formatLabel?: (item: T) => string
neverEmpty?: boolean
capitalize?: boolean
size?: 'standard' | 'small'
}>(),
{
neverEmpty: true,
// Intentional any type, as this default should only be used for primitives (string or number)
formatLabel: (item) => item.toString(),
capitalize: true,
size: 'standard',
},
)
const selected = defineModel<T | null>()
// If one always has to be selected, default to the first one

View File

@@ -61,6 +61,7 @@
:placeholder="searchPlaceholder"
class=""
@keydown.stop="handleSearchKeydown"
@input="emit('searchInput', searchQuery)"
/>
</div>
</div>
@@ -107,7 +108,7 @@
</div>
<div v-else-if="searchQuery" class="p-4 mb-2 text-center text-sm text-secondary">
No results found
{{ noOptionsMessage }}
</div>
</div>
</Teleport>
@@ -168,6 +169,7 @@ const props = withDefaults(
extraPosition?: 'top' | 'bottom'
triggerClass?: string
forceDirection?: 'up' | 'down'
noOptionsMessage?: string
}>(),
{
placeholder: 'Select an option',
@@ -178,6 +180,7 @@ const props = withDefaults(
showChevron: true,
maxHeight: DEFAULT_MAX_HEIGHT,
extraPosition: 'bottom',
noOptionsMessage: 'No results found',
},
)
@@ -186,6 +189,7 @@ const emit = defineEmits<{
select: [option: DropdownOption<T>]
open: []
close: []
searchInput: [query: string]
}>()
const slots = useSlots()

View File

@@ -0,0 +1,122 @@
<template>
<label
:class="[
'flex flex-col items-center justify-center cursor-pointer border-2 border-dashed bg-surface-4 text-contrast transition-colors',
size === 'small' ? 'p-5' : 'p-12',
size === 'small' ? 'gap-2' : 'gap-4',
size === 'small' ? 'rounded-2xl' : 'rounded-3xl',
isDragOver ? 'border-purple' : 'border-surface-5',
]"
@dragover.prevent="onDragOver"
@dragleave.prevent="onDragLeave"
@drop.prevent="handleDrop"
>
<div
:class="[
'grid place-content-center text-brand border-brand border-solid border bg-highlight-green',
size === 'small' ? 'w-10 h-10' : 'h-14 w-14',
size === 'small' ? 'rounded-xl' : 'rounded-2xl',
]"
>
<FolderUpIcon
aria-hidden="true"
:class="['text-brand', size === 'small' ? 'w-6 h-6' : 'w-8 h-8']"
/>
</div>
<div class="flex flex-col items-center justify-center gap-1 text-contrast text-center">
<div class="text-contrast font-medium text-pretty">
{{ primaryPrompt }}
</div>
<span class="text-primary text-sm text-pretty">
{{ secondaryPrompt }}
</span>
</div>
<input
ref="fileInput"
type="file"
:multiple="multiple"
:accept="accept"
:disabled="disabled"
class="hidden"
@change="handleChange"
/>
</label>
</template>
<script setup lang="ts">
import { FolderUpIcon } from '@modrinth/assets'
import { fileIsValid } from '@modrinth/utils'
import { ref } from 'vue'
const fileInput = ref<HTMLInputElement | null>(null)
const emit = defineEmits<{
(e: 'change', files: File[]): void
}>()
const props = withDefaults(
defineProps<{
prompt?: string
primaryPrompt?: string | null
secondaryPrompt?: string | null
multiple?: boolean
accept?: string
maxSize?: number | null
shouldAlwaysReset?: boolean
disabled?: boolean
size?: 'small' | 'standard'
}>(),
{
prompt: 'Drag and drop files or click to browse',
primaryPrompt: 'Drag and drop files or click to browse',
secondaryPrompt: 'You can try to drag files or folder or click this area to select it',
size: 'standard',
},
)
const files = ref<File[]>([])
function addFiles(incoming: FileList, shouldNotReset = false) {
if (!shouldNotReset || props.shouldAlwaysReset) {
files.value = Array.from(incoming)
}
const validationOptions = {
maxSize: props.maxSize ?? undefined,
alertOnInvalid: true,
}
files.value = files.value.filter((file) => fileIsValid(file, validationOptions))
if (files.value.length > 0) {
emit('change', files.value)
}
if (fileInput.value) fileInput.value.value = ''
}
const isDragOver = ref(false)
function onDragOver() {
isDragOver.value = true
}
function onDragLeave() {
isDragOver.value = false
}
function handleDrop(e: DragEvent) {
isDragOver.value = false
if (!e.dataTransfer) return
addFiles(e.dataTransfer.files)
}
function handleChange(e: Event) {
const input = e.target as HTMLInputElement
if (!input.files) return
addFiles(input.files)
}
</script>

View File

@@ -107,7 +107,7 @@ label {
grid-gap: 0.5rem;
background-color: var(--color-button-bg);
border-radius: var(--radius-sm);
border: dashed 0.3rem var(--color-contrast);
border: dashed 2px var(--color-contrast);
cursor: pointer;
color: var(--color-contrast);
}

View File

@@ -1,5 +1,5 @@
<template>
<Modal ref="linkModal" header="Insert link">
<NewModal ref="linkModal" header="Insert link">
<div class="modal-insert">
<label class="label" for="insert-link-label">
<span class="label__title">Label</span>
@@ -59,8 +59,8 @@
>
</div>
</div>
</Modal>
<Modal ref="imageModal" header="Insert image">
</NewModal>
<NewModal ref="imageModal" header="Insert image">
<div class="modal-insert">
<label class="label" for="insert-image-alt">
<span class="label__title">Description (alt text)<span class="required">*</span></span>
@@ -147,8 +147,8 @@
</Button>
</div>
</div>
</Modal>
<Modal ref="videoModal" header="Insert YouTube video">
</NewModal>
<NewModal ref="videoModal" header="Insert YouTube video">
<div class="modal-insert">
<label class="label" for="insert-video-url">
<span class="label__title">YouTube video URL<span class="required">*</span></span>
@@ -201,7 +201,7 @@
</Button>
</div>
</div>
</Modal>
</NewModal>
<div class="resizable-textarea-wrapper">
<div class="editor-action-row">
<div class="editor-actions">
@@ -223,10 +223,10 @@
</Button>
</template>
</template>
</div>
<div class="preview">
<Toggle id="preview" v-model="previewMode" />
<label class="label" for="preview"> Preview </label>
<div class="preview">
<Toggle id="preview" v-model="previewMode" />
<label class="label" for="preview"> Preview </label>
</div>
</div>
</div>
<div ref="editorRef" :class="{ hide: previewMode }" />
@@ -292,11 +292,11 @@ import {
XIcon,
YouTubeIcon,
} from '@modrinth/assets'
import NewModal from '@modrinth/ui/src/components/modal/NewModal.vue'
import { markdownCommands, modrinthMarkdownEditorKeymap } from '@modrinth/utils/codemirror'
import { renderHighlightedString } from '@modrinth/utils/highlightjs'
import { type Component, computed, onBeforeUnmount, onMounted, ref, toRef, watch } from 'vue'
import Modal from '../modal/Modal.vue'
import Button from './Button.vue'
import Chips from './Chips.vue'
import FileInput from './FileInput.vue'
@@ -756,9 +756,9 @@ const videoMarkdown = computed(() => {
return ''
})
const linkModal = ref<InstanceType<typeof Modal> | null>(null)
const imageModal = ref<InstanceType<typeof Modal> | null>(null)
const videoModal = ref<InstanceType<typeof Modal> | null>(null)
const linkModal = ref<InstanceType<typeof NewModal> | null>(null)
const imageModal = ref<InstanceType<typeof NewModal> | null>(null)
const videoModal = ref<InstanceType<typeof NewModal> | null>(null)
function resetModalStates() {
linkText.value = ''

View File

@@ -0,0 +1,227 @@
<template>
<NewModal
ref="modal"
:scrollable="true"
max-content-height="72vh"
:on-hide="onModalHide"
:closable="true"
:close-on-click-outside="false"
>
<template #title>
<div class="flex flex-wrap items-center gap-1 text-secondary">
<span class="text-lg font-bold text-contrast sm:text-xl">{{ resolvedTitle }}</span>
</div>
</template>
<progress
v-if="currentStage?.nonProgressStage !== true"
:value="progressValue"
max="100"
class="w-full h-1 appearance-none border-none absolute top-0 left-0"
></progress>
<div class="sm:w-[512px]">
<component :is="currentStage?.stageContent" />
</div>
<template #actions>
<div
class="flex flex-col justify-end gap-2 sm:flex-row"
:class="leftButtonConfig || rightButtonConfig ? 'mt-4' : ''"
>
<ButtonStyled v-if="leftButtonConfig" type="outlined">
<button
class="!border-surface-5"
:disabled="leftButtonConfig.disabled"
@click="leftButtonConfig.onClick"
>
<component :is="leftButtonConfig.icon" />
{{ leftButtonConfig.label }}
</button>
</ButtonStyled>
<ButtonStyled v-if="rightButtonConfig" :color="rightButtonConfig.color">
<button :disabled="rightButtonConfig.disabled" @click="rightButtonConfig.onClick">
<component
:is="rightButtonConfig.icon"
v-if="rightButtonConfig.iconPosition === 'before'"
:class="rightButtonConfig.iconClass"
/>
{{ rightButtonConfig.label }}
<component
:is="rightButtonConfig.icon"
v-if="rightButtonConfig.iconPosition === 'after'"
:class="rightButtonConfig.iconClass"
/>
</button>
</ButtonStyled>
</div>
</template>
</NewModal>
</template>
<script lang="ts">
import { ButtonStyled, NewModal } from '@modrinth/ui'
import type { Component } from 'vue'
import { computed, ref, useTemplateRef } from 'vue'
export interface StageButtonConfig {
label?: string
icon?: Component | null
iconPosition?: 'before' | 'after'
color?: InstanceType<typeof ButtonStyled>['$props']['color']
disabled?: boolean
iconClass?: string | null
onClick?: () => void
}
export type MaybeCtxFn<T, R> = R | ((ctx: T) => R)
export interface StageConfigInput<T> {
id: string
stageContent: Component
title: MaybeCtxFn<T, string>
skip?: MaybeCtxFn<T, boolean>
nonProgressStage?: boolean
leftButtonConfig: MaybeCtxFn<T, StageButtonConfig | null>
rightButtonConfig: MaybeCtxFn<T, StageButtonConfig | null>
}
export function resolveCtxFn<T, R>(value: MaybeCtxFn<T, R>, ctx: T): R {
return typeof value === 'function' ? (value as (ctx: T) => R)(ctx) : value
}
</script>
<script setup lang="ts" generic="T">
const props = defineProps<{
stages: StageConfigInput<T>[]
context: T
}>()
const modal = useTemplateRef<InstanceType<typeof NewModal>>('modal')
const currentStageIndex = ref<number>(0)
function show() {
modal.value?.show()
}
function hide() {
modal.value?.hide()
}
const setStage = (indexOrId: number | string) => {
let index: number = 0
if (typeof indexOrId === 'number') {
index = indexOrId
if (index < 0 || index >= props.stages.length) return
} else {
index = props.stages.findIndex((stage) => stage.id === indexOrId)
if (index === -1) return
}
while (index < props.stages.length) {
const skip = props.stages[index]?.skip
if (!skip || !resolveCtxFn(skip, props.context)) break
index++
}
if (index < props.stages.length) {
currentStageIndex.value = index
}
}
const nextStage = () => {
if (currentStageIndex.value === -1) return
if (currentStageIndex.value >= props.stages.length - 1) return
let nextIndex = currentStageIndex.value + 1
while (nextIndex < props.stages.length) {
const skip = props.stages[nextIndex]?.skip
if (!skip || !resolveCtxFn(skip, props.context)) break
nextIndex++
}
if (nextIndex < props.stages.length) {
currentStageIndex.value = nextIndex
}
}
const prevStage = () => {
if (currentStageIndex.value <= 0) return
let prevIndex = currentStageIndex.value - 1
while (prevIndex >= 0) {
const skip = props.stages[prevIndex]?.skip
if (!skip || !resolveCtxFn(skip, props.context)) break
prevIndex--
}
if (prevIndex >= 0) {
currentStageIndex.value = prevIndex
}
}
const currentStage = computed(() => props.stages[currentStageIndex.value])
const resolvedTitle = computed(() => {
const stage = currentStage.value
if (!stage) return ''
return resolveCtxFn(stage.title, props.context)
})
const leftButtonConfig = computed(() => {
const stage = currentStage.value
if (!stage) return null
return resolveCtxFn(stage.leftButtonConfig, props.context)
})
const rightButtonConfig = computed(() => {
const stage = currentStage.value
if (!stage) return null
return resolveCtxFn(stage.rightButtonConfig, props.context)
})
const progressValue = computed(() => {
const isProgressStage = (stage: StageConfigInput<T>) => {
if (stage.nonProgressStage) return false
const skip = stage.skip ? resolveCtxFn(stage.skip, props.context) : false
return !skip
}
const completedCount = props.stages
.slice(0, currentStageIndex.value + 1)
.filter(isProgressStage).length
const totalCount = props.stages.filter(isProgressStage).length
return totalCount > 0 ? (completedCount / totalCount) * 100 : 0
})
const emit = defineEmits<{
(e: 'refresh-data' | 'hide'): void
}>()
function onModalHide() {
emit('hide')
}
defineExpose({
show,
hide,
setStage,
nextStage,
prevStage,
currentStageIndex,
})
</script>
<style scoped>
progress {
@apply bg-surface-3;
background-color: var(--surface-3, rgb(30, 30, 30));
}
progress::-webkit-progress-bar {
@apply bg-surface-3;
}
progress::-webkit-progress-value {
@apply bg-contrast;
}
progress::-moz-progress-bar {
@apply bg-contrast;
}
</style>

View File

@@ -22,23 +22,30 @@
}"
class="page-number-container"
>
<div v-if="item === '-'">
<GapIcon />
<div v-if="item === '-'" class="rotate-90 grid place-content-center">
<EllipsisVerticalIcon />
</div>
<ButtonStyled
v-else
circular
:color="page === item ? 'brand' : 'standard'"
:type="page === item ? 'standard' : 'transparent'"
:type="page === item ? 'highlight' : 'transparent'"
>
<a
v-if="linkFunction"
:href="linkFunction(item)"
:class="page === item ? '!text-brand' : ''"
@click.prevent="page !== item ? switchPage(item) : null"
>
{{ item }}
</a>
<button v-else @click="page !== item ? switchPage(item) : null">{{ item }}</button>
<button
v-else
:class="page === item ? '!text-brand' : ''"
@click="page !== item ? switchPage(item) : null"
>
{{ item }}
</button>
</ButtonStyled>
</div>
@@ -58,7 +65,7 @@
</div>
</template>
<script setup lang="ts">
import { ChevronLeftIcon, ChevronRightIcon, GapIcon } from '@modrinth/assets'
import { ChevronLeftIcon, ChevronRightIcon, EllipsisVerticalIcon } from '@modrinth/assets'
import { computed } from 'vue'
import ButtonStyled from './ButtonStyled.vue'

View File

@@ -19,6 +19,7 @@ export { default as CopyCode } from './CopyCode.vue'
export { default as DoubleIcon } from './DoubleIcon.vue'
export { default as DropArea } from './DropArea.vue'
export { default as DropdownSelect } from './DropdownSelect.vue'
export { default as DropzoneFileInput } from './DropzoneFileInput.vue'
export { default as EnvironmentIndicator } from './EnvironmentIndicator.vue'
export { default as ErrorInformationCard } from './ErrorInformationCard.vue'
export { default as FileInput } from './FileInput.vue'
@@ -32,6 +33,9 @@ export { default as JoinedButtons } from './JoinedButtons.vue'
export { default as LoadingIndicator } from './LoadingIndicator.vue'
export { default as ManySelect } from './ManySelect.vue'
export { default as MarkdownEditor } from './MarkdownEditor.vue'
export type { MaybeCtxFn, StageButtonConfig, StageConfigInput } from './MultiStageModal.vue'
export { default as MultiStageModal } from './MultiStageModal.vue'
export { resolveCtxFn } from './MultiStageModal.vue'
export { default as OptionGroup } from './OptionGroup.vue'
export type { Option as OverflowMenuOption } from './OverflowMenu.vue'
export { default as OverflowMenu } from './OverflowMenu.vue'

View File

@@ -313,7 +313,7 @@ function handleKeyDown(event: KeyboardEvent) {
box-shadow: 4px 4px 26px 10px rgba(0, 0, 0, 0.08);
max-height: calc(100% - 2 * var(--gap-lg));
max-width: min(var(--_max-width, 60rem), calc(100% - 2 * var(--gap-lg)));
overflow-y: auto;
overflow-y: hidden;
overflow-x: hidden;
width: fit-content;
pointer-events: auto;

View File

@@ -1,18 +1,44 @@
<template>
<div class="mb-3 flex flex-wrap gap-2">
<VersionFilterControl
ref="versionFilters"
:versions="versions"
:game-versions="gameVersions"
:base-id="`${baseId}-filter`"
@update:query="updateQuery"
/>
<Pagination
:page="currentPage"
class="ml-auto mt-auto"
:count="Math.ceil(filteredVersions.length / pageSize)"
@switch-page="switchPage"
/>
<div class="flex flex-col gap-3 mb-3">
<div class="flex flex-wrap justify-between gap-2">
<VersionFilterControl
ref="versionFilters"
:versions="versions"
:game-versions="gameVersions"
:base-id="`${baseId}-filter`"
@update:query="updateQuery"
/>
<ButtonStyled v-if="openModal" color="green">
<button @click="openModal"><PlusIcon /> Create version</button>
</ButtonStyled>
<Pagination
v-if="!openModal"
:page="currentPage"
class="mt-auto"
:count="Math.ceil(filteredVersions.length / pageSize)"
@switch-page="switchPage"
/>
</div>
<div
v-if="openModal && filteredVersions.length > pageSize"
class="flex flex-wrap justify-between items-center gap-2"
>
<span>
Showing {{ (currentPage - 1) * pageSize + 1 }} to
{{ Math.min(currentPage * pageSize, filteredVersions.length) }} of
{{ filteredVersions.length }}
</span>
<Pagination
:page="currentPage"
class="mt-auto"
:count="Math.ceil(filteredVersions.length / pageSize)"
@switch-page="switchPage"
/>
</div>
</div>
<div
v-if="versions.length > 0"
@@ -169,14 +195,15 @@
</div>
</template>
<script setup lang="ts">
import { CalendarIcon, DownloadIcon, StarIcon } from '@modrinth/assets'
import type { Labrinth } from '@modrinth/api-client'
import { CalendarIcon, DownloadIcon, PlusIcon, StarIcon } from '@modrinth/assets'
import { ButtonStyled } from '@modrinth/ui'
import {
formatBytes,
formatCategory,
formatNumber,
formatVersionsForDisplay,
type GameVersionTag,
type PlatformTag,
type Version,
} from '@modrinth/utils'
import { useVIntl } from '@vintl/vintl'
@@ -207,9 +234,10 @@ const props = withDefaults(
versions: VersionWithDisplayUrlEnding[]
showFiles?: boolean
currentMember?: boolean
loaders: PlatformTag[]
loaders: Labrinth.Tags.v2.Loader[]
gameVersions: GameVersionTag[]
versionLink?: (version: Version) => string
openModal?: () => void
}>(),
{
baseId: undefined,

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import { defineMessage, type MessageDescriptor, useVIntl } from '@vintl/vintl'
import { computed, ref, watch } from 'vue'
@@ -7,7 +8,7 @@ import LargeRadioButton from '../../../base/LargeRadioButton.vue'
const { formatMessage } = useVIntl()
const value = defineModel<string | undefined>({ required: true })
const value = defineModel<Labrinth.Projects.v3.Environment | undefined>({ required: true })
withDefaults(
defineProps<{
@@ -134,7 +135,7 @@ type SubOptionKey = ValidKeys<(typeof OUTER_OPTIONS)[keyof typeof OUTER_OPTIONS]
const currentOuterOption = ref<OuterOptionKey>()
const currentSubOption = ref<SubOptionKey>()
const computedOption = computed<string>(() => {
const computedOption = computed<Labrinth.Projects.v3.Environment>(() => {
switch (currentOuterOption.value) {
case 'client':
return 'client_only'
@@ -169,7 +170,7 @@ const computedOption = computed<string>(() => {
}
})
function loadEnvironmentValues(env?: EnvironmentV3) {
function loadEnvironmentValues(env?: Labrinth.Projects.v3.Environment) {
switch (env) {
case 'client_and_server':
currentOuterOption.value = 'client_and_server'