You've already forked AstralRinth
forked from didirus/AstralRinth
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:
@@ -20,20 +20,6 @@
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<ModerationProjectNags
|
||||
v-if="
|
||||
(currentMember && project.status === 'draft') ||
|
||||
tags.rejectedStatuses.includes(project.status)
|
||||
"
|
||||
:project="project"
|
||||
:versions="versions"
|
||||
:current-member="currentMember"
|
||||
:collapsed="collapsed"
|
||||
:route-name="routeName"
|
||||
:tags="tags"
|
||||
@toggle-collapsed="handleToggleCollapsed"
|
||||
@set-processing="handleSetProcessing"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -45,8 +31,6 @@ import { computed } from 'vue'
|
||||
|
||||
import { acceptTeamInvite, removeTeamMember } from '~/helpers/teams.js'
|
||||
|
||||
import ModerationProjectNags from './moderation/ModerationProjectNags.vue'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
|
||||
interface Tags {
|
||||
@@ -71,12 +55,9 @@ interface Props {
|
||||
currentMember?: Member | null
|
||||
allMembers?: Member[] | null
|
||||
isSettings?: boolean
|
||||
collapsed?: boolean
|
||||
routeName?: string
|
||||
auth: Auth
|
||||
tags: Tags
|
||||
setProcessing?: (processing: boolean) => void
|
||||
toggleCollapsed?: () => void
|
||||
updateMembers?: () => void | Promise<void>
|
||||
}
|
||||
|
||||
@@ -144,7 +125,6 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
allMembers: null,
|
||||
isSettings: false,
|
||||
collapsed: false,
|
||||
routeName: '',
|
||||
setProcessing: () => {},
|
||||
toggleCollapsed: () => {},
|
||||
updateMembers: async () => {},
|
||||
@@ -164,14 +144,6 @@ const showInvitation = computed<boolean>(() => {
|
||||
return false
|
||||
})
|
||||
|
||||
function handleToggleCollapsed(): void {
|
||||
if (props.toggleCollapsed) {
|
||||
props.toggleCollapsed()
|
||||
} else {
|
||||
emit('toggleCollapsed')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpdateMembers(): Promise<void> {
|
||||
if (props.updateMembers) {
|
||||
await props.updateMembers()
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<MultiStageModal ref="modal" :stages="ctx.stageConfigs" :context="ctx" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { injectProjectPageContext, MultiStageModal } from '@modrinth/ui'
|
||||
import type { ComponentExposed } from 'vue-component-type-helpers'
|
||||
|
||||
import {
|
||||
createManageVersionContext,
|
||||
provideManageVersionContext,
|
||||
} from '~/providers/version/manage-version-modal'
|
||||
|
||||
const modal = useTemplateRef<ComponentExposed<typeof MultiStageModal>>('modal')
|
||||
|
||||
const ctx = createManageVersionContext(modal)
|
||||
provideManageVersionContext(ctx)
|
||||
|
||||
const { newDraftVersion, setProjectType } = ctx
|
||||
|
||||
const { projectV2 } = injectProjectPageContext()
|
||||
|
||||
function showCreateVersionModal(version: Labrinth.Versions.v3.DraftVersion | null = null) {
|
||||
newDraftVersion(projectV2.value.id, version)
|
||||
setProjectType(projectV2.value)
|
||||
modal.value?.setStage(0)
|
||||
modal.value?.show()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show: showCreateVersionModal,
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-between gap-2 rounded-xl bg-button-bg px-4 py-1 text-button-text"
|
||||
>
|
||||
<div class="grid max-w-[75%] grid-cols-[auto_1fr_auto] items-center gap-2">
|
||||
<Avatar v-if="icon" :src="icon" alt="dependency-icon" size="20px" :no-shadow="true" />
|
||||
|
||||
<span v-tooltip="name || projectId" class="truncate font-semibold text-contrast">
|
||||
{{ name || 'Unknown Project' }}
|
||||
</span>
|
||||
|
||||
<TagItem class="shrink-0 border !border-solid border-surface-5">
|
||||
{{ dependencyType }}
|
||||
</TagItem>
|
||||
</div>
|
||||
|
||||
<span
|
||||
v-if="versionName"
|
||||
v-tooltip="versionName"
|
||||
class="max-w-[35%] truncate whitespace-nowrap font-medium"
|
||||
>
|
||||
{{ versionName }}
|
||||
</span>
|
||||
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<ButtonStyled size="standard" :circular="true">
|
||||
<button aria-label="Remove file" class="!shadow-none" @click="emitRemove">
|
||||
<XIcon aria-hidden="true" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { XIcon } from '@modrinth/assets'
|
||||
import { Avatar, ButtonStyled, TagItem } from '@modrinth/ui'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'fileTypeChange', type: string): void
|
||||
(e: 'remove'): void
|
||||
}>()
|
||||
|
||||
const { projectId, name, icon, dependencyType, versionName } = defineProps<{
|
||||
projectId: string
|
||||
name?: string
|
||||
icon?: string
|
||||
dependencyType: Labrinth.Versions.v2.DependencyType
|
||||
versionName?: string
|
||||
}>()
|
||||
|
||||
function emitRemove() {
|
||||
emit('remove')
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<span class="font-semibold text-contrast">Loaders <span class="text-red">*</span></span>
|
||||
|
||||
<Chips
|
||||
v-model="loaderGroup"
|
||||
:items="groupLabels"
|
||||
:never-empty="false"
|
||||
:capitalize="true"
|
||||
size="small"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="flex min-h-[150px] flex-1 flex-col gap-4 overflow-y-auto rounded-xl border border-solid border-surface-5 p-3"
|
||||
>
|
||||
<div v-if="groupedLoaders[loaderGroup].length" class="flex flex-col gap-1.5">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<TagItem
|
||||
v-for="loader in groupedLoaders[loaderGroup]"
|
||||
:key="`loader-${loader.name}`"
|
||||
:action="() => toggleLoader(loader.name)"
|
||||
class="border !border-solid !transition-all hover:bg-button-bgHover hover:no-underline"
|
||||
:class="
|
||||
selectedLoaders.includes(loader.name)
|
||||
? 'border-brand bg-highlight-green text-brand'
|
||||
: 'border-surface-5'
|
||||
"
|
||||
:style="`--_color: var(--color-platform-${loader.name})`"
|
||||
>
|
||||
<div v-html="loader.icon"></div>
|
||||
{{ formatCategory(loader.name) }}
|
||||
</TagItem>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span>Select one or more loaders this version supports.</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { Chips, TagItem } from '@modrinth/ui'
|
||||
import { formatCategory } from '@modrinth/utils'
|
||||
|
||||
const selectedLoaders = defineModel<string[]>({ default: [] })
|
||||
|
||||
const { loaders } = defineProps<{
|
||||
loaders: Labrinth.Tags.v2.Loader[]
|
||||
toggleLoader: (loader: string) => void
|
||||
}>()
|
||||
|
||||
const loaderGroup = ref<GroupLabels>('mods')
|
||||
|
||||
type GroupLabels = 'mods' | 'plugins' | 'packs' | 'shaders' | 'other'
|
||||
|
||||
const groupLabels: GroupLabels[] = ['mods', 'plugins', 'packs', 'shaders']
|
||||
|
||||
function groupLoaders(loaders: Labrinth.Tags.v2.Loader[]) {
|
||||
const groups: Record<GroupLabels, Labrinth.Tags.v2.Loader[]> = {
|
||||
mods: [],
|
||||
plugins: [],
|
||||
packs: [],
|
||||
shaders: [],
|
||||
other: [],
|
||||
}
|
||||
|
||||
const MOD_SORT = [
|
||||
'fabric',
|
||||
'neoforge',
|
||||
'forge',
|
||||
'quilt',
|
||||
'liteloader',
|
||||
'rift',
|
||||
'ornithe',
|
||||
'nilloader',
|
||||
'risugami',
|
||||
'legacy-fabric',
|
||||
'bta-babric',
|
||||
'babric',
|
||||
'modloader',
|
||||
'java-agent',
|
||||
]
|
||||
|
||||
const PLUGIN_SORT = [
|
||||
'paper',
|
||||
'purpur',
|
||||
'spigot',
|
||||
'bukkit',
|
||||
'sponge',
|
||||
'folia',
|
||||
'bungeecord',
|
||||
'velocity',
|
||||
'waterfall',
|
||||
'geyser',
|
||||
]
|
||||
|
||||
const SHADER_SORT = ['optifine', 'iris', 'canvas', 'vanilla']
|
||||
const PACKS_SORT = ['minecraft', 'datapack']
|
||||
|
||||
for (const loader of loaders) {
|
||||
const name = loader.name.toLowerCase()
|
||||
if (PACKS_SORT.includes(name)) groups.packs.push(loader)
|
||||
else if (SHADER_SORT.includes(name)) groups.shaders.push(loader)
|
||||
else if (PLUGIN_SORT.includes(name)) groups.plugins.push(loader)
|
||||
else if (MOD_SORT.includes(name)) groups.mods.push(loader)
|
||||
else groups.other.push(loader)
|
||||
}
|
||||
|
||||
const sortByOrder = (arr: any[], order: string[]) =>
|
||||
arr.sort((a, b) => order.indexOf(a.name) - order.indexOf(b.name))
|
||||
|
||||
sortByOrder(groups.mods, MOD_SORT)
|
||||
sortByOrder(groups.plugins, PLUGIN_SORT)
|
||||
sortByOrder(groups.shaders, SHADER_SORT)
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
const groupedLoaders = computed(() => groupLoaders(loaders))
|
||||
</script>
|
||||
@@ -0,0 +1,208 @@
|
||||
<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>
|
||||
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<Combobox
|
||||
v-model="projectId"
|
||||
placeholder="Select project"
|
||||
:options="options"
|
||||
:searchable="true"
|
||||
search-placeholder="Search by name, slug, or paste ID..."
|
||||
:no-options-message="searchLoading ? 'Loading...' : 'No results found'"
|
||||
@search-input="(query) => handleSearch(query)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Combobox, injectModrinthClient, injectNotificationManager } from '@modrinth/ui'
|
||||
import type { DropdownOption } from '@modrinth/ui/src/components/base/Combobox.vue'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import { defineAsyncComponent, h } from 'vue'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const projectId = defineModel<string>()
|
||||
|
||||
const searchLoading = ref(false)
|
||||
const options = ref<DropdownOption<string>[]>([])
|
||||
|
||||
const { labrinth } = injectModrinthClient()
|
||||
|
||||
const search = async (query: string) => {
|
||||
if (!query.trim()) {
|
||||
searchLoading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const results = await labrinth.projects_v2.search({
|
||||
query: query,
|
||||
limit: 20,
|
||||
facets: [['project_type:mod']],
|
||||
})
|
||||
|
||||
options.value = results.hits.map((hit) => ({
|
||||
label: hit.title,
|
||||
value: hit.project_id,
|
||||
icon: defineAsyncComponent(() =>
|
||||
Promise.resolve({
|
||||
setup: () => () =>
|
||||
h('img', {
|
||||
src: hit.icon_url,
|
||||
alt: hit.title,
|
||||
class: 'h-5 w-5 rounded',
|
||||
}),
|
||||
}),
|
||||
),
|
||||
}))
|
||||
} catch (error: any) {
|
||||
addNotification({
|
||||
title: 'An error occurred',
|
||||
text: error.data ? error.data.description : error,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
searchLoading.value = false
|
||||
}
|
||||
|
||||
const throttledSearch = useDebounceFn(search, 500)
|
||||
|
||||
const handleSearch = async (query: string) => {
|
||||
searchLoading.value = true
|
||||
await throttledSearch(query)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div v-if="visibleDependencies.length" class="flex flex-col gap-4">
|
||||
<span class="font-semibold text-contrast">Suggested dependencies</span>
|
||||
<div class="flex flex-col gap-2">
|
||||
<template v-for="(dependency, index) in visibleDependencies">
|
||||
<SuggestedDependency
|
||||
v-if="dependency"
|
||||
:key="index"
|
||||
:project-id="dependency.project_id"
|
||||
:name="dependency.name"
|
||||
:icon="dependency.icon"
|
||||
:dependency-type="dependency.dependency_type"
|
||||
:version-name="dependency.versionName"
|
||||
@on-add-suggestion="
|
||||
() =>
|
||||
handleAddSuggestion({
|
||||
dependency_type: dependency.dependency_type,
|
||||
project_id: dependency.project_id,
|
||||
version_id: dependency.version_id,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
|
||||
import { injectManageVersionContext } from '~/providers/version/manage-version-modal'
|
||||
|
||||
import SuggestedDependency from './SuggestedDependency.vue'
|
||||
|
||||
export interface SuggestedDependency extends Labrinth.Versions.v3.Dependency {
|
||||
icon?: string
|
||||
name?: string
|
||||
versionName?: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
suggestedDependencies: SuggestedDependency[]
|
||||
}>()
|
||||
|
||||
const { draftVersion } = injectManageVersionContext()
|
||||
|
||||
const visibleDependencies = computed<SuggestedDependency[]>(() =>
|
||||
props.suggestedDependencies
|
||||
.filter(
|
||||
(dep) =>
|
||||
!draftVersion.value.dependencies?.some(
|
||||
(d) => d.project_id === dep.project_id && d.version_id === dep.version_id,
|
||||
),
|
||||
)
|
||||
.sort((a, b) => (a.name || '').localeCompare(b.name || '')),
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'onAddSuggestion', dependency: Labrinth.Versions.v3.Dependency): void
|
||||
}>()
|
||||
|
||||
function handleAddSuggestion(dependency: Labrinth.Versions.v3.Dependency) {
|
||||
emit('onAddSuggestion', dependency)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-between gap-2 rounded-xl bg-button-bg px-4 py-1 text-button-text"
|
||||
>
|
||||
<div class="grid max-w-[75%] grid-cols-[auto_1fr_auto] items-center gap-2">
|
||||
<Avatar v-if="icon" :src="icon" alt="dependency-icon" size="20px" :no-shadow="true" />
|
||||
|
||||
<span v-tooltip="name || 'Unknown Project'" class="truncate font-semibold text-contrast">
|
||||
{{ name || 'Unknown Project' }}
|
||||
</span>
|
||||
|
||||
<TagItem class="shrink-0 border !border-solid border-surface-5">
|
||||
{{ dependencyType }}
|
||||
</TagItem>
|
||||
</div>
|
||||
|
||||
<span
|
||||
v-if="versionName"
|
||||
v-tooltip="versionName"
|
||||
class="max-w-[35%] truncate whitespace-nowrap font-medium"
|
||||
>
|
||||
{{ versionName }}
|
||||
</span>
|
||||
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<ButtonStyled size="standard" :circular="true">
|
||||
<button aria-label="Add dependency" class="!shadow-none" @click="emitAddSuggestion">
|
||||
<PlusIcon aria-hidden="true" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { PlusIcon } from '@modrinth/assets'
|
||||
import { Avatar, ButtonStyled, TagItem } from '@modrinth/ui'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'onAddSuggestion'): void
|
||||
}>()
|
||||
|
||||
const { name, icon, dependencyType, versionName } = defineProps<{
|
||||
name?: string
|
||||
icon?: string
|
||||
dependencyType: Labrinth.Versions.v2.DependencyType
|
||||
versionName?: string
|
||||
}>()
|
||||
|
||||
function emitAddSuggestion() {
|
||||
emit('onAddSuggestion')
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-between gap-2 rounded-xl bg-button-bg px-4 py-2 text-button-text"
|
||||
>
|
||||
<div class="flex items-center gap-2 overflow-hidden">
|
||||
<div class="grid h-5 min-h-5 w-5 min-w-5 place-content-center rounded-full bg-green">
|
||||
<CheckIcon class="text-sm text-black" />
|
||||
</div>
|
||||
<span v-tooltip="name" class="overflow-hidden text-ellipsis whitespace-nowrap font-medium">
|
||||
{{ name }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
<template v-if="!isPrimary">
|
||||
<div class="w-36">
|
||||
<Combobox
|
||||
v-model="selectedType"
|
||||
:searchable="false"
|
||||
class="rounded-xl border border-solid border-surface-5 text-sm"
|
||||
:options="versionTypes"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:allow-empty="false"
|
||||
@update:model-value="emitFileTypeChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ButtonStyled v-if="onRemove" size="standard" :circular="true">
|
||||
<button aria-label="Remove file" class="!shadow-none" @click="onRemove">
|
||||
<XIcon aria-hidden="true" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="isPrimary" size="standard" :circular="true">
|
||||
<button
|
||||
v-tooltip="
|
||||
editingVersion
|
||||
? 'Primary file cannot be changed after version is uploaded'
|
||||
: 'Replace primary file'
|
||||
"
|
||||
aria-label="Change primary file"
|
||||
class="!shadow-none"
|
||||
:disabled="editingVersion"
|
||||
@click="primaryFileInput?.click()"
|
||||
>
|
||||
<ArrowLeftRightIcon aria-hidden="true" />
|
||||
<input
|
||||
ref="primaryFileInput"
|
||||
class="hidden"
|
||||
type="file"
|
||||
:accept="acceptFileFromProjectType(projectV2.project_type)"
|
||||
:disabled="editingVersion"
|
||||
@change="
|
||||
(e) => {
|
||||
emit('setPrimaryFile', (e.target as HTMLInputElement)?.files?.[0])
|
||||
;(e.target as HTMLInputElement).value = ''
|
||||
}
|
||||
"
|
||||
/>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { ArrowLeftRightIcon, CheckIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, Combobox, injectProjectPageContext } from '@modrinth/ui'
|
||||
import type { DropdownOption } from '@modrinth/ui/src/components/base/Combobox.vue'
|
||||
import { acceptFileFromProjectType } from '@modrinth/utils'
|
||||
|
||||
const { projectV2 } = injectProjectPageContext()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'setPrimaryFile', file?: File): void
|
||||
(e: 'setFileType', type: Labrinth.Versions.v3.FileType): void
|
||||
}>()
|
||||
|
||||
const { name, isPrimary, onRemove, initialFileType, editingVersion } = defineProps<{
|
||||
name: string
|
||||
isPrimary: boolean
|
||||
onRemove?: () => void
|
||||
initialFileType?: Labrinth.Versions.v3.FileType | 'primary'
|
||||
editingVersion: boolean
|
||||
}>()
|
||||
|
||||
const selectedType = ref<Labrinth.Versions.v3.FileType | 'primary'>(initialFileType || 'unknown')
|
||||
const primaryFileInput = ref<HTMLInputElement>()
|
||||
|
||||
const versionTypes = [
|
||||
!editingVersion && { class: 'text-sm', value: 'primary', label: 'Primary' },
|
||||
{ class: 'text-sm', value: 'unknown', label: 'Other' },
|
||||
{ class: 'text-sm', value: 'required-resource-pack', label: 'Required RP' },
|
||||
{ class: 'text-sm', value: 'optional-resource-pack', label: 'Optional RP' },
|
||||
{ class: 'text-sm', value: 'sources-jar', label: 'Sources JAR' },
|
||||
{ class: 'text-sm', value: 'dev-jar', label: 'Dev JAR' },
|
||||
{ class: 'text-sm', value: 'javadoc-jar', label: 'Javadoc JAR' },
|
||||
{ class: 'text-sm', value: 'signature', label: 'Signature' },
|
||||
].filter(Boolean) as DropdownOption<Labrinth.Versions.v3.FileType | 'primary'>[]
|
||||
|
||||
function emitFileTypeChange() {
|
||||
if (selectedType.value === 'primary') emit('setPrimaryFile')
|
||||
else emit('setFileType', selectedType.value)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<MarkdownEditor
|
||||
v-model="draftVersion.changelog"
|
||||
:on-image-upload="onImageUpload"
|
||||
:max-height="500"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { MarkdownEditor } from '@modrinth/ui'
|
||||
|
||||
import { useImageUpload } from '~/composables/image-upload.ts'
|
||||
import { injectManageVersionContext } from '~/providers/version/manage-version-modal'
|
||||
|
||||
const { draftVersion } = injectManageVersionContext()
|
||||
|
||||
async function onImageUpload(file: File) {
|
||||
const response = await useImageUpload(file, { context: 'version' })
|
||||
return response.url
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,283 @@
|
||||
<template>
|
||||
<div class="flex w-full max-w-full flex-col gap-6">
|
||||
<div class="flex flex-col gap-4">
|
||||
<span class="font-semibold text-contrast">Add dependency</span>
|
||||
<div class="flex flex-col gap-3 rounded-2xl border border-solid border-surface-5 p-4">
|
||||
<div class="grid gap-2.5">
|
||||
<span class="font-semibold text-contrast">Project <span class="text-red">*</span></span>
|
||||
<ModSelect v-model="newDependencyProjectId" />
|
||||
</div>
|
||||
|
||||
<template v-if="newDependencyProjectId">
|
||||
<div class="grid gap-2.5">
|
||||
<span class="font-semibold text-contrast"> Version </span>
|
||||
<Combobox
|
||||
v-model="newDependencyVersionId"
|
||||
placeholder="Select version"
|
||||
:options="[{ label: 'Any version', value: null }, ...newDependencyVersions]"
|
||||
:searchable="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2.5">
|
||||
<span class="font-semibold text-contrast"> Dependency relation </span>
|
||||
<Combobox
|
||||
v-model="newDependencyType"
|
||||
placeholder="Select dependency type"
|
||||
:options="[
|
||||
{ label: 'Required', value: 'required' },
|
||||
{ label: 'Optional', value: 'optional' },
|
||||
{ label: 'Incompatible', value: 'incompatible' },
|
||||
{ label: 'Embedded', value: 'embedded' },
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ButtonStyled>
|
||||
<button
|
||||
class="self-start"
|
||||
:disabled="!newDependencyProjectId"
|
||||
@click="
|
||||
() =>
|
||||
addDependency(
|
||||
toRaw({
|
||||
project_id: newDependencyProjectId,
|
||||
version_id: newDependencyVersionId || undefined,
|
||||
dependency_type: newDependencyType,
|
||||
}),
|
||||
)
|
||||
"
|
||||
>
|
||||
Add Dependency
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SuggestedDependencies
|
||||
:suggested-dependencies="suggestedDependencies"
|
||||
@on-add-suggestion="handleAddSuggestedDependency"
|
||||
/>
|
||||
|
||||
<div v-if="addedDependencies.length" class="flex flex-col gap-4">
|
||||
<span class="font-semibold text-contrast">Added dependencies</span>
|
||||
<div class="5 flex flex-col gap-2">
|
||||
<template v-for="(dependency, index) in addedDependencies">
|
||||
<AddedDependencyRow
|
||||
v-if="dependency"
|
||||
:key="index"
|
||||
:project-id="dependency.projectId"
|
||||
:name="dependency.name"
|
||||
:icon="dependency.icon"
|
||||
:dependency-type="dependency.dependencyType"
|
||||
:version-name="dependency.versionName"
|
||||
@remove="() => removeDependency(index)"
|
||||
/>
|
||||
</template>
|
||||
<span v-if="!addedDependencies.length"> No dependencies added. </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Combobox,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
injectProjectPageContext,
|
||||
} from '@modrinth/ui'
|
||||
import type { DropdownOption } from '@modrinth/ui/src/components/base/Combobox.vue'
|
||||
|
||||
import ModSelect from '~/components/ui/create-project-version/components/ModSelect.vue'
|
||||
import { injectManageVersionContext } from '~/providers/version/manage-version-modal'
|
||||
|
||||
import AddedDependencyRow from '../components/AddedDependencyRow.vue'
|
||||
import SuggestedDependencies from '../components/SuggestedDependencies/SuggestedDependencies.vue'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const { labrinth } = injectModrinthClient()
|
||||
|
||||
const errorNotification = (err: any) => {
|
||||
addNotification({
|
||||
title: 'An error occurred',
|
||||
text: err.data ? err.data.description : err,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
const newDependencyProjectId = ref<string>()
|
||||
const newDependencyType = ref<Labrinth.Versions.v2.DependencyType>('required')
|
||||
const newDependencyVersionId = ref<string | null>(null)
|
||||
|
||||
const newDependencyVersions = ref<DropdownOption<string>[]>([])
|
||||
|
||||
const projectsFetchLoading = ref(false)
|
||||
const suggestedDependencies = ref<
|
||||
Array<Labrinth.Versions.v3.Dependency & { name?: string; icon?: string; versionName?: string }>
|
||||
>([])
|
||||
|
||||
// reset to defaults when select different project
|
||||
watch(newDependencyProjectId, async () => {
|
||||
newDependencyVersionId.value = null
|
||||
newDependencyType.value = 'required'
|
||||
|
||||
if (!newDependencyProjectId.value) {
|
||||
newDependencyVersions.value = []
|
||||
} else {
|
||||
try {
|
||||
const versions = await labrinth.versions_v3.getProjectVersions(newDependencyProjectId.value)
|
||||
newDependencyVersions.value = versions.map((version) => ({
|
||||
label: version.name,
|
||||
value: version.id,
|
||||
}))
|
||||
} catch (error: any) {
|
||||
errorNotification(error)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const { draftVersion, dependencyProjects, dependencyVersions, getProject, getVersion } =
|
||||
injectManageVersionContext()
|
||||
const { projectV2: project } = injectProjectPageContext()
|
||||
|
||||
const getSuggestedDependencies = async () => {
|
||||
try {
|
||||
suggestedDependencies.value = []
|
||||
|
||||
if (!draftVersion.value.game_versions?.length || !draftVersion.value.loaders?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const versions = await labrinth.versions_v3.getProjectVersions(project.value.id, {
|
||||
loaders: draftVersion.value.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 ${project.value.id}:`, 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) {
|
||||
errorNotification(error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getSuggestedDependencies()
|
||||
})
|
||||
|
||||
watch(
|
||||
draftVersion,
|
||||
async (draftVersion) => {
|
||||
const deps = draftVersion.dependencies || []
|
||||
|
||||
for (const dep of deps) {
|
||||
if (dep?.project_id) {
|
||||
try {
|
||||
await getProject(dep.project_id)
|
||||
} catch (error: any) {
|
||||
errorNotification(error)
|
||||
}
|
||||
}
|
||||
|
||||
if (dep?.version_id) {
|
||||
try {
|
||||
await getVersion(dep.version_id)
|
||||
} catch (error: any) {
|
||||
errorNotification(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
projectsFetchLoading.value = false
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
)
|
||||
|
||||
const addedDependencies = computed(() =>
|
||||
(draftVersion.value.dependencies || [])
|
||||
.map((dep) => {
|
||||
if (!dep.project_id) return null
|
||||
|
||||
const dependencyProject = dependencyProjects.value[dep.project_id]
|
||||
const versionName = dependencyVersions.value[dep.version_id || '']?.name ?? ''
|
||||
|
||||
if (!dependencyProject && projectsFetchLoading.value) return null
|
||||
|
||||
return {
|
||||
projectId: dep.project_id,
|
||||
name: dependencyProject?.name,
|
||||
icon: dependencyProject?.icon_url,
|
||||
dependencyType: dep.dependency_type,
|
||||
versionName,
|
||||
}
|
||||
})
|
||||
.filter(Boolean),
|
||||
)
|
||||
|
||||
const addDependency = (dependency: Labrinth.Versions.v3.Dependency) => {
|
||||
if (!draftVersion.value.dependencies) draftVersion.value.dependencies = []
|
||||
|
||||
// already added
|
||||
if (
|
||||
draftVersion.value.dependencies.find(
|
||||
(d) => d.project_id === dependency.project_id && d.version_id === dependency.version_id,
|
||||
)
|
||||
) {
|
||||
addNotification({
|
||||
title: 'Dependency already added',
|
||||
text: 'You cannot add the same dependency twice.',
|
||||
type: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
projectsFetchLoading.value = true
|
||||
draftVersion.value.dependencies.push(dependency)
|
||||
newDependencyProjectId.value = undefined
|
||||
}
|
||||
|
||||
const removeDependency = (index: number) => {
|
||||
if (!draftVersion.value.dependencies) return
|
||||
draftVersion.value.dependencies.splice(index, 1)
|
||||
}
|
||||
|
||||
const handleAddSuggestedDependency = (dependency: Labrinth.Versions.v3.Dependency) => {
|
||||
draftVersion.value.dependencies?.push({
|
||||
project_id: dependency.project_id,
|
||||
version_id: dependency.version_id,
|
||||
dependency_type: dependency.dependency_type,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="font-semibold text-contrast">
|
||||
Version type <span class="text-red">*</span>
|
||||
</span>
|
||||
<Chips
|
||||
v-model="draftVersion.version_type"
|
||||
:items="['release', 'alpha', 'beta']"
|
||||
:never-empty="true"
|
||||
:capitalize="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="font-semibold text-contrast">
|
||||
Version number <span class="text-red">*</span>
|
||||
</span>
|
||||
<input
|
||||
id="version-number"
|
||||
v-model="draftVersion.version_number"
|
||||
placeholder="Enter version number, e.g. 1.2.3-alpha.1"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
maxlength="32"
|
||||
/>
|
||||
<span> The version number differentiates this specific version from others. </span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="font-semibold text-contrast"> Version subtitle </span>
|
||||
<input
|
||||
id="version-number"
|
||||
v-model="draftVersion.name"
|
||||
placeholder="Enter subtitle..."
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
maxlength="32"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template v-if="!noLoadersProject && (inferredVersionData?.loaders?.length || editingVersion)">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-semibold text-contrast">
|
||||
{{ usingDetectedLoaders ? 'Detected loaders' : 'Loaders' }}
|
||||
</span>
|
||||
|
||||
<ButtonStyled type="transparent" size="standard">
|
||||
<button
|
||||
v-tooltip="isModpack ? 'Modpack versions cannot be edited' : undefined"
|
||||
:disabled="isModpack"
|
||||
@click="editLoaders"
|
||||
>
|
||||
<EditIcon />
|
||||
Edit
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-col gap-1.5 gap-y-4 rounded-xl border border-solid border-surface-5 p-3 py-4"
|
||||
>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<template
|
||||
v-for="loader in draftVersion.loaders.map((selectedLoader) =>
|
||||
loaders.find((loader) => selectedLoader === loader.name),
|
||||
)"
|
||||
>
|
||||
<TagItem
|
||||
v-if="loader"
|
||||
:key="`loader-${loader.name}`"
|
||||
class="border !border-solid border-surface-5 hover:no-underline"
|
||||
:style="`--_color: var(--color-platform-${loader.name})`"
|
||||
>
|
||||
<div v-html="loader.icon"></div>
|
||||
{{ formatCategory(loader.name) }}
|
||||
</TagItem>
|
||||
</template>
|
||||
|
||||
<span v-if="!draftVersion.loaders.length">No loaders selected.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="inferredVersionData?.game_versions?.length || editingVersion">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-semibold text-contrast">
|
||||
{{ usingDetectedVersions ? 'Detected versions' : 'Versions' }}
|
||||
</span>
|
||||
|
||||
<ButtonStyled type="transparent" size="standard">
|
||||
<button
|
||||
v-tooltip="isModpack ? 'Modpack versions cannot be edited' : undefined"
|
||||
:disabled="isModpack"
|
||||
@click="editVersions"
|
||||
>
|
||||
<EditIcon />
|
||||
Edit
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-col gap-1.5 gap-y-4 rounded-xl border border-solid border-surface-5 p-3 py-4"
|
||||
>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<TagItem
|
||||
v-for="version in draftVersion.game_versions"
|
||||
:key="version"
|
||||
class="border !border-solid border-surface-5 hover:no-underline"
|
||||
>
|
||||
{{ version }}
|
||||
</TagItem>
|
||||
|
||||
<span v-if="!draftVersion.game_versions.length">No versions selected.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template
|
||||
v-if="
|
||||
!noEnvironmentProject &&
|
||||
((!editingVersion && inferredVersionData?.environment) ||
|
||||
(editingVersion && draftVersion.environment))
|
||||
"
|
||||
>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-semibold text-contrast"> Environment </span>
|
||||
|
||||
<ButtonStyled type="transparent" size="standard">
|
||||
<button @click="editEnvironment">
|
||||
<EditIcon />
|
||||
Edit
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5 gap-y-4 rounded-xl bg-surface-2 p-3 py-4">
|
||||
<div v-if="draftVersion.environment" class="flex flex-col gap-1">
|
||||
<div class="font-semibold text-contrast">
|
||||
{{ environmentCopy.title }}
|
||||
</div>
|
||||
<div class="text-sm font-medium">{{ environmentCopy.description }}</div>
|
||||
</div>
|
||||
|
||||
<span v-else class="text-sm font-medium">No environment has been set.</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { EditIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, Chips, TagItem } from '@modrinth/ui'
|
||||
import { formatCategory } from '@modrinth/utils'
|
||||
|
||||
import { useGeneratedState } from '~/composables/generated'
|
||||
import { injectManageVersionContext } from '~/providers/version/manage-version-modal'
|
||||
|
||||
const {
|
||||
draftVersion,
|
||||
inferredVersionData,
|
||||
projectType,
|
||||
editingVersion,
|
||||
noLoadersProject,
|
||||
noEnvironmentProject,
|
||||
modal,
|
||||
} = injectManageVersionContext()
|
||||
|
||||
const generatedState = useGeneratedState()
|
||||
const loaders = computed(() => generatedState.value.loaders)
|
||||
const isModpack = computed(() => projectType.value === 'modpack')
|
||||
|
||||
const editLoaders = () => {
|
||||
modal.value?.setStage('edit-loaders')
|
||||
}
|
||||
const editVersions = () => {
|
||||
modal.value?.setStage('edit-mc-versions')
|
||||
}
|
||||
const editEnvironment = () => {
|
||||
modal.value?.setStage('edit-environment')
|
||||
}
|
||||
|
||||
const usingDetectedVersions = computed(() => {
|
||||
if (!inferredVersionData.value?.game_versions) return false
|
||||
|
||||
const versionsMatch =
|
||||
draftVersion.value.game_versions.length === inferredVersionData.value.game_versions.length &&
|
||||
draftVersion.value.game_versions.every((version) =>
|
||||
inferredVersionData.value?.game_versions?.includes(version),
|
||||
)
|
||||
|
||||
return versionsMatch
|
||||
})
|
||||
|
||||
const usingDetectedLoaders = computed(() => {
|
||||
if (!inferredVersionData.value?.loaders) return false
|
||||
|
||||
const loadersMatch =
|
||||
draftVersion.value.loaders.length === inferredVersionData.value.loaders.length &&
|
||||
draftVersion.value.loaders.every((loader) =>
|
||||
inferredVersionData.value?.loaders?.includes(loader),
|
||||
)
|
||||
|
||||
return loadersMatch
|
||||
})
|
||||
|
||||
const environmentCopy = computed(() => {
|
||||
const emptyMessage = {
|
||||
title: 'No environment set',
|
||||
description: 'The environment for this version has not been specified.',
|
||||
}
|
||||
if (!draftVersion.value.environment) return emptyMessage
|
||||
|
||||
const envCopy: Record<string, { title: string; description: string }> = {
|
||||
client_only: {
|
||||
title: 'Client-side only',
|
||||
description: 'All functionality is done client-side and is compatible with vanilla servers.',
|
||||
},
|
||||
server_only: {
|
||||
title: 'Server-side only',
|
||||
description: 'All functionality is done server-side and is compatible with vanilla clients.',
|
||||
},
|
||||
singleplayer_only: {
|
||||
title: 'Singleplayer only',
|
||||
description: 'Only functions in Singleplayer or when not connected to a Multiplayer server.',
|
||||
},
|
||||
dedicated_server_only: {
|
||||
title: 'Server-side only',
|
||||
description: 'All functionality is done server-side and is compatible with vanilla clients.',
|
||||
},
|
||||
client_and_server: {
|
||||
title: 'Client and server',
|
||||
description: 'Has some functionality on both the client and server, even if only partially.',
|
||||
},
|
||||
client_only_server_optional: {
|
||||
title: 'Client and server',
|
||||
description: 'Has some functionality on both the client and server, even if only partially.',
|
||||
},
|
||||
server_only_client_optional: {
|
||||
title: 'Client and server',
|
||||
description: 'Has some functionality on both the client and server, even if only partially.',
|
||||
},
|
||||
client_or_server: {
|
||||
title: 'Client and server',
|
||||
description: 'Has some functionality on both the client and server, even if only partially.',
|
||||
},
|
||||
client_or_server_prefers_both: {
|
||||
title: 'Client and server',
|
||||
description: 'Has some functionality on both the client and server, even if only partially.',
|
||||
},
|
||||
unknown: {
|
||||
title: 'Unknown environment',
|
||||
description: 'The environment for this version could not be determined.',
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
envCopy[draftVersion.value.environment] || {
|
||||
title: 'Unknown environment',
|
||||
description: `The environment: "${draftVersion.value.environment}" is not recognized.`,
|
||||
}
|
||||
)
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<ProjectSettingsEnvSelector v-model="draftVersion.environment" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ProjectSettingsEnvSelector } from '@modrinth/ui'
|
||||
|
||||
import { injectManageVersionContext } from '~/providers/version/manage-version-modal'
|
||||
|
||||
const { draftVersion } = injectManageVersionContext()
|
||||
</script>
|
||||
@@ -0,0 +1,240 @@
|
||||
<template>
|
||||
<div class="flex w-full flex-col gap-4">
|
||||
<template v-if="!(filesToAdd.length || draftVersion.existing_files?.length)">
|
||||
<DropzoneFileInput
|
||||
aria-label="Upload file"
|
||||
multiple
|
||||
:accept="acceptFileFromProjectType(projectV2.project_type)"
|
||||
:max-size="524288000"
|
||||
@change="handleNewFiles"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-base font-semibold text-contrast">Primary file</span>
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<VersionFileRow
|
||||
v-if="primaryFile"
|
||||
:key="primaryFile.name"
|
||||
:name="primaryFile.name"
|
||||
:is-primary="true"
|
||||
:editing-version="editingVersion"
|
||||
:on-remove="undefined"
|
||||
@set-primary-file="
|
||||
(file) => {
|
||||
if (file && !editingVersion) filesToAdd[0] = { file }
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<span>
|
||||
The primary file is the default file a user downloads when installing the project.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-2">
|
||||
<Admonition v-if="hasSupplementaryFiles" type="warning">
|
||||
{{ formatMessage(messages.addFilesAdmonition) }}
|
||||
</Admonition>
|
||||
|
||||
<span class="text-base font-semibold text-contrast">Supplementary files</span>
|
||||
|
||||
<DropzoneFileInput
|
||||
aria-label="Upload additional file"
|
||||
multiple
|
||||
:accept="acceptFileFromProjectType(projectV2.project_type)"
|
||||
:max-size="524288000"
|
||||
size="small"
|
||||
:primary-prompt="null"
|
||||
secondary-prompt="Drag and drop files or click to browse"
|
||||
@change="handleNewFiles"
|
||||
/>
|
||||
|
||||
<div v-if="hasSupplementaryFiles" class="flex flex-col gap-2.5">
|
||||
<VersionFileRow
|
||||
v-for="versionFile in supplementaryExistingFiles"
|
||||
:key="versionFile.filename"
|
||||
:name="versionFile.filename"
|
||||
:is-primary="false"
|
||||
:initial-file-type="versionFile.file_type"
|
||||
:editing-version="editingVersion"
|
||||
:on-remove="() => handleRemoveExistingFile(versionFile.hashes.sha1 || '')"
|
||||
@set-file-type="(type) => (versionFile.file_type = type)"
|
||||
/>
|
||||
<VersionFileRow
|
||||
v-for="(versionFile, idx) in supplementaryNewFiles"
|
||||
:key="versionFile.file.name"
|
||||
:name="versionFile.file.name"
|
||||
:is-primary="false"
|
||||
:initial-file-type="versionFile.fileType"
|
||||
:editing-version="editingVersion"
|
||||
:on-remove="() => handleRemoveFile(idx + (primaryFile?.existing ? 0 : 1))"
|
||||
@set-file-type="(type) => (versionFile.fileType = type)"
|
||||
@set-primary-file="handleSetPrimaryFile(idx + (primaryFile?.existing ? 0 : 1))"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span>
|
||||
You can optionally add supplementary files such as source code, documentation, or required
|
||||
resource packs.
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { Admonition, DropzoneFileInput, injectProjectPageContext } from '@modrinth/ui'
|
||||
import { acceptFileFromProjectType } from '@modrinth/utils'
|
||||
|
||||
import { injectManageVersionContext } from '~/providers/version/manage-version-modal'
|
||||
|
||||
import VersionFileRow from '../components/VersionFileRow.vue'
|
||||
|
||||
const { projectV2 } = injectProjectPageContext()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const {
|
||||
draftVersion,
|
||||
filesToAdd,
|
||||
existingFilesToDelete,
|
||||
setPrimaryFile,
|
||||
setInferredVersionData,
|
||||
setProjectType,
|
||||
editingVersion,
|
||||
projectType,
|
||||
} = injectManageVersionContext()
|
||||
|
||||
const addDetectedData = async () => {
|
||||
if (editingVersion.value) return
|
||||
|
||||
const primaryFile = filesToAdd.value[0]?.file
|
||||
if (!primaryFile) return
|
||||
|
||||
try {
|
||||
const inferredData = await setInferredVersionData(primaryFile, projectV2.value)
|
||||
const mappedInferredData: Partial<Labrinth.Versions.v3.DraftVersion> = {
|
||||
...inferredData,
|
||||
name: inferredData.name || '',
|
||||
}
|
||||
|
||||
draftVersion.value = {
|
||||
...draftVersion.value,
|
||||
...mappedInferredData,
|
||||
}
|
||||
|
||||
setProjectType(projectV2.value, primaryFile)
|
||||
} catch (err) {
|
||||
console.error('Error parsing version file data', err)
|
||||
}
|
||||
|
||||
if (projectType.value === 'resourcepack') {
|
||||
draftVersion.value.loaders = ['minecraft']
|
||||
}
|
||||
}
|
||||
|
||||
// add detected data when the primary file changes
|
||||
watch(
|
||||
() => filesToAdd.value[0]?.file,
|
||||
() => addDetectedData(),
|
||||
)
|
||||
|
||||
function handleNewFiles(newFiles: File[]) {
|
||||
// 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) setPrimaryFile(primaryFileIndex)
|
||||
}
|
||||
}
|
||||
|
||||
function handleRemoveFile(index: number) {
|
||||
filesToAdd.value.splice(index, 1)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
function handleRemoveExistingFile(sha1: string) {
|
||||
existingFilesToDelete.value.push(sha1)
|
||||
draftVersion.value.existing_files = draftVersion.value.existing_files?.filter(
|
||||
(file) => file.hashes.sha1 !== sha1,
|
||||
)
|
||||
}
|
||||
|
||||
function handleSetPrimaryFile(index: number) {
|
||||
setPrimaryFile(index)
|
||||
}
|
||||
|
||||
interface PrimaryFile {
|
||||
name: string
|
||||
fileType?: string
|
||||
existing?: boolean
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
const supplementaryNewFiles = computed(() => {
|
||||
if (primaryFile.value?.existing) {
|
||||
return filesToAdd.value
|
||||
} else {
|
||||
return filesToAdd.value.slice(1)
|
||||
}
|
||||
})
|
||||
|
||||
const supplementaryExistingFiles = computed(() => {
|
||||
if (primaryFile.value?.existing) {
|
||||
return draftVersion.value.existing_files?.slice(1)
|
||||
} else {
|
||||
return draftVersion.value.existing_files
|
||||
}
|
||||
})
|
||||
|
||||
const hasSupplementaryFiles = computed(
|
||||
() => filesToAdd.value.length + (draftVersion.value.existing_files?.length || 0) > 1,
|
||||
)
|
||||
|
||||
const messages = defineMessages({
|
||||
addFilesAdmonition: {
|
||||
id: 'create-project-version.create-modal.stage.add-files.admonition',
|
||||
defaultMessage:
|
||||
'Supplementary files are for supporting resources like source code, not for alternative versions or variants.',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<LoaderPicker
|
||||
v-model="draftVersion.loaders"
|
||||
:loaders="generatedState.loaders"
|
||||
:toggle-loader="toggleLoader"
|
||||
/>
|
||||
|
||||
<div v-if="draftVersion.loaders.length" class="space-y-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-semibold text-contrast"> Added loaders </span>
|
||||
<ButtonStyled type="transparent" size="standard">
|
||||
<button @click="onClearAll()">Clear all</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col gap-1.5 gap-y-4 rounded-xl border border-solid border-surface-5 p-3 py-4"
|
||||
>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<template
|
||||
v-for="loader in draftVersion.loaders.map((loaderName) =>
|
||||
loaders.find((loader) => loaderName === loader.name),
|
||||
)"
|
||||
>
|
||||
<TagItem
|
||||
v-if="loader"
|
||||
:key="`loader-${loader.name}`"
|
||||
:action="() => toggleLoader(loader.name)"
|
||||
class="border !border-solid border-surface-5 !transition-all hover:bg-button-bgHover hover:no-underline"
|
||||
:style="`--_color: var(--color-platform-${loader.name})`"
|
||||
>
|
||||
<div v-html="loader.icon"></div>
|
||||
{{ formatCategory(loader.name) }}
|
||||
<XIcon class="text-secondary" />
|
||||
</TagItem>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, injectProjectPageContext, TagItem } from '@modrinth/ui'
|
||||
import { formatCategory } from '@modrinth/utils'
|
||||
|
||||
import { injectManageVersionContext } from '~/providers/version/manage-version-modal'
|
||||
|
||||
import LoaderPicker from '../components/LoaderPicker.vue'
|
||||
|
||||
const generatedState = useGeneratedState()
|
||||
|
||||
const { projectV2 } = injectProjectPageContext()
|
||||
const loaders = computed(() => generatedState.value.loaders)
|
||||
|
||||
const { draftVersion, setProjectType } = injectManageVersionContext()
|
||||
|
||||
const toggleLoader = (loader: string) => {
|
||||
if (draftVersion.value.loaders.includes(loader)) {
|
||||
draftVersion.value.loaders = draftVersion.value.loaders.filter((l) => l !== loader)
|
||||
} else {
|
||||
draftVersion.value.loaders = [...draftVersion.value.loaders, loader]
|
||||
}
|
||||
setProjectType(projectV2.value)
|
||||
}
|
||||
|
||||
const onClearAll = () => {
|
||||
draftVersion.value.loaders = []
|
||||
setProjectType(projectV2.value)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-6">
|
||||
<McVersionPicker v-model="draftVersion.game_versions" :game-versions="gameVersions" />
|
||||
<div v-if="draftVersion.game_versions.length" class="space-y-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-semibold text-contrast"> Added versions </span>
|
||||
<ButtonStyled type="transparent" size="standard">
|
||||
<button @click="clearAllVersions()">Clear all</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div
|
||||
class="flex max-h-56 flex-col gap-1.5 gap-y-4 overflow-y-auto rounded-xl border border-solid border-surface-5 p-3 py-4"
|
||||
>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<template v-if="draftVersion.game_versions.length">
|
||||
<TagItem
|
||||
v-for="version in draftVersion.game_versions"
|
||||
:key="version"
|
||||
:action="() => toggleVersion(version)"
|
||||
class="border !border-solid border-surface-5 !transition-all hover:bg-button-bgHover hover:no-underline"
|
||||
>
|
||||
{{ version }}
|
||||
<XIcon />
|
||||
</TagItem>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span>No versions selected.</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, TagItem } from '@modrinth/ui'
|
||||
|
||||
import { injectManageVersionContext } from '~/providers/version/manage-version-modal'
|
||||
|
||||
import McVersionPicker from '../components/McVersionPicker.vue'
|
||||
|
||||
const generatedState = useGeneratedState()
|
||||
const gameVersions = generatedState.value.gameVersions
|
||||
|
||||
const { draftVersion } = injectManageVersionContext()
|
||||
|
||||
const toggleVersion = (version: string) => {
|
||||
if (draftVersion.value.game_versions.includes(version)) {
|
||||
draftVersion.value.game_versions = draftVersion.value.game_versions.filter((v) => v !== version)
|
||||
} else {
|
||||
draftVersion.value.game_versions.push(version)
|
||||
}
|
||||
}
|
||||
|
||||
const clearAllVersions = () => {
|
||||
draftVersion.value.game_versions = []
|
||||
}
|
||||
</script>
|
||||
@@ -247,13 +247,7 @@ async function createProject() {
|
||||
})
|
||||
|
||||
modal.value.hide()
|
||||
await router.push({
|
||||
name: 'type-id',
|
||||
params: {
|
||||
type: 'project',
|
||||
id: slug.value,
|
||||
},
|
||||
})
|
||||
await router.push(`/project/${slug.value}/settings`)
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
title: formatMessage(messages.errorTitle),
|
||||
|
||||
Reference in New Issue
Block a user