From 79cc5fef722365357816f744c4f4853121c80763 Mon Sep 17 00:00:00 2001
From: Prospector <6166773+Prospector@users.noreply.github.com>
Date: Wed, 8 Jul 2026 07:30:47 -0700
Subject: [PATCH] feat: exclude project types filter (#6660)
* feat: exclude project types filter
* fix merge issue
---
apps/app-frontend/src/pages/Browse.vue | 15 ++
apps/app-frontend/src/store/theme.ts | 1 +
apps/frontend/src/composables/featureFlags.ts | 1 +
.../src/pages/discover/[type]/index.vue | 9 +
packages/app-lib/src/state/settings.rs | 1 +
.../components/search/SearchSidebarFilter.vue | 175 ++++++++++--------
.../browse-tab/providers/browse-manager.ts | 1 +
.../src/layouts/shared/browse-tab/sidebar.vue | 13 ++
packages/ui/src/locales/en-US/index.json | 12 ++
packages/ui/src/utils/search.ts | 77 +++++++-
10 files changed, 228 insertions(+), 77 deletions(-)
diff --git a/apps/app-frontend/src/pages/Browse.vue b/apps/app-frontend/src/pages/Browse.vue
index 0eeede917a..253d8430eb 100644
--- a/apps/app-frontend/src/pages/Browse.vue
+++ b/apps/app-frontend/src/pages/Browse.vue
@@ -49,6 +49,7 @@ import {
get_installed_project_ids as getInstalledProjectIds,
} from '@/helpers/instance'
import { get_loader_versions as getLoaderManifest } from '@/helpers/metadata'
+import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
import { get_instance_worlds } from '@/helpers/worlds'
import { injectContentInstall } from '@/providers/content-install'
@@ -1041,10 +1042,24 @@ function getProjectBrowseQuery() {
}
}
+const advancedFiltersCollapsed = computed({
+ get: () => themeStore.getFeatureFlag('advanced_filters_collapsed'),
+ set: (value) => {
+ themeStore.featureFlags['advanced_filters_collapsed'] = value
+ getSettings()
+ .then((settings) => {
+ settings.feature_flags['advanced_filters_collapsed'] = value
+ return setSettings(settings)
+ })
+ .catch(handleError)
+ },
+})
+
provideBrowseManager({
tags,
projectType,
...searchState,
+ advancedFiltersCollapsed,
getProjectLink: (result: Labrinth.Search.v2.ResultSearchProject) => ({
path: `/project/${result.project_id ?? result.slug}`,
query: getProjectBrowseQuery(),
diff --git a/apps/app-frontend/src/store/theme.ts b/apps/app-frontend/src/store/theme.ts
index 09d7f5189b..7c0d1624e3 100644
--- a/apps/app-frontend/src/store/theme.ts
+++ b/apps/app-frontend/src/store/theme.ts
@@ -16,6 +16,7 @@ export const DEFAULT_FEATURE_FLAGS = {
pride_fundraiser: true,
i18n_debug: false,
show_instance_play_time: true,
+ advanced_filters_collapsed: true,
}
export const THEME_OPTIONS = ['dark', 'light', 'oled', 'system'] as const
diff --git a/apps/frontend/src/composables/featureFlags.ts b/apps/frontend/src/composables/featureFlags.ts
index c6e7727e6c..956da2e300 100644
--- a/apps/frontend/src/composables/featureFlags.ts
+++ b/apps/frontend/src/composables/featureFlags.ts
@@ -58,6 +58,7 @@ export const DEFAULT_FEATURE_FLAGS = validateValues({
showHostingAccessInstanceAuditLog: false,
versionDevInfoCollapsed: true,
alwaysShowVersionDevInfo: false,
+ advancedFiltersCollapsed: true,
} as const)
export type FeatureFlag = keyof typeof DEFAULT_FEATURE_FLAGS
diff --git a/apps/frontend/src/pages/discover/[type]/index.vue b/apps/frontend/src/pages/discover/[type]/index.vue
index cd161f43bf..5e6721b8c2 100644
--- a/apps/frontend/src/pages/discover/[type]/index.vue
+++ b/apps/frontend/src/pages/discover/[type]/index.vue
@@ -372,6 +372,14 @@ const messages = defineMessages({
},
})
+const advancedFiltersCollapsed = computed({
+ get: () => flags.value.advancedFiltersCollapsed,
+ set: (value) => {
+ flags.value.advancedFiltersCollapsed = value
+ saveFeatureFlags()
+ },
+})
+
const projectTypeId = computed(() => projectType.value?.id ?? 'mod')
debug('projectTypeId:', projectTypeId.value)
@@ -478,6 +486,7 @@ provideBrowseManager({
showServerOnly: showServerOnlyToggle,
serverOnlyLabel: computed(() => formatMessage(commonMessages.serverOnlyLabel)),
hiddenFilterTypes: computed(() => (showServerOnlyToggle.value ? ['environment'] : [])),
+ advancedFiltersCollapsed,
displayMode: resultsDisplayMode,
cycleDisplayMode: cycleSearchDisplayMode,
maxResultsOptions: currentMaxResultsOptions,
diff --git a/packages/app-lib/src/state/settings.rs b/packages/app-lib/src/state/settings.rs
index 4a0343aaec..81df354664 100644
--- a/packages/app-lib/src/state/settings.rs
+++ b/packages/app-lib/src/state/settings.rs
@@ -64,6 +64,7 @@ pub enum FeatureFlag {
I18nDebug,
ShowInstancePlayTime,
SkipNonEssentialWarnings,
+ AdvancedFiltersCollapsed,
}
impl Settings {
diff --git a/packages/ui/src/components/search/SearchSidebarFilter.vue b/packages/ui/src/components/search/SearchSidebarFilter.vue
index 3fe20ce923..99ca2b54ec 100644
--- a/packages/ui/src/components/search/SearchSidebarFilter.vue
+++ b/packages/ui/src/components/search/SearchSidebarFilter.vue
@@ -71,83 +71,105 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ option.formatted_name ?? option.id }}
+
+
+
+
+
-
+
+
+ {{
+ showMore ? formatMessage(messages.showFewer) : formatMessage(messages.showMore)
+ }}
+
+
+
+
+
serverOnlyLabel?: ComputedRef
hiddenFilterTypes?: ComputedRef
+ advancedFiltersCollapsed?: Ref
onInstalled?: (projectId: string) => void
displayMode?: Ref<'list' | 'grid' | 'gallery'> | ComputedRef<'list' | 'grid' | 'gallery'>
diff --git a/packages/ui/src/layouts/shared/browse-tab/sidebar.vue b/packages/ui/src/layouts/shared/browse-tab/sidebar.vue
index a18fb7300c..29a78996ed 100644
--- a/packages/ui/src/layouts/shared/browse-tab/sidebar.vue
+++ b/packages/ui/src/layouts/shared/browse-tab/sidebar.vue
@@ -17,6 +17,14 @@ const isApp = computed(() => ctx.variant === 'app')
const lockedMessages = computed(() => toValue(ctx.lockedFilterMessages))
const hiddenFilterTypes = computed(() => ctx.hiddenFilterTypes?.value ?? [])
+const advancedFiltersCollapsed = computed(() => ctx.advancedFiltersCollapsed?.value ?? true)
+
+function setAdvancedFiltersCollapsed(collapsed: boolean) {
+ if (ctx.advancedFiltersCollapsed) {
+ ctx.advancedFiltersCollapsed.value = collapsed
+ }
+}
+
function closeFiltersMenu() {
if (ctx.filtersMenuOpen) {
ctx.filtersMenuOpen.value = false
@@ -49,6 +57,9 @@ function hasProvidedFilter(filterId: string): boolean {
}
function getFilterOpenByDefault(filterId: string): boolean {
+ if (filterId === 'advanced') {
+ return !advancedFiltersCollapsed.value
+ }
if (hasProvidedFilter(filterId)) {
return true
}
@@ -188,6 +199,8 @@ function getFilterOpenByDefault(filterId: string): boolean {
:content-class="contentClass"
:inner-panel-class="innerPanelClass"
:open-by-default="getFilterOpenByDefault(filter.id)"
+ @on-open="() => filter.id === 'advanced' && setAdvancedFiltersCollapsed(false)"
+ @on-close="() => filter.id === 'advanced' && setAdvancedFiltersCollapsed(true)"
>
diff --git a/packages/ui/src/locales/en-US/index.json b/packages/ui/src/locales/en-US/index.json
index 8f081ba1f6..440fb5f958 100644
--- a/packages/ui/src/locales/en-US/index.json
+++ b/packages/ui/src/locales/en-US/index.json
@@ -3572,6 +3572,18 @@
"search.filter.option.show_more": {
"defaultMessage": "Show more"
},
+ "search.filter_type.advanced": {
+ "defaultMessage": "Advanced"
+ },
+ "search.filter_type.advanced.exclude_datapack": {
+ "defaultMessage": "Exclude data packs"
+ },
+ "search.filter_type.advanced.exclude_mod": {
+ "defaultMessage": "Exclude mods"
+ },
+ "search.filter_type.advanced.exclude_plugin": {
+ "defaultMessage": "Exclude plugins"
+ },
"search.filter_type.environment": {
"defaultMessage": "Environment"
},
diff --git a/packages/ui/src/utils/search.ts b/packages/ui/src/utils/search.ts
index ea18519f96..5422a9b7fc 100644
--- a/packages/ui/src/utils/search.ts
+++ b/packages/ui/src/utils/search.ts
@@ -46,7 +46,7 @@ export type FilterType = {
ordering?: number
} & (
| {
- display: 'all' | 'scrollable' | 'none'
+ display: 'all' | 'scrollable' | 'none' | 'toggle'
}
| {
display: 'expandable'
@@ -112,6 +112,12 @@ export interface SortType {
const PLUGIN_PLATFORMS = ['bungeecord', 'waterfall', 'velocity', 'geyser']
+const PROJECT_TYPE_EXCLUSION_FILTERS: Partial> = {
+ mod: ['plugin', 'datapack'],
+ plugin: ['mod', 'datapack'],
+ datapack: ['mod', 'plugin'],
+}
+
export function useSearch(
projectTypes: Ref,
tags: Ref,
@@ -143,6 +149,34 @@ export function useSearch(
return formatCategory(formatMessage, categoryName)
}
+ const formatExcludeProjectTypeLabel = (projectType: ProjectType): string => {
+ switch (projectType) {
+ case 'mod':
+ return formatMessage(
+ defineMessage({
+ id: 'search.filter_type.advanced.exclude_mod',
+ defaultMessage: 'Exclude mods',
+ }),
+ )
+ case 'plugin':
+ return formatMessage(
+ defineMessage({
+ id: 'search.filter_type.advanced.exclude_plugin',
+ defaultMessage: 'Exclude plugins',
+ }),
+ )
+ case 'datapack':
+ return formatMessage(
+ defineMessage({
+ id: 'search.filter_type.advanced.exclude_datapack',
+ defaultMessage: 'Exclude data packs',
+ }),
+ )
+ default:
+ return projectType
+ }
+ }
+
const filters = computed(() => {
const categoryFilters: Record = {}
for (const category of sortedCategories(tags.value, formatCategoryName, locale.value)) {
@@ -171,6 +205,15 @@ export function useSearch(
})
}
+ const excludeableProjectTypes: ProjectType[] = []
+ for (const projectType of projectTypes.value) {
+ for (const target of PROJECT_TYPE_EXCLUSION_FILTERS[projectType] ?? []) {
+ if (!excludeableProjectTypes.includes(target)) {
+ excludeableProjectTypes.push(target)
+ }
+ }
+ }
+
const filterTypes: FilterType[] = [
...Object.values(categoryFilters),
{
@@ -429,6 +472,26 @@ export function useSearch(
options: [],
allows_custom_options: 'and',
},
+ {
+ id: 'advanced',
+ formatted_name: formatMessage(
+ defineMessage({
+ id: 'search.filter_type.advanced',
+ defaultMessage: 'Advanced',
+ }),
+ ),
+ supported_project_types: ['mod', 'plugin', 'datapack'],
+ display: 'toggle',
+ query_param: 'a',
+ searchable: false,
+ ordering: -1000,
+ options: excludeableProjectTypes.map((target) => ({
+ id: target,
+ formatted_name: formatExcludeProjectTypeLabel(target),
+ method: 'and',
+ value: `all_project_types:${mapProjectTypeToSearch(target)}`,
+ })),
+ },
]
return filterTypes
@@ -460,6 +523,9 @@ export function useSearch(
console.error(`Filter type ${filterValue.type} not found`)
continue
}
+ if (type.id === 'advanced') {
+ continue
+ }
let option = type?.options.find((option) => option.id === filterValue.option)
if (!option && type.allows_custom_options) {
option = {
@@ -550,6 +616,15 @@ export function useSearch(
parts.push(`project_types IN [${quoted}]`)
}
+ const excludedProjectTypes = filterValues
+ .filter((filterValue) => filterValue.type === 'advanced')
+ .map((filterValue) =>
+ formatSearchFilterValue(mapProjectTypeToSearch(filterValue.option as ProjectType)),
+ )
+ if (excludedProjectTypes.length > 0) {
+ parts.push(`all_project_types NOT IN [${excludedProjectTypes.join(', ')}]`)
+ }
+
return parts.join(' AND ')
})