mirror of
https://github.com/modrinth/code.git
synced 2026-08-25 00:55:25 +00:00
fix: version tags toggling every supported version and freezing the page (#6967)
Fixes #6601. Clicking any game version tag on the versions tab passed the release's entire game_versions array to toggleFilters, so clicking 26.x (or even a single version tag) selected every version the release supports. On projects supporting many versions this also froze the page for seconds: toggleFilters awaited a needlessly-async toggleFilter per version, which let Vue flush a full re-render between every iteration (~90 flushes on the reported project). - formatVersionsForDisplay's grouping now also returns the member versions per displayed range (getVersionGroupsForDisplay), so version tags toggle exactly the versions they represent - toggleFilters applies the whole batch synchronously in one update, and selects the missing versions unless all are already selected (instead of blindly inverting each one) Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -120,14 +120,17 @@
|
|||||||
<template #cell-gameVersions="{ row: version }">
|
<template #cell-gameVersions="{ row: version }">
|
||||||
<div class="flex min-w-0 w-full max-w-[12rem] flex-wrap gap-1">
|
<div class="flex min-w-0 w-full max-w-[12rem] flex-wrap gap-1">
|
||||||
<TagItem
|
<TagItem
|
||||||
v-for="gameVersion in getDisplayGameVersions(version).slice(0, MAX_GAME_VERSION_TAGS)"
|
v-for="gameVersionGroup in getDisplayGameVersions(version).slice(
|
||||||
:key="`version-tag-${gameVersion}`"
|
0,
|
||||||
v-tooltip="getFilterTooltip(gameVersion)"
|
MAX_GAME_VERSION_TAGS,
|
||||||
|
)"
|
||||||
|
:key="`version-tag-${gameVersionGroup.label}`"
|
||||||
|
v-tooltip="getFilterTooltip(gameVersionGroup.label)"
|
||||||
data-no-row-click
|
data-no-row-click
|
||||||
class="w-fit max-w-full truncate"
|
class="w-fit max-w-full truncate"
|
||||||
:action="() => versionFilters?.toggleFilters('gameVersion', version.game_versions)"
|
:action="() => versionFilters?.toggleFilters('gameVersion', gameVersionGroup.versions)"
|
||||||
>
|
>
|
||||||
<span class="min-w-0 truncate">{{ gameVersion }}</span>
|
<span class="min-w-0 truncate">{{ gameVersionGroup.label }}</span>
|
||||||
</TagItem>
|
</TagItem>
|
||||||
<Menu
|
<Menu
|
||||||
v-if="getDisplayGameVersions(version).length > MAX_GAME_VERSION_TAGS"
|
v-if="getDisplayGameVersions(version).length > MAX_GAME_VERSION_TAGS"
|
||||||
@@ -144,12 +147,16 @@
|
|||||||
<template #popper>
|
<template #popper>
|
||||||
<div class="flex max-w-[20rem] flex-wrap gap-1">
|
<div class="flex max-w-[20rem] flex-wrap gap-1">
|
||||||
<TagItem
|
<TagItem
|
||||||
v-for="gameVersion in getDisplayGameVersions(version).slice(MAX_GAME_VERSION_TAGS)"
|
v-for="gameVersionGroup in getDisplayGameVersions(version).slice(
|
||||||
:key="`overflow-version-tag-${gameVersion}`"
|
MAX_GAME_VERSION_TAGS,
|
||||||
|
)"
|
||||||
|
:key="`overflow-version-tag-${gameVersionGroup.label}`"
|
||||||
class="w-fit max-w-full truncate"
|
class="w-fit max-w-full truncate"
|
||||||
:action="() => versionFilters?.toggleFilters('gameVersion', version.game_versions)"
|
:action="
|
||||||
|
() => versionFilters?.toggleFilters('gameVersion', gameVersionGroup.versions)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<span class="min-w-0 truncate">{{ gameVersion }}</span>
|
<span class="min-w-0 truncate">{{ gameVersionGroup.label }}</span>
|
||||||
</TagItem>
|
</TagItem>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -316,16 +323,18 @@
|
|||||||
<div class="flex flex-col justify-center gap-3">
|
<div class="flex flex-col justify-center gap-3">
|
||||||
<div class="flex flex-row flex-wrap items-center gap-1.5">
|
<div class="flex flex-row flex-wrap items-center gap-1.5">
|
||||||
<TagItem
|
<TagItem
|
||||||
v-for="gameVersion in getDisplayGameVersions(version).slice(
|
v-for="gameVersionGroup in getDisplayGameVersions(version).slice(
|
||||||
0,
|
0,
|
||||||
MAX_GAME_VERSION_TAGS,
|
MAX_GAME_VERSION_TAGS,
|
||||||
)"
|
)"
|
||||||
:key="`version-tag-${gameVersion}`"
|
:key="`version-tag-${gameVersionGroup.label}`"
|
||||||
v-tooltip="getFilterTooltip(gameVersion)"
|
v-tooltip="getFilterTooltip(gameVersionGroup.label)"
|
||||||
class="smart-clickable:allow-pointer-events"
|
class="smart-clickable:allow-pointer-events"
|
||||||
:action="() => versionFilters?.toggleFilters('gameVersion', version.game_versions)"
|
:action="
|
||||||
|
() => versionFilters?.toggleFilters('gameVersion', gameVersionGroup.versions)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
{{ gameVersion }}
|
{{ gameVersionGroup.label }}
|
||||||
</TagItem>
|
</TagItem>
|
||||||
<Menu
|
<Menu
|
||||||
v-if="getDisplayGameVersions(version).length > MAX_GAME_VERSION_TAGS"
|
v-if="getDisplayGameVersions(version).length > MAX_GAME_VERSION_TAGS"
|
||||||
@@ -339,15 +348,16 @@
|
|||||||
<template #popper>
|
<template #popper>
|
||||||
<div class="flex max-w-[20rem] flex-wrap gap-1">
|
<div class="flex max-w-[20rem] flex-wrap gap-1">
|
||||||
<TagItem
|
<TagItem
|
||||||
v-for="gameVersion in getDisplayGameVersions(version).slice(
|
v-for="gameVersionGroup in getDisplayGameVersions(version).slice(
|
||||||
MAX_GAME_VERSION_TAGS,
|
MAX_GAME_VERSION_TAGS,
|
||||||
)"
|
)"
|
||||||
:key="`overflow-version-tag-${gameVersion}`"
|
:key="`overflow-version-tag-${gameVersionGroup.label}`"
|
||||||
:action="
|
:action="
|
||||||
() => versionFilters?.toggleFilters('gameVersion', version.game_versions)
|
() =>
|
||||||
|
versionFilters?.toggleFilters('gameVersion', gameVersionGroup.versions)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
{{ gameVersion }}
|
{{ gameVersionGroup.label }}
|
||||||
</TagItem>
|
</TagItem>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -461,7 +471,12 @@ import {
|
|||||||
VersionChannelIndicator,
|
VersionChannelIndicator,
|
||||||
VersionFilterControl,
|
VersionFilterControl,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import { formatVersionsForDisplay, type GameVersionTag, type Version } from '@modrinth/utils'
|
import {
|
||||||
|
type GameVersionTag,
|
||||||
|
getVersionGroupsForDisplay,
|
||||||
|
type Version,
|
||||||
|
type VersionDisplayGroup,
|
||||||
|
} from '@modrinth/utils'
|
||||||
import { Menu } from 'floating-vue'
|
import { Menu } from 'floating-vue'
|
||||||
import { computed, type Ref, ref } from 'vue'
|
import { computed, type Ref, ref } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
@@ -622,8 +637,8 @@ function hasNoModLoader(loaders: string[]): boolean {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDisplayGameVersions(version: DisplayVersion): string[] {
|
function getDisplayGameVersions(version: DisplayVersion): VersionDisplayGroup[] {
|
||||||
return formatVersionsForDisplay(version.game_versions, props.gameVersions)
|
return getVersionGroupsForDisplay(version.game_versions, props.gameVersions)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFilterTooltip(filter: string): string {
|
function getFilterTooltip(filter: string): string {
|
||||||
|
|||||||
@@ -243,31 +243,35 @@ if (selectedGameVersions.value.some((version) => !isReleaseGameVersion(version))
|
|||||||
showSnapshots.value = true
|
showSnapshots.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleFilters(type: FilterType, filters: Filter[]) {
|
function selectedFiltersOfType(type: FilterType) {
|
||||||
for (const filter of filters) {
|
if (type === 'channel') {
|
||||||
await toggleFilter(type, filter, true)
|
return selectedChannels
|
||||||
|
} else if (type === 'gameVersion') {
|
||||||
|
return selectedGameVersions
|
||||||
|
} else {
|
||||||
|
return selectedPlatforms
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleFilters(type: FilterType, filters: Filter[]) {
|
||||||
|
const selected = selectedFiltersOfType(type)
|
||||||
|
const allSelected = filters.every((filter) => selected.value.includes(filter))
|
||||||
|
|
||||||
|
selected.value = allSelected
|
||||||
|
? selected.value.filter((x) => !filters.includes(x))
|
||||||
|
: [...selected.value, ...filters.filter((filter) => !selected.value.includes(filter))]
|
||||||
|
|
||||||
updateFilters()
|
updateFilters()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleFilter(type: FilterType, filter: Filter, bulk = false) {
|
function toggleFilter(type: FilterType, filter: Filter) {
|
||||||
if (type === 'channel') {
|
const selected = selectedFiltersOfType(type)
|
||||||
selectedChannels.value = selectedChannels.value.includes(filter)
|
|
||||||
? selectedChannels.value.filter((x) => x !== filter)
|
selected.value = selected.value.includes(filter)
|
||||||
: [...selectedChannels.value, filter]
|
? selected.value.filter((x) => x !== filter)
|
||||||
} else if (type === 'gameVersion') {
|
: [...selected.value, filter]
|
||||||
selectedGameVersions.value = selectedGameVersions.value.includes(filter)
|
|
||||||
? selectedGameVersions.value.filter((x) => x !== filter)
|
|
||||||
: [...selectedGameVersions.value, filter]
|
|
||||||
} else if (type === 'platform') {
|
|
||||||
selectedPlatforms.value = selectedPlatforms.value.includes(filter)
|
|
||||||
? selectedPlatforms.value.filter((x) => x !== filter)
|
|
||||||
: [...selectedPlatforms.value, filter]
|
|
||||||
}
|
|
||||||
if (!bulk) {
|
|
||||||
updateFilters()
|
updateFilters()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSelectedGameVersions(versions: string[]) {
|
function updateSelectedGameVersions(versions: string[]) {
|
||||||
@@ -302,7 +306,7 @@ function updateShowSnapshots(value: boolean, _event?: MouseEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clearFilters() {
|
function clearFilters() {
|
||||||
selectedChannels.value = []
|
selectedChannels.value = []
|
||||||
selectedGameVersions.value = []
|
selectedGameVersions.value = []
|
||||||
selectedPlatforms.value = []
|
selectedPlatforms.value = []
|
||||||
|
|||||||
+44
-20
@@ -66,6 +66,11 @@ export type PlatformTag = {
|
|||||||
supported_project_types: DisplayProjectType[]
|
supported_project_types: DisplayProjectType[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type VersionDisplayGroup = {
|
||||||
|
label: string
|
||||||
|
versions: string[]
|
||||||
|
}
|
||||||
|
|
||||||
export function getVersionsToDisplay(project, allGameVersions: GameVersionTag[]) {
|
export function getVersionsToDisplay(project, allGameVersions: GameVersionTag[]) {
|
||||||
return formatVersionsForDisplay(project.game_versions.slice(), allGameVersions)
|
return formatVersionsForDisplay(project.game_versions.slice(), allGameVersions)
|
||||||
}
|
}
|
||||||
@@ -74,6 +79,13 @@ export function formatVersionsForDisplay(
|
|||||||
gameVersions: string[],
|
gameVersions: string[],
|
||||||
allGameVersions: GameVersionTag[],
|
allGameVersions: GameVersionTag[],
|
||||||
) {
|
) {
|
||||||
|
return getVersionGroupsForDisplay(gameVersions, allGameVersions).map((group) => group.label)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getVersionGroupsForDisplay(
|
||||||
|
gameVersions: string[],
|
||||||
|
allGameVersions: GameVersionTag[],
|
||||||
|
): VersionDisplayGroup[] {
|
||||||
const inputVersions = gameVersions.slice()
|
const inputVersions = gameVersions.slice()
|
||||||
const allVersions = allGameVersions.slice()
|
const allVersions = allGameVersions.slice()
|
||||||
|
|
||||||
@@ -112,26 +124,33 @@ export function formatVersionsForDisplay(
|
|||||||
)
|
)
|
||||||
const projectVersionsGrouped = groupVersions(releaseVersions, true)
|
const projectVersionsGrouped = groupVersions(releaseVersions, true)
|
||||||
|
|
||||||
const releaseVersionsAsRanges = projectVersionsGrouped.map(({ major, minor }) => {
|
const releaseVersionsAsRanges: VersionDisplayGroup[] = projectVersionsGrouped.map(
|
||||||
|
({ major, minor }) => {
|
||||||
|
const versions = minor.map((minorVersion) => formatMinecraftMinorVersion(major, minorVersion))
|
||||||
|
|
||||||
if (minor.length === 1) {
|
if (minor.length === 1) {
|
||||||
return formatMinecraftMinorVersion(major, minor[0])
|
return { label: versions[0], versions }
|
||||||
}
|
}
|
||||||
|
|
||||||
const range = allReleasesGrouped.find((x) => x.major === major)
|
const range = allReleasesGrouped.find((x) => x.major === major)
|
||||||
|
|
||||||
if (range?.minor.every((value, index) => value === minor[index])) {
|
if (range?.minor.every((value, index) => value === minor[index])) {
|
||||||
return `${major}.x`
|
return { label: `${major}.x`, versions }
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${formatMinecraftMinorVersion(major, minor[0])}–${formatMinecraftMinorVersion(major, minor[minor.length - 1])}`
|
return {
|
||||||
})
|
label: `${formatMinecraftMinorVersion(major, minor[0])}–${formatMinecraftMinorVersion(major, minor[minor.length - 1])}`,
|
||||||
|
versions,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const legacyVersionsAsRanges = groupConsecutiveIndices(
|
const legacyVersionsAsRanges = groupConsecutiveIndices(
|
||||||
inputVersions.filter((projVer) => allLegacy.some((gameVer) => gameVer.version === projVer)),
|
inputVersions.filter((projVer) => allLegacy.some((gameVer) => gameVer.version === projVer)),
|
||||||
allLegacy,
|
allLegacy,
|
||||||
)
|
)
|
||||||
|
|
||||||
let output = [...legacyVersionsAsRanges]
|
let output: VersionDisplayGroup[] = [...legacyVersionsAsRanges]
|
||||||
|
|
||||||
// show all snapshots if there's no release versions
|
// show all snapshots if there's no release versions
|
||||||
if (releaseVersionsAsRanges.length === 0) {
|
if (releaseVersionsAsRanges.length === 0) {
|
||||||
@@ -141,14 +160,14 @@ export function formatVersionsForDisplay(
|
|||||||
const snapshotVersionsAsRanges =
|
const snapshotVersionsAsRanges =
|
||||||
snapshotVersions.length > 3
|
snapshotVersions.length > 3
|
||||||
? groupConsecutiveIndices(snapshotVersions, allSnapshots)
|
? groupConsecutiveIndices(snapshotVersions, allSnapshots)
|
||||||
: snapshotVersions
|
: snapshotVersions.map((version) => ({ label: version, versions: [version] }))
|
||||||
output = [...snapshotVersionsAsRanges, ...output]
|
output = [...snapshotVersionsAsRanges, ...output]
|
||||||
} else {
|
} else {
|
||||||
output = [...releaseVersionsAsRanges, ...output]
|
output = [...releaseVersionsAsRanges, ...output]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (releaseVersionsAsRanges.length > 0 && latestSnapshot) {
|
if (releaseVersionsAsRanges.length > 0 && latestSnapshot) {
|
||||||
output = [latestSnapshot, ...output]
|
output = [{ label: latestSnapshot, versions: [latestSnapshot] }, ...output]
|
||||||
}
|
}
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
@@ -188,7 +207,10 @@ function groupVersions(versions: string[], consecutive = false) {
|
|||||||
.reverse()
|
.reverse()
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupConsecutiveIndices(versions: string[], referenceList: GameVersionTag[]) {
|
function groupConsecutiveIndices(
|
||||||
|
versions: string[],
|
||||||
|
referenceList: GameVersionTag[],
|
||||||
|
): VersionDisplayGroup[] {
|
||||||
if (!versions || versions.length === 0) {
|
if (!versions || versions.length === 0) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@@ -202,21 +224,23 @@ function groupConsecutiveIndices(versions: string[], referenceList: GameVersionT
|
|||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => referenceMap.get(a) - referenceMap.get(b))
|
.sort((a, b) => referenceMap.get(a) - referenceMap.get(b))
|
||||||
|
|
||||||
const ranges: string[] = []
|
const ranges: VersionDisplayGroup[] = []
|
||||||
let start = sortedList[0]
|
let rangeStartIndex = 0
|
||||||
let previous = sortedList[0]
|
|
||||||
|
|
||||||
for (let i = 1; i < sortedList.length; i++) {
|
for (let i = 1; i <= sortedList.length; i++) {
|
||||||
const current = sortedList[i]
|
if (
|
||||||
if (referenceMap.get(current) !== referenceMap.get(previous) + 1) {
|
i === sortedList.length ||
|
||||||
ranges.push(validateRange(`${previous}–${start}`))
|
referenceMap.get(sortedList[i]) !== referenceMap.get(sortedList[i - 1]) + 1
|
||||||
start = current
|
) {
|
||||||
|
const members = sortedList.slice(rangeStartIndex, i)
|
||||||
|
ranges.push({
|
||||||
|
label: validateRange(`${members[members.length - 1]}–${members[0]}`),
|
||||||
|
versions: members,
|
||||||
|
})
|
||||||
|
rangeStartIndex = i
|
||||||
}
|
}
|
||||||
previous = current
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ranges.push(validateRange(`${previous}–${start}`))
|
|
||||||
|
|
||||||
return ranges
|
return ranges
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user