Fixes on small frontend bugs (#4719)

* Account list is not scrollable
Fixes #4688

* Selecting Glitch in the log Screen
Fixes #4687 by explicitly defining the buffer

* When sorting or grouping your instance, the option you choose does not get saved
Fixes #4647

* use label prop to specify specific local storage for grid display state

* Implement persistent filters on mods page
Fixes #4517

* fix lint errors

* update schemastore links

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
Truman Gao
2025-11-06 23:56:00 -08:00
committed by GitHub
parent 60ffa75653
commit af39a1769c
6 changed files with 40 additions and 20 deletions

View File

@@ -12,6 +12,7 @@ import {
} from '@modrinth/assets' } from '@modrinth/assets'
import { Button, DropdownSelect, injectNotificationManager } from '@modrinth/ui' import { Button, DropdownSelect, injectNotificationManager } from '@modrinth/ui'
import { formatCategoryHeader } from '@modrinth/utils' import { formatCategoryHeader } from '@modrinth/utils'
import { useStorage } from '@vueuse/core'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
@@ -121,40 +122,50 @@ const handleOptionsClick = async (args) => {
} }
} }
const state = useStorage(
`${props.label}-grid-display-state`,
{
group: 'Group',
sortBy: 'Name',
},
localStorage,
{ mergeDefaults: true },
)
const search = ref('') const search = ref('')
const group = ref('Group')
const sortBy = ref('Name')
const filteredResults = computed(() => { const filteredResults = computed(() => {
const { group = 'Group', sortBy = 'Name' } = state.value
const instances = props.instances.filter((instance) => { const instances = props.instances.filter((instance) => {
return instance.name.toLowerCase().includes(search.value.toLowerCase()) return instance.name.toLowerCase().includes(search.value.toLowerCase())
}) })
if (sortBy.value === 'Name') { if (sortBy === 'Name') {
instances.sort((a, b) => { instances.sort((a, b) => {
return a.name.localeCompare(b.name) return a.name.localeCompare(b.name)
}) })
} }
if (sortBy.value === 'Game version') { if (sortBy === 'Game version') {
instances.sort((a, b) => { instances.sort((a, b) => {
return a.game_version.localeCompare(b.game_version, undefined, { numeric: true }) return a.game_version.localeCompare(b.game_version, undefined, { numeric: true })
}) })
} }
if (sortBy.value === 'Last played') { if (sortBy === 'Last played') {
instances.sort((a, b) => { instances.sort((a, b) => {
return dayjs(b.last_played ?? 0).diff(dayjs(a.last_played ?? 0)) return dayjs(b.last_played ?? 0).diff(dayjs(a.last_played ?? 0))
}) })
} }
if (sortBy.value === 'Date created') { if (sortBy === 'Date created') {
instances.sort((a, b) => { instances.sort((a, b) => {
return dayjs(b.date_created).diff(dayjs(a.date_created)) return dayjs(b.date_created).diff(dayjs(a.date_created))
}) })
} }
if (sortBy.value === 'Date modified') { if (sortBy === 'Date modified') {
instances.sort((a, b) => { instances.sort((a, b) => {
return dayjs(b.date_modified).diff(dayjs(a.date_modified)) return dayjs(b.date_modified).diff(dayjs(a.date_modified))
}) })
@@ -162,7 +173,7 @@ const filteredResults = computed(() => {
const instanceMap = new Map() const instanceMap = new Map()
if (group.value === 'Loader') { if (group === 'Loader') {
instances.forEach((instance) => { instances.forEach((instance) => {
const loader = formatCategoryHeader(instance.loader) const loader = formatCategoryHeader(instance.loader)
if (!instanceMap.has(loader)) { if (!instanceMap.has(loader)) {
@@ -171,7 +182,7 @@ const filteredResults = computed(() => {
instanceMap.get(loader).push(instance) instanceMap.get(loader).push(instance)
}) })
} else if (group.value === 'Game version') { } else if (group === 'Game version') {
instances.forEach((instance) => { instances.forEach((instance) => {
if (!instanceMap.has(instance.game_version)) { if (!instanceMap.has(instance.game_version)) {
instanceMap.set(instance.game_version, []) instanceMap.set(instance.game_version, [])
@@ -179,7 +190,7 @@ const filteredResults = computed(() => {
instanceMap.get(instance.game_version).push(instance) instanceMap.get(instance.game_version).push(instance)
}) })
} else if (group.value === 'Group') { } else if (group === 'Group') {
instances.forEach((instance) => { instances.forEach((instance) => {
if (instance.groups.length === 0) { if (instance.groups.length === 0) {
instance.groups.push('None') instance.groups.push('None')
@@ -199,7 +210,7 @@ const filteredResults = computed(() => {
// For 'name', we intuitively expect the sorting to apply to the name of the group first, not just the name of the instance // For 'name', we intuitively expect the sorting to apply to the name of the group first, not just the name of the instance
// ie: Category A should come before B, even if the first instance in B comes before the first instance in A // ie: Category A should come before B, even if the first instance in B comes before the first instance in A
if (sortBy.value === 'Name') { if (sortBy === 'Name') {
const sortedEntries = [...instanceMap.entries()].sort((a, b) => { const sortedEntries = [...instanceMap.entries()].sort((a, b) => {
// None should always be first // None should always be first
if (a[0] === 'None' && b[0] !== 'None') { if (a[0] === 'None' && b[0] !== 'None') {
@@ -217,7 +228,7 @@ const filteredResults = computed(() => {
} }
// default sorting would do 1.20.4 < 1.8.9 because 2 < 8 // default sorting would do 1.20.4 < 1.8.9 because 2 < 8
// localeCompare with numeric=true puts 1.8.9 < 1.20.4 because 8 < 20 // localeCompare with numeric=true puts 1.8.9 < 1.20.4 because 8 < 20
if (group.value === 'Game version') { if (group === 'Game version') {
const sortedEntries = [...instanceMap.entries()].sort((a, b) => { const sortedEntries = [...instanceMap.entries()].sort((a, b) => {
return a[0].localeCompare(b[0], undefined, { numeric: true }) return a[0].localeCompare(b[0], undefined, { numeric: true })
}) })
@@ -241,7 +252,7 @@ const filteredResults = computed(() => {
</div> </div>
<DropdownSelect <DropdownSelect
v-slot="{ selected }" v-slot="{ selected }"
v-model="sortBy" v-model="state.sortBy"
name="Sort Dropdown" name="Sort Dropdown"
class="max-w-[16rem]" class="max-w-[16rem]"
:options="['Name', 'Last played', 'Date created', 'Date modified', 'Game version']" :options="['Name', 'Last played', 'Date created', 'Date modified', 'Game version']"
@@ -252,7 +263,7 @@ const filteredResults = computed(() => {
</DropdownSelect> </DropdownSelect>
<DropdownSelect <DropdownSelect
v-slot="{ selected }" v-slot="{ selected }"
v-model="group" v-model="state.group"
class="max-w-[16rem]" class="max-w-[16rem]"
name="Group Dropdown" name="Group Dropdown"
:options="['Group', 'Loader', 'Game version', 'None']" :options="['Group', 'Loader', 'Game version', 'None']"

View File

@@ -289,7 +289,7 @@ onUnmounted(() => {
user-select: none; user-select: none;
-ms-user-select: none; -ms-user-select: none;
-webkit-user-select: none; -webkit-user-select: none;
max-height: 98vh; max-height: calc(100vh - 300px);
overflow-y: auto; overflow-y: auto;
&::-webkit-scrollbar-track { &::-webkit-scrollbar-track {

View File

@@ -67,6 +67,7 @@
direction="vertical" direction="vertical"
:item-size="20" :item-size="20"
key-field="id" key-field="id"
buffer="200"
> >
<div class="user no-wrap"> <div class="user no-wrap">
<span :style="{ color: item.prefixColor, 'font-weight': item.weight }">{{ <span :style="{ color: item.prefixColor, 'font-weight': item.weight }">{{
@@ -508,7 +509,7 @@ onUnmounted(() => {
background-color: var(--color-accent-contrast); background-color: var(--color-accent-contrast);
color: var(--color-contrast); color: var(--color-contrast);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
padding: 1.5rem; padding-top: 1.5rem;
overflow-x: auto; /* Enables horizontal scrolling */ overflow-x: auto; /* Enables horizontal scrolling */
overflow-y: hidden; /* Disables vertical scrolling on this wrapper */ overflow-y: hidden; /* Disables vertical scrolling on this wrapper */
white-space: nowrap; /* Keeps content on a single line */ white-space: nowrap; /* Keeps content on a single line */
@@ -557,9 +558,10 @@ onUnmounted(() => {
.user { .user {
height: 32%; height: 32%;
padding: 0 12px; padding: 0 1.5rem;
display: flex; display: flex;
align-items: center; align-items: center;
user-select: text;
} }
</style> </style>

View File

@@ -285,6 +285,7 @@ import type { Organization, Project, TeamMember, Version } from '@modrinth/utils
import { formatProjectType } from '@modrinth/utils' import { formatProjectType } from '@modrinth/utils'
import { getCurrentWebview } from '@tauri-apps/api/webview' import { getCurrentWebview } from '@tauri-apps/api/webview'
import { defineMessages, useVIntl } from '@vintl/vintl' import { defineMessages, useVIntl } from '@vintl/vintl'
import { useStorage } from '@vueuse/core'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import type { ComputedRef } from 'vue' import type { ComputedRef } from 'vue'
import { computed, onUnmounted, ref, watch } from 'vue' import { computed, onUnmounted, ref, watch } from 'vue'
@@ -531,7 +532,13 @@ const filterOptions: ComputedRef<FilterOption[]> = computed(() => {
return options return options
}) })
const selectedFilters = ref<string[]>([]) const selectedFilters = useStorage<string[]>(
`${props.instance.name}-mod-selected-filters`,
[],
sessionStorage,
{ mergeDefaults: true },
)
const filteredProjects = computed(() => { const filteredProjects = computed(() => {
const updatesFilter = selectedFilters.value.includes('updates') const updatesFilter = selectedFilters.value.includes('updates')
const disabledFilter = selectedFilters.value.includes('disabled') const disabledFilter = selectedFilters.value.includes('disabled')

View File

@@ -1,5 +1,5 @@
{ {
"$schema": "https://json.schemastore.org/tsconfig", "$schema": "https://www.schemastore.org/tsconfig",
"compilerOptions": { "compilerOptions": {
"target": "ES2020", "target": "ES2020",
"module": "ESNext", "module": "ESNext",

View File

@@ -1,5 +1,5 @@
{ {
"$schema": "https://json.schemastore.org/tsconfig", "$schema": "https://www.schemastore.org/tsconfig",
"extends": "@vue/tsconfig/tsconfig.dom.json", "extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": { "compilerOptions": {
"lib": ["ESNext", "DOM"], "lib": ["ESNext", "DOM"],