You've already forked AstralRinth
forked from didirus/AstralRinth
feat: manage project versions v2 (#5049)
* update add files copy and go to next step on just one file * rename and reorder stages * add metadata stage and update details stage * implement files inside metadata stage * use regular prettier instead of prettier eslint * remove changelog stage config * save button on details stage * update edit buttons in versions table * add collapse environment selector * implement dependencies list in metadata step * move dependencies into provider * add suggested dependencies to metadata stage * pnpm prepr * fix unused var * Revert "add collapse environment selector" This reverts commit f90fabc7a57ff201f26e1b628eeced8e6ef75865. * hide resource pack loader only when its the only loader * fix no dependencies for modpack * add breadcrumbs with hide breadcrumb option * wider stages * add proper horizonal scroll breadcrumbs * fix titles * handle save version in version page * remove box shadow * add notification provider to storybook * add drop area for versions to drop file right into page * fix mobile versions table buttons overflowing * pnpm prepr * fix drop file opening modal in wrong stage * implement invalid file for dropping files * allow horizontal scroll on breadcrumbs * update infer.js as best as possible * add create version button uploading version state * add extractVersionFromFilename for resource pack and datapack * allow jars for datapack project * detect multiple loaders when possible * iris means compatible with optifine too * infer environment on loader change as well * add tooltip * prevent navigate forward when cannot go to next step * larger breadcrumb click targets * hide loaders and mc versions stage until files added * fix max width in header * fix add files from metadata step jumping steps * define width in NewModal instead * disable remove dependency in metadata stage * switch metadata and details buttons positions * fix remove button spacing * do not allow duplicate suggested dependencies * fix version detection for fabric minecraft version semvar * better verion number detection based on filename * show resource pack loader but uneditable * remove vanilla shader detection * refactor: break up large infer.js into ts and modules * remove duplicated types * add fill missing from file name step * pnpm prepr * fix neoforge loader parse failing and not adding neoforge loader * add missing pack formats * handle new pack format * pnpm prepr * add another regex where it is version in anywhere in filename * only show resource pack or data pack options for filetype on datapack project * add redundant zip folder check * reject RP and DP if has redundant folder * fix hide stage in breadcrumb * add snapshot group key in case no release version. brings out 26.1 snapshots * pnpm prepr * open in group if has something selected * fix resource pack loader uneditable if accidentally selected on different project type * add new environment tags * add unknown and not applicable environment tags * pnpm prepr * use shared constant on labels * use ref for timeout * remove console logs * remove box shadow only for cm-content * feat: xhr upload + fix wrangler prettierignore * fix: upload content type fix * fix dependencies version width * fix already added dependencies logic * add changelog minheight * set progress percentage on button * add legacy fabric detection logic * lint * small update on create version button label --------- Co-authored-by: Calum H. (IMB11) <contact@cal.engineer> Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
@@ -1,15 +1,28 @@
|
||||
<template>
|
||||
<MultiStageModal ref="modal" :stages="ctx.stageConfigs" :context="ctx" />
|
||||
<MultiStageModal
|
||||
ref="modal"
|
||||
:stages="ctx.stageConfigs"
|
||||
:context="ctx"
|
||||
:breadcrumbs="!editingVersion"
|
||||
@hide="() => (modalOpen = false)"
|
||||
/>
|
||||
<DropArea
|
||||
v-if="!modalOpen"
|
||||
:accept="acceptFileFromProjectType(projectV2.project_type)"
|
||||
@change="handleDropArea"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import {
|
||||
DropArea,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
injectProjectPageContext,
|
||||
MultiStageModal,
|
||||
} from '@modrinth/ui'
|
||||
import { acceptFileFromProjectType } from '@modrinth/utils'
|
||||
import type { ComponentExposed } from 'vue-component-type-helpers'
|
||||
|
||||
import {
|
||||
@@ -17,12 +30,17 @@ import {
|
||||
provideManageVersionContext,
|
||||
} from '~/providers/version/manage-version-modal'
|
||||
|
||||
const modal = useTemplateRef<ComponentExposed<typeof MultiStageModal>>('modal')
|
||||
const emit = defineEmits<{
|
||||
(e: 'save'): void
|
||||
}>()
|
||||
|
||||
const ctx = createManageVersionContext(modal)
|
||||
const modal = useTemplateRef<ComponentExposed<typeof MultiStageModal>>('modal')
|
||||
const modalOpen = ref(false)
|
||||
|
||||
const ctx = createManageVersionContext(modal, () => emit('save'))
|
||||
provideManageVersionContext(ctx)
|
||||
|
||||
const { newDraftVersion } = ctx
|
||||
const { newDraftVersion, editingVersion, handleNewFiles } = ctx
|
||||
|
||||
const { projectV2 } = injectProjectPageContext()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
@@ -64,6 +82,15 @@ function openCreateVersionModal(
|
||||
newDraftVersion(projectV2.value.id, version)
|
||||
modal.value?.setStage(stageId ?? 0)
|
||||
modal.value?.show()
|
||||
modalOpen.value = true
|
||||
}
|
||||
|
||||
async function handleDropArea(files: FileList) {
|
||||
newDraftVersion(projectV2.value.id, null)
|
||||
modal.value?.setStage(0)
|
||||
await handleNewFiles(Array.from(files))
|
||||
modal.value?.show()
|
||||
modalOpen.value = true
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-between gap-2 rounded-xl bg-button-bg px-4 py-1 text-button-text"
|
||||
class="flex h-11 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" />
|
||||
@@ -9,7 +9,7 @@
|
||||
{{ name || 'Unknown Project' }}
|
||||
</span>
|
||||
|
||||
<TagItem class="shrink-0 border !border-solid border-surface-5">
|
||||
<TagItem class="shrink-0 border !border-solid border-surface-5 capitalize">
|
||||
{{ dependencyType }}
|
||||
</TagItem>
|
||||
</div>
|
||||
@@ -17,14 +17,15 @@
|
||||
<span
|
||||
v-if="versionName"
|
||||
v-tooltip="versionName"
|
||||
class="max-w-[35%] truncate whitespace-nowrap font-medium"
|
||||
class="truncate whitespace-nowrap font-medium"
|
||||
:class="!hideRemove ? 'max-w-[35%]' : 'max-w-[50%]'"
|
||||
>
|
||||
{{ versionName }}
|
||||
</span>
|
||||
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<div v-if="!hideRemove" class="flex items-center justify-end gap-1">
|
||||
<ButtonStyled size="standard" :circular="true">
|
||||
<button aria-label="Remove file" class="!shadow-none" @click="emitRemove">
|
||||
<button aria-label="Remove file" class="-mr-2 !shadow-none" @click="emitRemove">
|
||||
<XIcon aria-hidden="true" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
@@ -42,12 +43,13 @@ const emit = defineEmits<{
|
||||
(e: 'remove'): void
|
||||
}>()
|
||||
|
||||
const { projectId, name, icon, dependencyType, versionName } = defineProps<{
|
||||
const { projectId, name, icon, dependencyType, versionName, hideRemove } = defineProps<{
|
||||
projectId: string
|
||||
name?: string
|
||||
icon?: string
|
||||
dependencyType: Labrinth.Versions.v2.DependencyType
|
||||
versionName?: string
|
||||
hideRemove?: boolean
|
||||
}>()
|
||||
|
||||
function emitRemove() {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div v-if="addedDependencies.length" 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"
|
||||
:hide-remove="disableRemove"
|
||||
@remove="() => removeDependency(index)"
|
||||
/>
|
||||
</template>
|
||||
<span v-if="!addedDependencies.length"> No dependencies added. </span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { injectManageVersionContext } from '~/providers/version/manage-version-modal'
|
||||
|
||||
import AddedDependencyRow from './AddedDependencyRow.vue'
|
||||
|
||||
const { disableRemove } = defineProps<{
|
||||
disableRemove?: boolean
|
||||
}>()
|
||||
|
||||
const { draftVersion, dependencyProjects, dependencyVersions, projectsFetchLoading } =
|
||||
injectManageVersionContext()
|
||||
|
||||
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 removeDependency = (index: number) => {
|
||||
if (!draftVersion.value.dependencies) return
|
||||
draftVersion.value.dependencies.splice(index, 1)
|
||||
}
|
||||
</script>
|
||||
@@ -118,4 +118,17 @@ function groupLoaders(loaders: Labrinth.Tags.v2.Loader[]) {
|
||||
}
|
||||
|
||||
const groupedLoaders = computed(() => groupLoaders(loaders))
|
||||
|
||||
onMounted(() => {
|
||||
if (selectedLoaders.value.length === 0) return
|
||||
|
||||
// Find the first group that contains any of the selected loaders
|
||||
const groups = groupedLoaders.value
|
||||
for (const [groupName, loadersInGroup] of Object.entries(groups)) {
|
||||
if (loadersInGroup.some((loader) => selectedLoaders.value.includes(loader.name))) {
|
||||
loaderGroup.value = groupName as GroupLabels
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -66,7 +66,7 @@ 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'
|
||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||
|
||||
type GameVersion = Labrinth.Tags.v2.GameVersion
|
||||
|
||||
@@ -147,9 +147,15 @@ function groupVersions(gameVersions: GameVersion[]) {
|
||||
)
|
||||
|
||||
const getGroupKey = (v: string) => v.split('.').slice(0, 2).join('.')
|
||||
|
||||
const getSnapshotGroupKey = (v: string) => {
|
||||
const cleanVersion = v.split('-')[0]
|
||||
return cleanVersion.split('.').slice(0, 2).join('.')
|
||||
}
|
||||
|
||||
const groups: Record<string, string[]> = {}
|
||||
|
||||
let currentGroupKey = getGroupKey(gameVersions.find((v) => v.major)?.version || '')
|
||||
let currentGroupKey = getSnapshotGroupKey(gameVersions.find((v) => v.major)?.version || '')
|
||||
|
||||
gameVersions.forEach((gameVersion) => {
|
||||
if (gameVersion.version_type === 'release') {
|
||||
@@ -157,6 +163,8 @@ function groupVersions(gameVersions: GameVersion[]) {
|
||||
if (!groups[currentGroupKey]) groups[currentGroupKey] = []
|
||||
groups[currentGroupKey].push(gameVersion.version)
|
||||
} else {
|
||||
if (!currentGroupKey) currentGroupKey = getSnapshotGroupKey(gameVersion.version)
|
||||
|
||||
const key = `${currentGroupKey} ${DEV_RELEASE_KEY}`
|
||||
if (!groups[key]) groups[key] = []
|
||||
groups[key].push(gameVersion.version)
|
||||
@@ -205,4 +213,27 @@ function compareGroupKeys(a: string, b: string) {
|
||||
function searchFilter(gameVersion: Labrinth.Tags.v2.GameVersion) {
|
||||
return gameVersion.version.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.modelValue.length === 0) return
|
||||
|
||||
// Open non-release tab if any non-release versions are selected
|
||||
const hasNonReleaseVersions = props.gameVersions.some(
|
||||
(v) => props.modelValue.includes(v.version) && v.version_type !== 'release',
|
||||
)
|
||||
|
||||
if (hasNonReleaseVersions) {
|
||||
versionType.value = 'all'
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
const firstSelectedVersion = allVersionsFlat.value.find((v) => props.modelValue.includes(v))
|
||||
if (firstSelectedVersion) {
|
||||
const buttons = Array.from(document.querySelectorAll('button'))
|
||||
const element = buttons.find((btn) => btn.textContent?.trim() === firstSelectedVersion)
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
<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 v-if="visibleSuggestedDependencies.length" class="flex flex-col gap-2">
|
||||
<template v-for="(dependency, index) in visibleSuggestedDependencies">
|
||||
<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>
|
||||
</template>
|
||||
|
||||
@@ -32,28 +29,7 @@ import { injectManageVersionContext } from '~/providers/version/manage-version-m
|
||||
|
||||
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 { visibleSuggestedDependencies } = injectManageVersionContext()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'onAddSuggestion', dependency: Labrinth.Versions.v3.Dependency): void
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-between gap-2 rounded-xl bg-button-bg px-4 py-1 text-button-text"
|
||||
class="flex items-center justify-between gap-2 rounded-xl border-2 border-dashed border-surface-5 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" />
|
||||
@@ -9,7 +9,7 @@
|
||||
{{ name || 'Unknown Project' }}
|
||||
</span>
|
||||
|
||||
<TagItem class="shrink-0 border !border-solid border-surface-5">
|
||||
<TagItem class="shrink-0 border !border-solid border-surface-5 capitalize">
|
||||
{{ dependencyType }}
|
||||
</TagItem>
|
||||
</div>
|
||||
@@ -23,7 +23,7 @@
|
||||
</span>
|
||||
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<ButtonStyled size="standard" :circular="true">
|
||||
<ButtonStyled size="standard" :circular="true" type="transparent">
|
||||
<button aria-label="Add dependency" class="!shadow-none" @click="emitAddSuggestion">
|
||||
<PlusIcon aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
@@ -68,10 +68,16 @@
|
||||
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 type { ComboboxOption } from '@modrinth/ui/src/components/base/Combobox.vue'
|
||||
import { acceptFileFromProjectType } from '@modrinth/utils'
|
||||
|
||||
import {
|
||||
fileTypeLabels,
|
||||
injectManageVersionContext,
|
||||
} from '~/providers/version/manage-version-modal'
|
||||
|
||||
const { projectV2 } = injectProjectPageContext()
|
||||
const { projectType } = injectManageVersionContext()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'setPrimaryFile', file?: File): void
|
||||
@@ -89,16 +95,29 @@ const { name, isPrimary, onRemove, initialFileType, editingVersion } = definePro
|
||||
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'>[]
|
||||
const isDatapackProject = computed(() => projectType.value === 'datapack')
|
||||
|
||||
const versionTypes = computed(
|
||||
() =>
|
||||
[
|
||||
!editingVersion && { class: 'text-sm', value: 'primary', label: fileTypeLabels.primary },
|
||||
{ class: 'text-sm', value: 'unknown', label: fileTypeLabels.unknown },
|
||||
isDatapackProject.value && {
|
||||
class: 'text-sm',
|
||||
value: 'required-resource-pack',
|
||||
label: fileTypeLabels['required-resource-pack'],
|
||||
},
|
||||
isDatapackProject.value && {
|
||||
class: 'text-sm',
|
||||
value: 'optional-resource-pack',
|
||||
label: fileTypeLabels['optional-resource-pack'],
|
||||
},
|
||||
{ class: 'text-sm', value: 'sources-jar', label: fileTypeLabels['sources-jar'] },
|
||||
{ class: 'text-sm', value: 'dev-jar', label: fileTypeLabels['dev-jar'] },
|
||||
{ class: 'text-sm', value: 'javadoc-jar', label: fileTypeLabels['javadoc-jar'] },
|
||||
{ class: 'text-sm', value: 'signature', label: fileTypeLabels.signature },
|
||||
].filter(Boolean) as ComboboxOption<Labrinth.Versions.v3.FileType | 'primary'>[],
|
||||
)
|
||||
|
||||
function emitFileTypeChange() {
|
||||
if (selectedType.value === 'primary') emit('setPrimaryFile')
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<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">
|
||||
<FileIcon v-if="isPrimary" class="text-lg" />
|
||||
<FilePlusIcon v-else class="text-lg" />
|
||||
|
||||
<span v-tooltip="name" class="overflow-hidden text-ellipsis whitespace-nowrap font-medium">
|
||||
{{ name }}
|
||||
</span>
|
||||
|
||||
<TagItem class="shrink-0 border !border-solid border-surface-5">
|
||||
{{ isPrimary ? 'Primary' : fileTypeLabels[fileType ?? 'unknown'] }}
|
||||
</TagItem>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { FileIcon, FilePlusIcon } from '@modrinth/assets'
|
||||
import { TagItem } from '@modrinth/ui'
|
||||
|
||||
import { fileTypeLabels } from '~/providers/version/manage-version-modal'
|
||||
|
||||
const { name, isPrimary, fileType } = defineProps<{
|
||||
name: string
|
||||
isPrimary?: boolean
|
||||
fileType?: Labrinth.Versions.v3.FileType | 'primary'
|
||||
}>()
|
||||
</script>
|
||||
@@ -1,23 +0,0 @@
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<MarkdownEditor
|
||||
v-model="draftVersion.changelog"
|
||||
:on-image-upload="onImageUpload"
|
||||
:max-height="500"
|
||||
/>
|
||||
</div>
|
||||
</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>
|
||||
@@ -1,272 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-6 sm:w-[512px]">
|
||||
<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', 'beta', 'alpha']"
|
||||
: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="256"
|
||||
/>
|
||||
</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 draftVersionLoaders.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 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">
|
||||
<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,
|
||||
defineMessages,
|
||||
ENVIRONMENTS_COPY,
|
||||
TagItem,
|
||||
useVIntl,
|
||||
} 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 draftVersionLoaders = computed(() =>
|
||||
[
|
||||
...new Set([...draftVersion.value.loaders, ...(draftVersion.value.mrpack_loaders ?? [])]),
|
||||
].filter((loader) => loader !== 'mrpack'),
|
||||
)
|
||||
|
||||
const editLoaders = () => {
|
||||
modal.value?.setStage('from-details-loaders')
|
||||
}
|
||||
const editVersions = () => {
|
||||
modal.value?.setStage('from-details-mc-versions')
|
||||
}
|
||||
const editEnvironment = () => {
|
||||
modal.value?.setStage('from-details-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 { formatMessage } = useVIntl()
|
||||
|
||||
const noEnvironmentMessage = defineMessages({
|
||||
title: {
|
||||
id: 'version.environment.none.title',
|
||||
defaultMessage: 'No environment set',
|
||||
},
|
||||
description: {
|
||||
id: 'version.environment.none.description',
|
||||
defaultMessage: 'The environment for this version has not been specified.',
|
||||
},
|
||||
})
|
||||
|
||||
const unknownEnvironmentMessage = defineMessages({
|
||||
title: {
|
||||
id: 'version.environment.unknown.title',
|
||||
defaultMessage: 'Unknown environment',
|
||||
},
|
||||
description: {
|
||||
id: 'version.environment.unknown.description',
|
||||
defaultMessage: 'The environment: "{environment}" is not recognized.',
|
||||
},
|
||||
})
|
||||
|
||||
const environmentCopy = computed(() => {
|
||||
if (!draftVersion.value.environment) {
|
||||
return {
|
||||
title: formatMessage(noEnvironmentMessage.title),
|
||||
description: formatMessage(noEnvironmentMessage.description),
|
||||
}
|
||||
}
|
||||
|
||||
const envCopy = ENVIRONMENTS_COPY[draftVersion.value.environment]
|
||||
if (envCopy) {
|
||||
return {
|
||||
title: formatMessage(envCopy.title),
|
||||
description: formatMessage(envCopy.description),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title: formatMessage(unknownEnvironmentMessage.title),
|
||||
description: formatMessage(unknownEnvironmentMessage.description, {
|
||||
environment: draftVersion.value.environment,
|
||||
}),
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,11 +1,15 @@
|
||||
<template>
|
||||
<div class="flex w-full flex-col gap-4 sm:w-[512px]">
|
||||
<template v-if="!(filesToAdd.length || draftVersion.existing_files?.length)">
|
||||
<div class="flex w-full flex-col gap-4">
|
||||
<template
|
||||
v-if="handlingNewFiles || !(filesToAdd.length || draftVersion.existing_files?.length)"
|
||||
>
|
||||
<DropzoneFileInput
|
||||
aria-label="Upload file"
|
||||
multiple
|
||||
:accept="acceptFileFromProjectType(projectV2.project_type)"
|
||||
:max-size="524288000"
|
||||
primary-prompt="Upload primary and supporting files"
|
||||
secondary-prompt="Drag and drop files or click to browse"
|
||||
@change="handleNewFiles"
|
||||
/>
|
||||
</template>
|
||||
@@ -21,11 +25,7 @@
|
||||
:is-primary="true"
|
||||
:editing-version="editingVersion"
|
||||
:on-remove="undefined"
|
||||
@set-primary-file="
|
||||
(file) => {
|
||||
if (file && !editingVersion) filesToAdd[0] = { file }
|
||||
}
|
||||
"
|
||||
@set-primary-file="(file) => file && replacePrimaryFile(file)"
|
||||
/>
|
||||
</div>
|
||||
<span>
|
||||
@@ -72,7 +72,7 @@
|
||||
: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))"
|
||||
@set-primary-file="() => swapPrimaryFile(idx + (primaryFile?.existing ? 0 : 1))"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -86,7 +86,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import {
|
||||
Admonition,
|
||||
defineMessages,
|
||||
@@ -107,68 +106,18 @@ const {
|
||||
draftVersion,
|
||||
filesToAdd,
|
||||
existingFilesToDelete,
|
||||
setPrimaryFile,
|
||||
setInferredVersionData,
|
||||
handlingNewFiles,
|
||||
swapPrimaryFile,
|
||||
replacePrimaryFile,
|
||||
editingVersion,
|
||||
primaryFile,
|
||||
handleNewFiles,
|
||||
} = 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,
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error parsing version file data', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 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(
|
||||
@@ -176,38 +125,6 @@ function handleRemoveExistingFile(sha1: string) {
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="flex w-full max-w-full flex-col gap-6 sm:w-[512px]">
|
||||
<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>
|
||||
<span class="font-semibold text-contrast">Project</span>
|
||||
<DependencySelect v-model="newDependencyProjectId" />
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ButtonStyled>
|
||||
<ButtonStyled color="green">
|
||||
<button
|
||||
class="self-start"
|
||||
:disabled="!newDependencyProjectId"
|
||||
@@ -55,28 +55,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SuggestedDependencies
|
||||
:suggested-dependencies="suggestedDependencies"
|
||||
@on-add-suggestion="handleAddSuggestedDependency"
|
||||
/>
|
||||
<div v-if="visibleSuggestedDependencies.length" class="flex flex-col gap-4">
|
||||
<span class="font-semibold text-contrast">Suggested dependencies</span>
|
||||
<SuggestedDependencies @on-add-suggestion="handleAddSuggestedDependency" />
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<DependenciesList />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -88,19 +74,26 @@ import {
|
||||
Combobox,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
injectProjectPageContext,
|
||||
} from '@modrinth/ui'
|
||||
import type { DropdownOption } from '@modrinth/ui/src/components/base/Combobox.vue'
|
||||
import type { ComboboxOption } from '@modrinth/ui/src/components/base/Combobox.vue'
|
||||
|
||||
import DependencySelect from '~/components/ui/create-project-version/components/DependencySelect.vue'
|
||||
import { injectManageVersionContext } from '~/providers/version/manage-version-modal'
|
||||
|
||||
import AddedDependencyRow from '../components/AddedDependencyRow.vue'
|
||||
import DependenciesList from '../components/DependenciesList.vue'
|
||||
import SuggestedDependencies from '../components/SuggestedDependencies/SuggestedDependencies.vue'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const { labrinth } = injectModrinthClient()
|
||||
|
||||
const {
|
||||
draftVersion,
|
||||
dependencyProjects,
|
||||
dependencyVersions,
|
||||
projectsFetchLoading,
|
||||
visibleSuggestedDependencies,
|
||||
} = injectManageVersionContext()
|
||||
|
||||
const errorNotification = (err: any) => {
|
||||
addNotification({
|
||||
title: 'An error occurred',
|
||||
@@ -113,12 +106,7 @@ 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 }>
|
||||
>([])
|
||||
const newDependencyVersions = ref<ComboboxOption<string>[]>([])
|
||||
|
||||
// reset to defaults when select different project
|
||||
watch(newDependencyProjectId, async () => {
|
||||
@@ -140,91 +128,6 @@ watch(newDependencyProjectId, async () => {
|
||||
}
|
||||
})
|
||||
|
||||
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) => {
|
||||
@@ -249,12 +152,13 @@ const addedDependencies = computed(() =>
|
||||
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,
|
||||
)
|
||||
) {
|
||||
const alreadyAdded = draftVersion.value.dependencies.some((existing) => {
|
||||
if (existing.project_id !== dependency.project_id) return false
|
||||
if (!existing.version_id && !dependency.version_id) return true
|
||||
return existing.version_id === dependency.version_id
|
||||
})
|
||||
|
||||
if (alreadyAdded) {
|
||||
addNotification({
|
||||
title: 'Dependency already added',
|
||||
text: 'You cannot add the same dependency twice.',
|
||||
@@ -268,11 +172,6 @@ const addDependency = (dependency: Labrinth.Versions.v3.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,
|
||||
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div class="flex w-full 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', 'beta', 'alpha']"
|
||||
: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="256"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="font-semibold text-contrast"> Version changlog </span>
|
||||
|
||||
<div class="w-full">
|
||||
<MarkdownEditor
|
||||
v-model="draftVersion.changelog"
|
||||
:on-image-upload="onImageUpload"
|
||||
:min-height="150"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Chips, 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>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="space-y-6 sm:w-[512px]">
|
||||
<div class="space-y-6">
|
||||
<LoaderPicker
|
||||
v-model="draftVersion.loaders"
|
||||
:loaders="generatedState.loaders"
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-6 sm:w-[512px]">
|
||||
<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">
|
||||
@@ -0,0 +1,380 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div v-if="!editingVersion" class="flex flex-col gap-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-semibold text-contrast"> Uploaded files </span>
|
||||
|
||||
<ButtonStyled type="transparent" size="standard">
|
||||
<button @click="editFiles">
|
||||
<EditIcon />
|
||||
Edit
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<ViewOnlyFileRow
|
||||
v-if="primaryFile"
|
||||
:key="primaryFile.name"
|
||||
:name="primaryFile.name"
|
||||
:is-primary="true"
|
||||
/>
|
||||
<ViewOnlyFileRow
|
||||
v-for="file in supplementaryNewFiles"
|
||||
:key="file.file.name"
|
||||
:name="file.file.name"
|
||||
:file-type="file.fileType"
|
||||
/>
|
||||
<ViewOnlyFileRow
|
||||
v-for="file in supplementaryExistingFiles"
|
||||
:key="file.filename"
|
||||
:name="file.filename"
|
||||
:file-type="file.file_type"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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 loaders cannot be edited'
|
||||
: isResourcePack
|
||||
? 'Resource pack loaders cannot be edited'
|
||||
: undefined
|
||||
"
|
||||
:disabled="isModpack || isResourcePack"
|
||||
@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 draftVersionLoaders.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>
|
||||
|
||||
<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 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">
|
||||
<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 v-if="!noEnvironmentProject">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-semibold text-contrast"> Environment </span>
|
||||
<UnknownIcon v-tooltip="'Pre-filled from a previous similar version'" />
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<template v-if="!noDependenciesProject">
|
||||
<div v-if="visibleSuggestedDependencies.length" class="flex flex-col gap-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-semibold text-contrast"> Suggested dependencies </span>
|
||||
|
||||
<ButtonStyled type="transparent" size="standard">
|
||||
<button @click="editDependencies">
|
||||
<EditIcon />
|
||||
Edit
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<SuggestedDependencies @on-add-suggestion="handleAddSuggestedDependency" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!visibleSuggestedDependencies.length || draftVersion.dependencies?.length"
|
||||
class="flex flex-col gap-1"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-semibold text-contrast"> Dependencies </span>
|
||||
|
||||
<ButtonStyled type="transparent" size="standard">
|
||||
<button @click="editDependencies">
|
||||
<EditIcon />
|
||||
Edit
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<div v-if="draftVersion.dependencies?.length" class="flex flex-col gap-4">
|
||||
<DependenciesList disable-remove />
|
||||
</div>
|
||||
<div v-else class="flex flex-col gap-1.5 gap-y-4 rounded-xl bg-surface-2 p-3 py-4">
|
||||
<span class="text-sm font-medium">No dependencies added.</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { EditIcon, UnknownIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
defineMessages,
|
||||
ENVIRONMENTS_COPY,
|
||||
injectProjectPageContext,
|
||||
TagItem,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { formatCategory } from '@modrinth/utils'
|
||||
|
||||
import { useGeneratedState } from '~/composables/generated'
|
||||
import { injectManageVersionContext } from '~/providers/version/manage-version-modal'
|
||||
|
||||
import DependenciesList from '../components/DependenciesList.vue'
|
||||
import SuggestedDependencies from '../components/SuggestedDependencies/SuggestedDependencies.vue'
|
||||
import ViewOnlyFileRow from '../components/ViewOnlyFileRow.vue'
|
||||
|
||||
const {
|
||||
draftVersion,
|
||||
inferredVersionData,
|
||||
projectType,
|
||||
noEnvironmentProject,
|
||||
noDependenciesProject,
|
||||
modal,
|
||||
filesToAdd,
|
||||
editingVersion,
|
||||
visibleSuggestedDependencies,
|
||||
} = injectManageVersionContext()
|
||||
|
||||
const { projectV2 } = injectProjectPageContext()
|
||||
|
||||
const generatedState = useGeneratedState()
|
||||
const loaders = computed(() => generatedState.value.loaders)
|
||||
const isModpack = computed(() => projectType.value === 'modpack')
|
||||
const isResourcePack = computed(
|
||||
() =>
|
||||
projectType.value === 'resourcepack' &&
|
||||
(projectV2.value?.project_type === 'resourcepack' ||
|
||||
projectV2.value?.project_type === 'project'),
|
||||
)
|
||||
|
||||
const draftVersionLoaders = computed(() =>
|
||||
[
|
||||
...new Set([...draftVersion.value.loaders, ...(draftVersion.value.mrpack_loaders ?? [])]),
|
||||
].filter((loader) => loader !== 'mrpack'),
|
||||
)
|
||||
|
||||
const editLoaders = () => {
|
||||
modal.value?.setStage('from-details-loaders')
|
||||
}
|
||||
const editVersions = () => {
|
||||
modal.value?.setStage('from-details-mc-versions')
|
||||
}
|
||||
const editEnvironment = () => {
|
||||
modal.value?.setStage('from-details-environment')
|
||||
}
|
||||
const editFiles = () => {
|
||||
modal.value?.setStage('from-details-files')
|
||||
}
|
||||
const editDependencies = () => {
|
||||
modal.value?.setStage('from-details-dependencies')
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
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 { formatMessage } = useVIntl()
|
||||
|
||||
const noEnvironmentMessage = defineMessages({
|
||||
title: {
|
||||
id: 'version.environment.none.title',
|
||||
defaultMessage: 'No environment set',
|
||||
},
|
||||
description: {
|
||||
id: 'version.environment.none.description',
|
||||
defaultMessage: 'The environment for this version has not been specified.',
|
||||
},
|
||||
})
|
||||
|
||||
const unknownEnvironmentMessage = defineMessages({
|
||||
title: {
|
||||
id: 'version.environment.unknown.title',
|
||||
defaultMessage: 'Unknown environment',
|
||||
},
|
||||
description: {
|
||||
id: 'version.environment.unknown.description',
|
||||
defaultMessage: 'The environment: "{environment}" is not recognized.',
|
||||
},
|
||||
})
|
||||
|
||||
const environmentCopy = computed(() => {
|
||||
if (!draftVersion.value.environment) {
|
||||
return {
|
||||
title: formatMessage(noEnvironmentMessage.title),
|
||||
description: formatMessage(noEnvironmentMessage.description),
|
||||
}
|
||||
}
|
||||
|
||||
const envCopy = ENVIRONMENTS_COPY[draftVersion.value.environment]
|
||||
if (envCopy) {
|
||||
return {
|
||||
title: formatMessage(envCopy.title),
|
||||
description: formatMessage(envCopy.description),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title: formatMessage(unknownEnvironmentMessage.title),
|
||||
description: formatMessage(unknownEnvironmentMessage.description, {
|
||||
environment: draftVersion.value.environment,
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
const handleAddSuggestedDependency = (dependency: Labrinth.Versions.v3.Dependency) => {
|
||||
if (!draftVersion.value.dependencies) draftVersion.value.dependencies = []
|
||||
draftVersion.value.dependencies.push({
|
||||
project_id: dependency.project_id,
|
||||
version_id: dependency.version_id,
|
||||
dependency_type: dependency.dependency_type,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user