Search fixes (#134)

* Search fixes

* Fix small instance ui

* fix javaw issue

* menu fix

* Add confirm modal for deletion

* fix build
This commit is contained in:
Geometrically
2023-06-11 15:26:25 -07:00
committed by GitHub
parent e836738887
commit 3535f0c4b4
24 changed files with 796 additions and 576 deletions
+533 -187
View File
@@ -1,12 +1,11 @@
<script setup>
import { computed, onMounted, ref, watch } from 'vue'
import { computed, nextTick, ref, readonly, shallowRef, watch } from 'vue'
import {
Pagination,
Checkbox,
Button,
ClearIcon,
SearchIcon,
DropdownSelect,
SearchFilter,
Card,
ClientIcon,
@@ -15,74 +14,337 @@ import {
formatCategoryHeader,
formatCategory,
Promotion,
DropdownSelect,
} from 'omorphia'
import Multiselect from 'vue-multiselect'
import { handleError, useSearch } from '@/store/state'
import { handleError } from '@/store/state'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { get_categories, get_loaders, get_game_versions } from '@/helpers/tags'
import { useRoute } from 'vue-router'
import { useRoute, useRouter } from 'vue-router'
import { Avatar } from 'omorphia'
import SearchCard from '@/components/ui/SearchCard.vue'
import InstallConfirmModal from '@/components/ui/InstallConfirmModal.vue'
import InstanceInstallModal from '@/components/ui/InstanceInstallModal.vue'
import SplashScreen from '@/components/ui/SplashScreen.vue'
import Instance from '@/components/ui/Instance.vue'
import IncompatibilityWarningModal from '@/components/ui/IncompatibilityWarningModal.vue'
import { useFetch } from '@/helpers/fetch.js'
import { check_installed, get as getInstance } from '@/helpers/profile.js'
import { convertFileSrc } from '@tauri-apps/api/tauri'
const router = useRouter()
const route = useRoute()
const searchStore = useSearch()
searchStore.projectType = route.params.projectType
const showVersions = computed(
() => searchStore.instanceContext === null || searchStore.ignoreInstance
)
const showLoaders = computed(
() =>
searchStore.projectType !== 'datapack' &&
searchStore.projectType !== 'resourcepack' &&
searchStore.projectType !== 'shader' &&
(searchStore.instanceContext === null || searchStore.ignoreInstance)
)
const confirmModal = ref(null)
const modInstallModal = ref(null)
const incompatibilityWarningModal = ref(null)
const breadcrumbs = useBreadcrumbs()
breadcrumbs.setContext({ name: 'Browse', link: route.path, query: route.query })
const loading = ref(false)
const query = ref('')
const facets = ref([])
const orFacets = ref([])
const selectedVersions = ref([])
const onlyOpenSource = ref(false)
const showSnapshots = ref(false)
const loading = ref(true)
const selectedEnvironments = ref([])
const sortTypes = readonly([
{ display: 'Relevance', name: 'relevance' },
{ display: 'Download count', name: 'downloads' },
{ display: 'Follow count', name: 'follows' },
{ display: 'Recently published', name: 'newest' },
{ display: 'Recently updated', name: 'updated' },
])
const sortType = ref(sortTypes[0])
const maxResults = ref(20)
const currentPage = ref(1)
const projectType = ref(route.params.projectType)
const instanceContext = ref(null)
const ignoreInstanceLoaders = ref(false)
const ignoreInstanceGameVersions = ref(false)
const categories = ref([])
const loaders = ref([])
const availableGameVersions = ref([])
const results = shallowRef([])
const pageCount = computed(() =>
results.value ? Math.ceil(results.value.total_hits / results.value.limit) : 1
)
breadcrumbs.setContext({ name: 'Browse', link: route.path })
if (searchStore.projectType === 'modpack') {
searchStore.instanceContext = null
function getArrayOrString(x) {
if (typeof x === 'string' || x instanceof String) {
return [x]
} else {
return x
}
}
onMounted(async () => {
;[categories.value, loaders.value, availableGameVersions.value] = await Promise.all([
get_categories().catch(handleError),
get_loaders().catch(handleError),
get_game_versions().catch(handleError),
])
breadcrumbs.setContext({ name: 'Browse', link: route.path })
if (searchStore.projectType === 'modpack') {
searchStore.instanceContext = null
if (route.query.iv) {
ignoreInstanceGameVersions.value = route.query.iv === 'true'
}
if (route.query.il) {
ignoreInstanceLoaders.value = route.query.il === 'true'
}
if (route.query.i) {
instanceContext.value = await getInstance(route.query.i, true)
}
if (route.query.q) {
query.value = route.query.q
}
if (route.query.f) {
facets.value = getArrayOrString(route.query.f)
}
if (route.query.g) {
orFacets.value = getArrayOrString(route.query.g)
}
if (route.query.v) {
selectedVersions.value = getArrayOrString(route.query.v)
}
if (route.query.l) {
onlyOpenSource.value = route.query.l === 'true'
}
if (route.query.h) {
showSnapshots.value = route.query.h === 'true'
}
if (route.query.e) {
selectedEnvironments.value = getArrayOrString(route.query.e)
}
if (route.query.s) {
sortType.value.name = route.query.s
switch (sortType.value.name) {
case 'relevance':
sortType.value.display = 'Relevance'
break
case 'downloads':
sortType.value.display = 'Downloads'
break
case 'newest':
sortType.value.display = 'Recently published'
break
case 'updated':
sortType.value.display = 'Recently updated'
break
case 'follows':
sortType.value.display = 'Follow count'
break
}
searchStore.searchInput = ''
await handleReset()
loading.value = false
})
}
if (route.query.m) {
maxResults.value = route.query.m
}
if (route.query.o) {
currentPage.value = Math.ceil(route.query.o / maxResults.value) + 1
}
async function refreshSearch() {
const base = 'https://api.modrinth.com/v2/'
const params = [`limit=${maxResults.value}`, `index=${sortType.value.name}`]
if (query.value.length > 0) {
params.push(`query=${query.value.replace(/ /g, '+')}`)
}
if (instanceContext.value) {
if (!ignoreInstanceLoaders.value && projectType.value === 'mod') {
orFacets.value = [`categories:${encodeURIComponent(instanceContext.value.metadata.loader)}`]
}
if (!ignoreInstanceGameVersions.value) {
selectedVersions.value = [instanceContext.value.metadata.game_version]
}
}
if (
facets.value.length > 0 ||
orFacets.value.length > 0 ||
selectedVersions.value.length > 0 ||
selectedEnvironments.value.length > 0 ||
projectType.value
) {
let formattedFacets = []
for (const facet of facets.value) {
formattedFacets.push([facet])
}
// loaders specifier
if (orFacets.value.length > 0) {
formattedFacets.push(orFacets.value)
} else if (projectType.value === 'mod') {
formattedFacets.push(
['forge', 'fabric', 'quilt'].map((x) => `categories:'${encodeURIComponent(x)}'`)
)
} else if (projectType.value === 'datapack') {
formattedFacets.push(['datapack'].map((x) => `categories:'${encodeURIComponent(x)}'`))
}
if (selectedVersions.value.length > 0) {
const versionFacets = []
for (const facet of selectedVersions.value) {
versionFacets.push('versions:' + facet)
}
formattedFacets.push(versionFacets)
}
if (onlyOpenSource.value) {
formattedFacets.push(['open_source:true'])
}
if (selectedEnvironments.value.length > 0) {
let environmentFacets = []
const includesClient = selectedEnvironments.value.includes('client')
const includesServer = selectedEnvironments.value.includes('server')
if (includesClient && includesServer) {
environmentFacets = [['client_side:required'], ['server_side:required']]
} else {
if (includesClient) {
environmentFacets = [
['client_side:optional', 'client_side:required'],
['server_side:optional', 'server_side:unsupported'],
]
}
if (includesServer) {
environmentFacets = [
['client_side:optional', 'client_side:unsupported'],
['server_side:optional', 'server_side:required'],
]
}
}
formattedFacets = [...formattedFacets, ...environmentFacets]
}
if (projectType.value) {
formattedFacets.push([
`project_type:${projectType.value === 'datapack' ? 'mod' : projectType.value}`,
])
}
params.push(`facets=${JSON.stringify(formattedFacets)}`)
}
const offset = (currentPage.value - 1) * maxResults.value
if (currentPage.value !== 1) {
params.push(`offset=${offset}`)
}
let url = 'search'
if (params.length > 0) {
for (let i = 0; i < params.length; i++) {
url += i === 0 ? `?${params[i]}` : `&${params[i]}`
}
}
let val = `${base}${url}`
const rawResults = await useFetch(val, 'search results')
if (instanceContext.value) {
for (let val of rawResults.hits) {
val.installed = await check_installed(instanceContext.value.path, val.project_id).then(
(x) => (val.installed = x)
)
}
}
results.value = rawResults
}
async function onSearchChange(newPageNumber) {
currentPage.value = newPageNumber
if (query.value === null) {
return
}
await refreshSearch()
const obj = getSearchUrl((currentPage.value - 1) * maxResults.value, true)
await router.replace({ path: route.path, query: obj })
breadcrumbs.setContext({ name: 'Browse', link: route.path, query: obj })
}
const searchWrapper = ref(null)
async function onSearchChangeToTop(newPageNumber) {
await onSearchChange(newPageNumber)
await nextTick()
searchWrapper.value.scrollTo({ top: 0, behavior: 'smooth' })
}
function getSearchUrl(offset, useObj) {
const queryItems = []
const obj = {}
if (query.value) {
queryItems.push(`q=${encodeURIComponent(query.value)}`)
obj.q = query.value
}
if (offset > 0) {
queryItems.push(`o=${offset}`)
obj.o = offset
}
if (facets.value.length > 0) {
queryItems.push(`f=${encodeURIComponent(facets.value)}`)
obj.f = facets.value
}
if (orFacets.value.length > 0) {
queryItems.push(`g=${encodeURIComponent(orFacets.value)}`)
obj.g = orFacets.value
}
if (selectedVersions.value.length > 0) {
queryItems.push(`v=${encodeURIComponent(selectedVersions.value)}`)
obj.v = selectedVersions.value
}
if (onlyOpenSource.value) {
queryItems.push('l=true')
obj.l = true
}
if (showSnapshots.value) {
queryItems.push('h=true')
obj.h = true
}
if (selectedEnvironments.value.length > 0) {
queryItems.push(`e=${encodeURIComponent(selectedEnvironments.value)}`)
obj.e = selectedEnvironments.value
}
if (sortType.value.name !== 'relevance') {
queryItems.push(`s=${encodeURIComponent(sortType.value.name)}`)
obj.s = sortType.value.name
}
if (maxResults.value !== 20) {
queryItems.push(`m=${encodeURIComponent(maxResults.value)}`)
obj.m = maxResults.value
}
if (instanceContext.value) {
queryItems.push(`i=${encodeURIComponent(instanceContext.value.path)}`)
obj.i = instanceContext.value.path
}
if (ignoreInstanceGameVersions.value) {
queryItems.push('iv=true')
obj.iv = true
}
if (ignoreInstanceLoaders.value) {
queryItems.push('il=true')
obj.il = true
}
let url = `${route.path}`
if (queryItems.length > 0) {
url += `?${queryItems[0]}`
for (let i = 1; i < queryItems.length; i++) {
url += `&${queryItems[i]}`
}
}
return useObj ? obj : url
}
const sortedCategories = computed(() => {
const values = new Map()
for (const category of categories.value.filter(
(cat) =>
cat.project_type ===
(searchStore.projectType === 'datapack' ? 'mod' : searchStore.projectType)
(cat) => cat.project_type === (projectType.value === 'datapack' ? 'mod' : projectType.value)
)) {
if (!values.has(category.header)) {
values.set(category.header, [])
@@ -92,128 +354,188 @@ const sortedCategories = computed(() => {
return values
})
const getSearchResults = async () => {
const queryString = searchStore.getQueryString()
const response = await useFetch(
`https://api.modrinth.com/v2/search${queryString}`,
'search results'
)
searchStore.setSearchResults(response)
async function clearFilters() {
for (const facet of [...facets.value]) {
await toggleFacet(facet, true)
}
for (const facet of [...orFacets.value]) {
await toggleOrFacet(facet, true)
}
onlyOpenSource.value = false
selectedVersions.value = []
selectedEnvironments.value = []
await onSearchChange(1)
}
const handleReset = async () => {
searchStore.currentPage = 1
searchStore.offset = 0
searchStore.resetFilters()
await getSearchResults()
async function toggleFacet(elementName, doNotSendRequest = false) {
const index = facets.value.indexOf(elementName)
if (index !== -1) {
facets.value.splice(index, 1)
} else {
facets.value.push(elementName)
}
if (!doNotSendRequest) {
await onSearchChange(1)
}
}
const toggleFacet = async (facet) => {
searchStore.currentPage = 1
searchStore.offset = 0
const index = searchStore.facets.indexOf(facet)
async function toggleOrFacet(elementName, doNotSendRequest) {
const index = orFacets.value.indexOf(elementName)
if (index !== -1) {
orFacets.value.splice(index, 1)
} else {
orFacets.value.push(elementName)
}
if (index !== -1) searchStore.facets.splice(index, 1)
else searchStore.facets.push(facet)
await switchPage(1)
if (!doNotSendRequest) {
await onSearchChange(1)
}
}
const toggleOrFacet = async (orFacet) => {
const index = searchStore.orFacets.indexOf(orFacet)
function toggleEnv(environment, sendRequest) {
const index = selectedEnvironments.value.indexOf(environment)
if (index !== -1) {
selectedEnvironments.value.splice(index, 1)
} else {
selectedEnvironments.value.push(environment)
}
if (index !== -1) searchStore.orFacets.splice(index, 1)
else searchStore.orFacets.push(orFacet)
await switchPage(1)
}
const switchPage = async (page) => {
searchStore.currentPage = parseInt(page)
if (page === 1) searchStore.offset = 0
else searchStore.offset = (searchStore.currentPage - 1) * searchStore.limit
await getSearchResults()
if (!sendRequest) {
onSearchChange(1)
}
}
watch(
() => route.params.projectType,
async (projectType) => {
if (!projectType) return
searchStore.projectType = projectType
breadcrumbs.setContext({ name: 'Browse', link: `/browse/${searchStore.projectType}` })
await handleReset()
await switchPage(1)
async (newType) => {
if (!newType) return
projectType.value = newType
breadcrumbs.setContext({ name: 'Browse', link: `/browse/${projectType.value}` })
sortType.value = { display: 'Relevance', name: 'relevance' }
query.value = ''
loading.value = true
await clearFilters()
loading.value = false
}
)
const handleInstanceSwitch = async (value) => {
searchStore.ignoreInstance = value
await switchPage(1)
}
const [categories, loaders, availableGameVersions] = await Promise.all([
get_categories().catch(handleError).then(ref),
get_loaders().catch(handleError).then(ref),
get_game_versions().catch(handleError).then(ref),
refreshSearch(),
])
const selectableProjectTypes = computed(() => {
const values = [
{ label: 'Data Packs', href: `/browse/datapack` },
{ label: 'Shaders', href: `/browse/shader` },
{ label: 'Resource Packs', href: `/browse/resourcepack` },
]
if (searchStore.instanceContext) {
if (searchStore.instanceContext.metadata.loader !== 'vanilla') {
if (instanceContext.value) {
if (
availableGameVersions.value.findIndex(
(x) => x.version === instanceContext.value.metadata.game_version
) <= availableGameVersions.value.findIndex((x) => x.version === '1.13')
) {
values.unshift({ label: 'Data Packs', href: `/browse/datapack` })
}
if (instanceContext.value.metadata.loader !== 'vanilla') {
values.unshift({ label: 'Mods', href: '/browse/mod' })
}
} else {
values.unshift({ label: 'Data Packs', href: `/browse/datapack` })
values.unshift({ label: 'Mods', href: '/browse/mod' })
values.unshift({ label: 'Modpacks', href: '/browse/modpack' })
}
return values
})
const showVersions = computed(
() => instanceContext.value === null || ignoreInstanceGameVersions.value
)
const showLoaders = computed(
() =>
(projectType.value !== 'datapack' &&
projectType.value !== 'resourcepack' &&
projectType.value !== 'shader' &&
instanceContext.value === null) ||
ignoreInstanceLoaders.value
)
</script>
<template>
<div class="search-container">
<aside class="filter-panel">
<Instance v-if="searchStore.instanceContext" :instance="searchStore.instanceContext" small>
<template #content>
<Checkbox
:model-value="searchStore.ignoreInstance"
:checked="searchStore.ignoreInstance"
label="Show unsupported content"
class="filter-checkbox"
@update:model-value="(value) => handleInstanceSwitch(value)"
<div v-if="instanceContext" class="small-instance">
<div class="instance">
<Avatar
:src="
!instanceContext.metadata.icon ||
(instanceContext.metadata.icon && instanceContext.metadata.icon.startsWith('http'))
? instanceContext.metadata.icon
: convertFileSrc(instanceContext.metadata.icon)
"
:alt="instanceContext.metadata.name"
size="sm"
/>
</template>
</Instance>
<div class="small-instance_info">
<span class="title">{{ instanceContext.metadata.name }}</span>
<span>
{{
instanceContext.metadata.loader.charAt(0).toUpperCase() +
instanceContext.metadata.loader.slice(1)
}}
{{ instanceContext.metadata.game_version }}
</span>
</div>
</div>
<Checkbox
v-model="ignoreInstanceGameVersions"
label="Override game versions"
class="filter-checkbox"
@update:model-value="onSearchChangeToTop(1)"
/>
<Checkbox
v-model="ignoreInstanceLoaders"
label="Override loaders"
class="filter-checkbox"
@update:model-value="onSearchChangeToTop(1)"
/>
</div>
<Card class="search-panel-card">
<Button
role="button"
:disabled="
!(
searchStore.facets.length > 0 ||
searchStore.orFacets.length > 0 ||
searchStore.environments.server === true ||
searchStore.environments.client === true ||
searchStore.openSource === true ||
searchStore.activeVersions.length > 0
)
onlyOpenSource === false &&
selectedEnvironments.length === 0 &&
selectedVersions.length === 0 &&
facets.length === 0 &&
orFacets.length === 0
"
@click="handleReset"
><ClearIcon />Clear Filters</Button
@click="clearFilters"
>
<ClearIcon /> Clear Filters
</Button>
<div v-if="showLoaders" class="loaders">
<h2>Loaders</h2>
<div
v-for="loader in loaders.filter(
(l) =>
(searchStore.projectType !== 'mod' &&
l.supported_project_types?.includes(searchStore.projectType)) ||
(searchStore.projectType === 'mod' && ['fabric', 'forge', 'quilt'].includes(l.name))
(projectType !== 'mod' && l.supported_project_types?.includes(projectType)) ||
(projectType === 'mod' && ['fabric', 'forge', 'quilt'].includes(l.name))
)"
:key="loader"
>
<SearchFilter
:active-filters="searchStore.orFacets"
:active-filters="orFacets"
:icon="loader.icon"
:display-name="formatCategory(loader.name)"
:facet-name="`categories:${encodeURIComponent(loader.name)}`"
@@ -222,49 +544,11 @@ const selectableProjectTypes = computed(() => {
/>
</div>
</div>
<div
v-for="categoryList in Array.from(sortedCategories)"
:key="categoryList[0]"
class="categories"
>
<h2>{{ formatCategoryHeader(categoryList[0]) }}</h2>
<div v-for="category in categoryList[1]" :key="category.name">
<SearchFilter
:active-filters="searchStore.facets"
:icon="category.icon"
:display-name="formatCategory(category.name)"
:facet-name="`categories:${encodeURIComponent(category.name)}`"
class="filter-checkbox"
@toggle="toggleFacet"
/>
</div>
</div>
<div v-if="searchStore.projectType !== 'datapack'" class="environment">
<h2>Environments</h2>
<SearchFilter
v-model="searchStore.environments.client"
display-name="Client"
facet-name="client"
class="filter-checkbox"
@click="getSearchResults"
>
<ClientIcon aria-hidden="true" />
</SearchFilter>
<SearchFilter
v-model="searchStore.environments.server"
display-name="Server"
facet-name="server"
class="filter-checkbox"
@click="getSearchResults"
>
<ServerIcon aria-hidden="true" />
</SearchFilter>
</div>
<div v-if="showVersions" class="versions">
<h2>Minecraft versions</h2>
<Checkbox v-model="showSnapshots" class="filter-checkbox" label="Include snapshots" />
<multiselect
v-model="searchStore.activeVersions"
v-model="selectedVersions"
:options="
showSnapshots
? availableGameVersions.map((x) => x.version)
@@ -279,21 +563,59 @@ const selectableProjectTypes = computed(() => {
:clear-search-on-select="false"
:show-labels="false"
placeholder="Choose versions..."
@update:model-value="getSearchResults"
@update:model-value="onSearchChange(1)"
/>
</div>
<div
v-for="categoryList in Array.from(sortedCategories)"
:key="categoryList[0]"
class="categories"
>
<h2>{{ formatCategoryHeader(categoryList[0]) }}</h2>
<div v-for="category in categoryList[1]" :key="category.name">
<SearchFilter
:active-filters="facets"
:icon="category.icon"
:display-name="formatCategory(category.name)"
:facet-name="`categories:${encodeURIComponent(category.name)}`"
class="filter-checkbox"
@toggle="toggleFacet"
/>
</div>
</div>
<div v-if="projectType !== 'datapack'" class="environment">
<h2>Environments</h2>
<SearchFilter
:active-filters="selectedEnvironments"
display-name="Client"
facet-name="client"
class="filter-checkbox"
@toggle="toggleEnv"
>
<ClientIcon aria-hidden="true" />
</SearchFilter>
<SearchFilter
:active-filters="selectedEnvironments"
display-name="Server"
facet-name="server"
class="filter-checkbox"
@toggle="toggleEnv"
>
<ServerIcon aria-hidden="true" />
</SearchFilter>
</div>
<div class="open-source">
<h2>Open source</h2>
<Checkbox
v-model="searchStore.openSource"
v-model="onlyOpenSource"
label="Open source only"
class="filter-checkbox"
label="Open source"
@click="getSearchResults"
@update:model-value="onSearchChange(1)"
/>
</div>
</Card>
</aside>
<div class="search">
<div ref="searchWrapper" class="search">
<Promotion class="promotion" />
<Card class="project-type-container">
<NavRow :links="selectableProjectTypes" />
@@ -302,64 +624,59 @@ const selectableProjectTypes = computed(() => {
<div class="iconified-input">
<SearchIcon aria-hidden="true" />
<input
v-model="searchStore.searchInput"
v-model="query"
autocomplete="off"
type="text"
:placeholder="`Search ${searchStore.projectType}s...`"
@input="getSearchResults"
:placeholder="`Search ${projectType}s...`"
@input="onSearchChange(1)"
/>
</div>
<div class="inline-option">
<span>Sort by</span>
<DropdownSelect
v-model="searchStore.filter"
name="Sort dropdown"
:options="[
'Relevance',
'Download count',
'Follow count',
'Recently published',
'Recently updated',
]"
class="sort-dropdown"
@change="getSearchResults"
v-model="sortType"
name="Sort by"
:options="sortTypes"
:display-name="(option) => option?.display"
@change="onSearchChange(1)"
/>
</div>
<div class="inline-option">
<span>Show per page</span>
<DropdownSelect
v-model="searchStore.limit"
name="Limit dropdown"
v-model="maxResults"
name="Max results"
:options="[5, 10, 15, 20, 50, 100]"
:default-value="searchStore.limit.toString()"
:model-value="searchStore.limit.toString()"
:default-value="maxResults"
:model-value="maxResults"
class="limit-dropdown"
@change="getSearchResults"
@change="onSearchChange(1)"
/>
</div>
</Card>
<Pagination
:page="searchStore.currentPage"
:count="searchStore.pageCount"
@switch-page="switchPage"
:page="currentPage"
:count="pageCount"
:link-function="(x) => getSearchUrl(x <= 1 ? 0 : (x - 1) * maxResults)"
class="pagination-before"
@switch-page="onSearchChange"
/>
<SplashScreen v-if="loading" />
<section v-else class="project-list display-mode--list instance-results" role="list">
<SearchCard
v-for="result in searchStore.searchResults"
v-for="result in results.hits"
:key="result?.project_id"
:project="result"
:instance="searchStore.instanceContext"
:instance="instanceContext"
:categories="[
...categories.filter(
(cat) =>
result?.display_categories.includes(cat.name) &&
cat.project_type === searchStore.projectType
result?.display_categories.includes(cat.name) && cat.project_type === projectType
),
...loaders.filter(
(loader) =>
result?.display_categories.includes(loader.name) &&
loader.supported_project_types?.includes(searchStore.projectType)
loader.supported_project_types?.includes(projectType)
),
]"
:confirm-modal="confirmModal"
@@ -367,10 +684,12 @@ const selectableProjectTypes = computed(() => {
:incompatibility-warning-modal="incompatibilityWarningModal"
/>
</section>
<Pagination
:page="searchStore.currentPage"
:count="searchStore.pageCount"
@switch-page="switchPage"
<pagination
:page="currentPage"
:count="pageCount"
:link-function="(x) => getSearchUrl(x <= 1 ? 0 : (x - 1) * maxResults)"
class="pagination-after"
@switch-page="onSearchChangeToTop"
/>
</div>
</div>
@@ -381,6 +700,32 @@ const selectableProjectTypes = computed(() => {
<style src="vue-multiselect/dist/vue-multiselect.css"></style>
<style lang="scss">
.small-instance {
background: var(--color-bg);
padding: var(--gap-lg);
border-radius: var(--radius-md);
margin-bottom: var(--gap-md);
.instance {
display: flex;
gap: 0.5rem;
margin-bottom: 0.5rem;
.title {
font-weight: 600;
color: var(--color-contrast);
}
}
.small-instance_info {
display: flex;
flex-direction: column;
gap: 0.25rem;
justify-content: space-between;
padding: 0.25rem 0;
}
}
.filter-checkbox {
margin-bottom: 0.3rem;
font-size: 1rem;
@@ -492,7 +837,8 @@ const selectableProjectTypes = computed(() => {
}
.search {
margin: 0 1rem 0 21rem;
scroll-behavior: smooth;
margin: 0 1rem 0.5rem 21rem;
width: calc(100% - 22rem);
.loading {