fix: environment filters (#6611)

This commit is contained in:
Prospector
2026-07-04 16:15:28 -07:00
committed by GitHub
parent ac63abadb7
commit b0108e0624
13 changed files with 190 additions and 68 deletions
@@ -180,7 +180,7 @@
"message": "Discover servers" "message": "Discover servers"
}, },
"app.browse.hide-added-servers": { "app.browse.hide-added-servers": {
"message": "Hide already added servers" "message": "Hide servers already added"
}, },
"app.browse.project-type.modpacks": { "app.browse.project-type.modpacks": {
"message": "Modpacks" "message": "Modpacks"
+1 -1
View File
@@ -403,7 +403,7 @@ const messages = defineMessages({
}, },
hideAddedServers: { hideAddedServers: {
id: 'app.browse.hide-added-servers', id: 'app.browse.hide-added-servers',
defaultMessage: 'Hide already added servers', defaultMessage: 'Hide servers already added',
}, },
installingToServer: { installingToServer: {
id: 'app.browse.server.installing', id: 'app.browse.server.installing',
@@ -4,6 +4,7 @@ import type {
BrowseInstallPlan, BrowseInstallPlan,
BrowseSearchState, BrowseSearchState,
CreationFlowContextValue, CreationFlowContextValue,
EnvironmentSearchOverride,
FilterValue, FilterValue,
PendingServerContentInstall, PendingServerContentInstall,
PendingServerContentInstallType, PendingServerContentInstallType,
@@ -160,6 +161,7 @@ export function useServerInstallContent({
}) })
const serverHideInstalled = ref(false) const serverHideInstalled = ref(false)
const serverContentServerOnly = ref(false)
const hideSelectedServerInstalls = ref(false) const hideSelectedServerInstalls = ref(false)
const installingProjectIds = ref<Set<string>>(new Set()) const installingProjectIds = ref<Set<string>>(new Set())
const optimisticallyInstalledProjectIds = ref<Set<string>>(new Set()) const optimisticallyInstalledProjectIds = ref<Set<string>>(new Set())
@@ -348,10 +350,6 @@ export function useServerInstallContent({
filters.push({ type: 'plugin_loader', option: platform }) filters.push({ type: 'plugin_loader', option: platform })
} }
if (projectType.value?.id === 'mod') {
filters.push({ type: 'environment', option: 'server' })
}
if (serverHideInstalled.value && hiddenInstalledProjectIds.value.size > 0) { if (serverHideInstalled.value && hiddenInstalledProjectIds.value.size > 0) {
for (const x of hiddenInstalledProjectIds.value) { for (const x of hiddenInstalledProjectIds.value) {
filters.push({ filters.push({
@@ -383,6 +381,28 @@ export function useServerInstallContent({
return filters return filters
}) })
const showServerOnlyToggle = computed(() => !!serverData.value && projectType.value?.id === 'mod')
const serverEnvironmentOverride = computed<EnvironmentSearchOverride | undefined>(() => {
if (!showServerOnlyToggle.value) return undefined
if (serverContentServerOnly.value) {
return {
mode: 'include',
values: [
'server_only',
'dedicated_server_only',
'server_only_client_optional',
'client_or_server_prefers_both',
'client_or_server',
],
}
}
return {
mode: 'exclude',
values: ['client_only', 'singleplayer_only'],
}
})
function getCurrentServerInstallType(): BrowseInstallContentType { function getCurrentServerInstallType(): BrowseInstallContentType {
const type = projectType.value?.id const type = projectType.value?.id
if (type === 'modpack' || type === 'mod' || type === 'plugin' || type === 'datapack') { if (type === 'modpack' || type === 'mod' || type === 'plugin' || type === 'datapack') {
@@ -737,6 +757,10 @@ export function useServerInstallContent({
serverHideInstalled.value = route.query.shi === 'true' serverHideInstalled.value = route.query.shi === 'true'
} }
if (route.query.so && projectType.value?.id === 'mod') {
serverContentServerOnly.value = route.query.so === 'true'
}
watch(serverHideInstalled, (hideInstalled) => { watch(serverHideInstalled, (hideInstalled) => {
if (hideInstalled) { if (hideInstalled) {
syncHiddenInstalledProjectIds() syncHiddenInstalledProjectIds()
@@ -763,6 +787,9 @@ export function useServerInstallContent({
serverContentData, serverContentData,
serverFilters, serverFilters,
serverHideInstalled, serverHideInstalled,
serverContentServerOnly,
showServerOnlyToggle,
serverEnvironmentOverride,
hideSelectedServerInstalls, hideSelectedServerInstalls,
installingProjectIds, installingProjectIds,
optimisticallyInstalledProjectIds, optimisticallyInstalledProjectIds,
@@ -158,6 +158,9 @@ const {
serverContentData, serverContentData,
serverFilters, serverFilters,
serverHideInstalled, serverHideInstalled,
serverContentServerOnly,
showServerOnlyToggle,
serverEnvironmentOverride,
hideSelectedServerInstalls, hideSelectedServerInstalls,
installingProjectIds, installingProjectIds,
optimisticallyInstalledProjectIds, optimisticallyInstalledProjectIds,
@@ -378,10 +381,12 @@ const searchState = useBrowseSearch({
projectType: projectTypeId, projectType: projectTypeId,
tags, tags,
providedFilters: serverFilters, providedFilters: serverFilters,
environmentOverride: serverEnvironmentOverride,
search, search,
persistentQueryParams: ['sid', 'wid', 'shi', 'from'], persistentQueryParams: ['sid', 'wid', 'shi', 'so', 'from'],
getExtraQueryParams: () => ({ getExtraQueryParams: () => ({
shi: serverHideInstalled.value ? 'true' : undefined, shi: serverHideInstalled.value ? 'true' : undefined,
so: showServerOnlyToggle.value && serverContentServerOnly.value ? 'true' : undefined,
}), }),
maxResultsOptions: currentMaxResultsOptions, maxResultsOptions: currentMaxResultsOptions,
displayMode: resultsDisplayMode, displayMode: resultsDisplayMode,
@@ -469,6 +474,10 @@ provideBrowseManager({
queuedServerInstallCount.value > 0, queuedServerInstallCount.value > 0,
), ),
hideSelectedLabel: computed(() => formatMessage(commonMessages.hideSelectedContentLabel)), hideSelectedLabel: computed(() => formatMessage(commonMessages.hideSelectedContentLabel)),
serverOnly: serverContentServerOnly,
showServerOnly: showServerOnlyToggle,
serverOnlyLabel: computed(() => formatMessage(commonMessages.serverOnlyLabel)),
hiddenFilterTypeIds: computed(() => (showServerOnlyToggle.value ? ['environment'] : [])),
displayMode: resultsDisplayMode, displayMode: resultsDisplayMode,
cycleDisplayMode: cycleSearchDisplayMode, cycleDisplayMode: cycleSearchDisplayMode,
maxResultsOptions: currentMaxResultsOptions, maxResultsOptions: currentMaxResultsOptions,
@@ -1,6 +1,6 @@
<template> <template>
<div v-bind="$attrs"> <div v-bind="$attrs">
<div v-if="divider && !!slots.title" class="flex items-center gap-4 mb-4"> <div v-if="divider && (!!slots.title || !!slots.button)" class="flex items-center gap-4 mb-4">
<button <button
:class=" :class="
buttonClass ?? buttonClass ??
@@ -24,7 +24,7 @@
<hr class="h-px w-full border-none bg-divider" aria-hidden="true" /> <hr class="h-px w-full border-none bg-divider" aria-hidden="true" />
</div> </div>
<button <button
v-else-if="!!slots.title" v-else-if="!!slots.title || !!slots.button"
:class="buttonClass ?? 'flex flex-col gap-2 bg-transparent m-0 p-0 border-none'" :class="buttonClass ?? 'flex flex-col gap-2 bg-transparent m-0 p-0 border-none'"
@click="() => (forceOpen ? undefined : toggledOpen ? close() : open())" @click="() => (forceOpen ? undefined : toggledOpen ? close() : open())"
> >
@@ -7,10 +7,16 @@
title-wrapper-class="flex flex-col gap-2 justify-start items-start" title-wrapper-class="flex flex-col gap-2 justify-start items-start"
:open-by-default="openByDefault !== undefined ? openByDefault : true" :open-by-default="openByDefault !== undefined ? openByDefault : true"
> >
<template #title> <template #button="{ open }">
<slot name="header" :filter="filterType"> <div class="flex items-center gap-1 w-full text-contrast">
<h2 class="font-semibold">{{ filterType.formatted_name }}</h2> <slot name="header" :filter="filterType">
</slot> <h2 class="text-base font-semibold text-red">{{ filterType.formatted_name }}</h2>
</slot>
<DropdownIcon
class="ml-auto size-5 transition-transform duration-300 shrink-0 text-primary group-hover:text-contrast"
:class="{ 'rotate-180': open }"
/>
</div>
</template> </template>
<template <template
v-if=" v-if="
@@ -39,7 +45,7 @@
</div> </div>
</template> </template>
<template v-if="locked" #default> <template v-if="locked" #default>
<div class="flex flex-col gap-2 p-3 border-dashed border-2 rounded-2xl border-divider mx-2"> <div class="flex flex-col gap-2 p-3 border-dashed border-2 rounded-2xl border-divider">
<p class="m-0 font-bold items-center"> <p class="m-0 font-bold items-center">
<slot :name="`locked-${filterType.id}`"> <slot :name="`locked-${filterType.id}`">
{{ formatMessage(messages.lockedTitle, { type: filterType.formatted_name }) }} {{ formatMessage(messages.lockedTitle, { type: filterType.formatted_name }) }}
@@ -4,7 +4,13 @@ import { computed, nextTick, ref, shallowRef, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { useDebugLogger } from '#ui/composables/debug-logger' import { useDebugLogger } from '#ui/composables/debug-logger'
import type { FilterType, FilterValue, ProjectType, SortType } from '#ui/utils/search' import type {
EnvironmentSearchOverride,
FilterType,
FilterValue,
ProjectType,
SortType,
} from '#ui/utils/search'
import { LOADER_FILTER_TYPES, useSearch } from '#ui/utils/search' import { LOADER_FILTER_TYPES, useSearch } from '#ui/utils/search'
import { useServerSearch } from '#ui/utils/server-search' import { useServerSearch } from '#ui/utils/server-search'
@@ -18,6 +24,7 @@ export interface UseBrowseSearchOptions {
categories: Labrinth.Tags.v2.Category[] categories: Labrinth.Tags.v2.Category[]
}> }>
providedFilters?: ComputedRef<FilterValue[]> providedFilters?: ComputedRef<FilterValue[]>
environmentOverride?: ComputedRef<EnvironmentSearchOverride | undefined>
search: (params: string) => Promise<BrowseSearchResponse> search: (params: string) => Promise<BrowseSearchResponse>
persistentQueryParams: string[] persistentQueryParams: string[]
getExtraQueryParams?: () => Record<string, string | undefined> getExtraQueryParams?: () => Record<string, string | undefined>
@@ -82,7 +89,12 @@ export function useBrowseSearch(options: UseBrowseSearchOptions): BrowseSearchSt
sortTypes, sortTypes,
requestParams, requestParams,
createPageParams, createPageParams,
} = useSearch(projectTypes, options.tags, options.providedFilters ?? computed(() => [])) } = useSearch(
projectTypes,
options.tags,
options.providedFilters ?? computed(() => []),
options.environmentOverride ?? computed(() => undefined),
)
const { const {
serverCurrentSortType, serverCurrentSortType,
@@ -164,7 +164,11 @@ const messages = defineMessages({
<SearchFilterControl <SearchFilterControl
v-else v-else
v-model:selected-filters="ctx.currentFilters.value" v-model:selected-filters="ctx.currentFilters.value"
:filters="ctx.filters.value.filter((f) => f.display !== 'none')" :filters="
ctx.filters.value.filter(
(f) => f.display !== 'none' && !(ctx.hiddenFilterTypes?.value ?? []).includes(f.id),
)
"
:provided-filters="ctx.providedFilters?.value ?? []" :provided-filters="ctx.providedFilters?.value ?? []"
:overridden-provided-filter-types="ctx.overriddenProvidedFilterTypes.value" :overridden-provided-filter-types="ctx.overriddenProvidedFilterTypes.value"
:provided-message="lockedMessages?.providedBy" :provided-message="lockedMessages?.providedBy"
@@ -69,6 +69,10 @@ export interface BrowseManagerContext {
hideSelected?: Ref<boolean> hideSelected?: Ref<boolean>
showHideSelected?: ComputedRef<boolean> showHideSelected?: ComputedRef<boolean>
hideSelectedLabel?: ComputedRef<string> hideSelectedLabel?: ComputedRef<string>
serverOnly?: Ref<boolean>
showServerOnly?: ComputedRef<boolean>
serverOnlyLabel?: ComputedRef<string>
hiddenFilterTypes?: ComputedRef<string[]>
onInstalled?: (projectId: string) => void onInstalled?: (projectId: string) => void
displayMode?: Ref<'list' | 'grid' | 'gallery'> | ComputedRef<'list' | 'grid' | 'gallery'> displayMode?: Ref<'list' | 'grid' | 'gallery'> | ComputedRef<'list' | 'grid' | 'gallery'>
@@ -3,7 +3,7 @@ import { InfoIcon, XIcon } from '@modrinth/assets'
import { computed, toValue } from 'vue' import { computed, toValue } from 'vue'
import ButtonStyled from '#ui/components/base/ButtonStyled.vue' import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
import Checkbox from '#ui/components/base/Checkbox.vue' import Toggle from '#ui/components/base/Toggle.vue'
import SearchSidebarFilter from '#ui/components/search/SearchSidebarFilter.vue' import SearchSidebarFilter from '#ui/components/search/SearchSidebarFilter.vue'
import { useVIntl } from '#ui/composables/i18n' import { useVIntl } from '#ui/composables/i18n'
import { commonMessages } from '#ui/utils/common-messages' import { commonMessages } from '#ui/utils/common-messages'
@@ -15,6 +15,7 @@ const { formatMessage } = useVIntl()
const isApp = computed(() => ctx.variant === 'app') const isApp = computed(() => ctx.variant === 'app')
const lockedMessages = computed(() => toValue(ctx.lockedFilterMessages)) const lockedMessages = computed(() => toValue(ctx.lockedFilterMessages))
const hiddenFilterTypes = computed(() => ctx.hiddenFilterTypes?.value ?? [])
function closeFiltersMenu() { function closeFiltersMenu() {
if (ctx.filtersMenuOpen) { if (ctx.filtersMenuOpen) {
@@ -37,7 +38,7 @@ const buttonClass = computed(() => {
if (isApp.value) { if (isApp.value) {
return 'button-animation flex flex-col gap-1 px-3 py-3 w-full bg-transparent cursor-pointer border-none hover:bg-button-bg' return 'button-animation flex flex-col gap-1 px-3 py-3 w-full bg-transparent cursor-pointer border-none hover:bg-button-bg'
} }
return 'button-animation flex flex-col gap-1 px-6 py-3 w-full bg-transparent cursor-pointer border-none' return 'button-animation flex flex-col gap-1 px-4 py-3 w-full bg-transparent cursor-pointer border-none'
}) })
const contentClass = computed(() => (isApp.value ? 'mt-2 mb-3' : 'mb-4 mx-3')) const contentClass = computed(() => (isApp.value ? 'mt-2 mb-3' : 'mb-4 mx-3'))
@@ -99,38 +100,60 @@ function getFilterOpenByDefault(filterId: string): boolean {
</div> </div>
<div <div
v-if="ctx.showHideInstalled?.value || ctx.showHideSelected?.value" v-if="
ctx.showHideInstalled?.value || ctx.showHideSelected?.value || ctx.showServerOnly?.value
"
:class=" :class="
isApp isApp
? 'flex flex-col gap-3 border-0 border-b-[1px] p-4 last:border-b-0 border-[--brand-gradient-border] border-solid' ? 'flex flex-col gap-3 border-0 border-b-[1px] p-4 last:border-b-0 border-[--brand-gradient-border] border-solid'
: 'card-shadow flex flex-col gap-3 rounded-2xl bg-bg-raised p-4' : 'card-shadow flex flex-col gap-3 rounded-2xl bg-bg-raised border-solid border-surface-4 border p-4'
" "
> >
<Checkbox <label
v-if="ctx.showServerOnly?.value"
class="flex cursor-pointer items-center justify-between gap-3 text-contrast font-medium"
>
{{ ctx.serverOnlyLabel?.value ?? formatMessage(commonMessages.serverOnlyLabel) }}
<Toggle
v-model="ctx.serverOnly!.value"
small
class="shrink-0"
@update:model-value="ctx.onFilterChange()"
/>
</label>
<label
v-if="ctx.showHideInstalled?.value" v-if="ctx.showHideInstalled?.value"
v-model="ctx.hideInstalled!.value" class="flex cursor-pointer items-center justify-between gap-3 text-contrast font-medium"
:label=" >
{{
ctx.hideInstalledLabel?.value ?? formatMessage(commonMessages.hideInstalledContentLabel) ctx.hideInstalledLabel?.value ?? formatMessage(commonMessages.hideInstalledContentLabel)
" }}
class="filter-checkbox" <Toggle
@update:model-value="ctx.onFilterChange()" v-model="ctx.hideInstalled!.value"
@click.prevent.stop small
/> class="shrink-0"
<Checkbox @update:model-value="ctx.onFilterChange()"
/>
</label>
<label
v-if="ctx.showHideSelected?.value" v-if="ctx.showHideSelected?.value"
v-model="ctx.hideSelected!.value" class="flex cursor-pointer items-center justify-between gap-3 text-contrast font-medium"
:label=" >
ctx.hideSelectedLabel?.value ?? formatMessage(commonMessages.hideSelectedContentLabel) {{ ctx.hideSelectedLabel?.value ?? formatMessage(commonMessages.hideSelectedContentLabel) }}
" <Toggle
class="filter-checkbox" v-model="ctx.hideSelected!.value"
@update:model-value="ctx.onFilterChange()" small
@click.prevent.stop class="shrink-0"
/> @update:model-value="ctx.onFilterChange()"
/>
</label>
</div> </div>
<template v-if="ctx.isServerType.value"> <template v-if="ctx.isServerType.value">
<SearchSidebarFilter <SearchSidebarFilter
v-for="filterType in ctx.serverFilterTypes.value.filter((f) => f.options.length > 0)" v-for="filterType in ctx.serverFilterTypes.value.filter(
(f) => f.options.length > 0 && !hiddenFilterTypes.includes(f.id),
)"
:key="`server-filter-${filterType.id}`" :key="`server-filter-${filterType.id}`"
v-model:selected-filters="ctx.serverCurrentFilters.value" v-model:selected-filters="ctx.serverCurrentFilters.value"
v-model:toggled-groups="ctx.serverToggledGroups.value" v-model:toggled-groups="ctx.serverToggledGroups.value"
@@ -143,7 +166,7 @@ function getFilterOpenByDefault(filterId: string): boolean {
:open-by-default="getFilterOpenByDefault(filterType.id)" :open-by-default="getFilterOpenByDefault(filterType.id)"
> >
<template #header> <template #header>
<h3 :class="isApp ? 'text-base m-0' : 'm-0 text-lg font-semibold'"> <h3 :class="isApp ? 'text-base m-0' : 'm-0 text-base font-semibold'">
{{ filterType.formatted_name }} {{ filterType.formatted_name }}
</h3> </h3>
</template> </template>
@@ -151,7 +174,9 @@ function getFilterOpenByDefault(filterId: string): boolean {
</template> </template>
<template v-else> <template v-else>
<SearchSidebarFilter <SearchSidebarFilter
v-for="filter in ctx.filters.value.filter((f) => f.display !== 'none')" v-for="filter in ctx.filters.value.filter(
(f) => f.display !== 'none' && !hiddenFilterTypes.includes(f.id),
)"
:key="`filter-${filter.id}`" :key="`filter-${filter.id}`"
v-model:selected-filters="ctx.currentFilters.value" v-model:selected-filters="ctx.currentFilters.value"
v-model:toggled-groups="ctx.toggledGroups.value" v-model:toggled-groups="ctx.toggledGroups.value"
+4 -1
View File
@@ -2190,7 +2190,7 @@
"defaultMessage": "Game version" "defaultMessage": "Game version"
}, },
"label.hide-installed-content": { "label.hide-installed-content": {
"defaultMessage": "Hide already installed content" "defaultMessage": "Hide content already installed"
}, },
"label.hide-selected-content": { "label.hide-selected-content": {
"defaultMessage": "Hide selected content" "defaultMessage": "Hide selected content"
@@ -2294,6 +2294,9 @@
"label.server": { "label.server": {
"defaultMessage": "Server" "defaultMessage": "Server"
}, },
"label.server-only": {
"defaultMessage": "Server only"
},
"label.servers": { "label.servers": {
"defaultMessage": "Servers" "defaultMessage": "Servers"
}, },
+5 -1
View File
@@ -429,12 +429,16 @@ export const commonMessages = defineMessages({
}, },
hideInstalledContentLabel: { hideInstalledContentLabel: {
id: 'label.hide-installed-content', id: 'label.hide-installed-content',
defaultMessage: 'Hide already installed content', defaultMessage: 'Hide content already installed',
}, },
hideSelectedContentLabel: { hideSelectedContentLabel: {
id: 'label.hide-selected-content', id: 'label.hide-selected-content',
defaultMessage: 'Hide selected content', defaultMessage: 'Hide selected content',
}, },
serverOnlyLabel: {
id: 'label.server-only',
defaultMessage: 'Server only',
},
installedModpackTitle: { installedModpackTitle: {
id: 'label.installed-modpack', id: 'label.installed-modpack',
defaultMessage: 'Installed modpack', defaultMessage: 'Installed modpack',
+53 -25
View File
@@ -60,6 +60,10 @@ export type FilterValue = {
negative?: boolean negative?: boolean
} }
export type EnvironmentSearchOverride =
| { mode: 'include'; values: string[] }
| { mode: 'exclude'; values: string[] }
export const LOADER_FILTER_TYPES = [ export const LOADER_FILTER_TYPES = [
'mod_loader', 'mod_loader',
'plugin_loader', 'plugin_loader',
@@ -112,6 +116,7 @@ export function useSearch(
projectTypes: Ref<ProjectType[]>, projectTypes: Ref<ProjectType[]>,
tags: Ref<Tags>, tags: Ref<Tags>,
providedFilters: Ref<FilterValue[]>, providedFilters: Ref<FilterValue[]>,
environmentOverride: Ref<EnvironmentSearchOverride | undefined> = ref(undefined),
) { ) {
const query = ref('') const query = ref('')
const maxResults = ref(20) const maxResults = ref(20)
@@ -505,22 +510,34 @@ export function useSearch(
} }
// Environment facets // Environment facets
const client = filterValues.some( const override = environmentOverride.value
(filter) => filter.type === 'environment' && filter.option === 'client', if (override) {
) if (override.values.length === 1) {
const server = filterValues.some( const operator = override.mode === 'include' ? '=' : '!='
(filter) => filter.type === 'environment' && filter.option === 'server', parts.push(`environment ${operator} ${formatSearchFilterValue(override.values[0])}`)
) } else if (override.values.length > 1) {
for (const envGroup of getEnvironmentFilterGroups(client, server)) { const operator = override.mode === 'include' ? 'IN' : 'NOT IN'
if (envGroup.length === 1) { const quoted = override.values.map(formatSearchFilterValue).join(', ')
const [field, val] = envGroup[0].split(':') parts.push(`environment ${operator} [${quoted}]`)
parts.push(`${field} = ${formatSearchFilterValue(val)}`) }
} else if (envGroup.length > 1) { } else {
const conditions = envGroup.map((f) => { const client = filterValues.some(
const [field, val] = f.split(':') (filter) => filter.type === 'environment' && filter.option === 'client',
return `${field} = ${formatSearchFilterValue(val)}` )
}) const server = filterValues.some(
parts.push(`(${conditions.join(' OR ')})`) (filter) => filter.type === 'environment' && filter.option === 'server',
)
for (const envGroup of getEnvironmentFilterGroups(client, server)) {
if (envGroup.length === 1) {
const [field, val] = envGroup[0].split(':')
parts.push(`${field} = ${formatSearchFilterValue(val)}`)
} else if (envGroup.length > 1) {
const conditions = envGroup.map((f) => {
const [field, val] = f.split(':')
return `${field} = ${formatSearchFilterValue(val)}`
})
parts.push(`(${conditions.join(' OR ')})`)
}
} }
} }
@@ -777,17 +794,28 @@ function mapProjectTypeToSearch(projectType: ProjectType): string {
function getEnvironmentFilterGroups(client: boolean, server: boolean): string[][] { function getEnvironmentFilterGroups(client: boolean, server: boolean): string[][] {
const groups: string[][] = [] const groups: string[][] = []
if (client && server) { if (client && server) {
groups.push( groups.push([
['client_side:required', 'client_side:optional', 'client_side:unsupported'], 'environment:client_only_server_optional',
['server_side:required', 'server_side:optional'], 'environment:server_only_client_optional',
) 'environment:client_and_server',
'environment:client_or_server',
'environment:client_or_server_prefers_both',
])
} else if (client) { } else if (client) {
groups.push( groups.push([
['client_side:optional', 'client_side:required'], 'environment:client_only',
['server_side:optional', 'server_side:unsupported'], 'environment:client_only_server_optional',
) 'environment:client_or_server_prefers_both',
'environment:client_or_server',
])
} else if (server) { } else if (server) {
groups.push(['server_side:optional', 'server_side:required']) groups.push([
'environment:server_only',
'environment:dedicated_server_only',
'environment:server_only_client_optional',
'environment:client_or_server_prefers_both',
'environment:client_or_server',
])
} }
return groups return groups
} }