refactor: move flags into settings, change icon (#5678)

* refactor: move flags into settings, change icon

* fix: use ButtonStyled for app
This commit is contained in:
Prospector
2026-03-26 14:10:01 -07:00
committed by GitHub
parent 381ea51cce
commit 36f62a3285
11 changed files with 137 additions and 106 deletions

View File

@@ -6,12 +6,13 @@ import {
LanguagesIcon, LanguagesIcon,
ModrinthIcon, ModrinthIcon,
PaintbrushIcon, PaintbrushIcon,
ReportIcon,
SettingsIcon, SettingsIcon,
ShieldIcon, ShieldIcon,
ToggleRightIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { import {
commonMessages, commonMessages,
commonSettingsMessages,
defineMessage, defineMessage,
defineMessages, defineMessages,
ProgressBar, ProgressBar,
@@ -95,11 +96,8 @@ const tabs = [
content: ResourceManagementSettings, content: ResourceManagementSettings,
}, },
{ {
name: defineMessage({ name: commonSettingsMessages.featureFlags,
id: 'app.settings.tabs.feature-flags', icon: ToggleRightIcon,
defaultMessage: 'Feature flags',
}),
icon: ReportIcon,
content: FeatureFlagSettings, content: FeatureFlagSettings,
developerOnly: true, developerOnly: true,
}, },

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { Toggle } from '@modrinth/ui' import { ButtonStyled, Toggle } from '@modrinth/ui'
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
import { get as getSettings, set as setSettings } from '@/helpers/settings.ts' import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
@@ -31,11 +31,20 @@ watch(
{{ option.replaceAll('_', ' ') }} {{ option.replaceAll('_', ' ') }}
</h2> </h2>
</div> </div>
<div class="flex items-center gap-2">
<Toggle <ButtonStyled type="transparent">
id="advanced-rendering" <button
:model-value="themeStore.getFeatureFlag(option)" :disabled="themeStore.getFeatureFlag(option) === DEFAULT_FEATURE_FLAGS[option]"
@update:model-value="() => setFeatureFlag(option, !themeStore.getFeatureFlag(option))" @click="setFeatureFlag(option, DEFAULT_FEATURE_FLAGS[option])"
/> >
Reset to default
</button>
</ButtonStyled>
<Toggle
id="advanced-rendering"
:model-value="themeStore.getFeatureFlag(option)"
@update:model-value="() => setFeatureFlag(option, !themeStore.getFeatureFlag(option))"
/>
</div>
</div> </div>
</template> </template>

View File

@@ -197,9 +197,6 @@
"app.settings.tabs.default-instance-options": { "app.settings.tabs.default-instance-options": {
"message": "Default instance options" "message": "Default instance options"
}, },
"app.settings.tabs.feature-flags": {
"message": "Feature flags"
},
"app.settings.tabs.java-installations": { "app.settings.tabs.java-installations": {
"message": "Java installations" "message": "Java installations"
}, },

View File

@@ -473,7 +473,8 @@
<SettingsIcon aria-hidden="true" /> {{ formatMessage(commonMessages.settingsLabel) }} <SettingsIcon aria-hidden="true" /> {{ formatMessage(commonMessages.settingsLabel) }}
</template> </template>
<template #flags> <template #flags>
<ReportIcon aria-hidden="true" /> {{ formatMessage(messages.featureFlags) }} <ToggleRightIcon aria-hidden="true" />
{{ formatMessage(commonSettingsMessages.featureFlags) }}
</template> </template>
<template #projects> <template #projects>
<BoxIcon aria-hidden="true" /> {{ formatMessage(messages.projects) }} <BoxIcon aria-hidden="true" /> {{ formatMessage(messages.projects) }}
@@ -585,9 +586,9 @@
<ScaleIcon aria-hidden="true" /> <ScaleIcon aria-hidden="true" />
{{ formatMessage(commonMessages.moderationLabel) }} {{ formatMessage(commonMessages.moderationLabel) }}
</NuxtLink> </NuxtLink>
<NuxtLink v-if="flags.developerMode" class="iconified-button" to="/flags"> <NuxtLink v-if="flags.developerMode" class="iconified-button" to="/settings/flags">
<ReportIcon aria-hidden="true" /> <ToggleRightIcon aria-hidden="true" />
{{ formatMessage(messages.featureFlags) }} {{ formatMessage(commonSettingsMessages.featureFlags) }}
</NuxtLink> </NuxtLink>
</template> </template>
<NuxtLink class="iconified-button" to="/settings"> <NuxtLink class="iconified-button" to="/settings">
@@ -724,6 +725,7 @@ import {
SettingsIcon, SettingsIcon,
ShieldAlertIcon, ShieldAlertIcon,
SunIcon, SunIcon,
ToggleRightIcon,
TransferIcon, TransferIcon,
UserIcon, UserIcon,
UserSearchIcon, UserSearchIcon,
@@ -734,6 +736,7 @@ import {
ButtonStyled, ButtonStyled,
commonMessages, commonMessages,
commonProjectTypeCategoryMessages, commonProjectTypeCategoryMessages,
commonSettingsMessages,
defineMessages, defineMessages,
injectModrinthClient, injectModrinthClient,
OverflowMenu, OverflowMenu,
@@ -918,10 +921,6 @@ const messages = defineMessages({
id: 'layout.nav.upgrade-to-modrinth-plus', id: 'layout.nav.upgrade-to-modrinth-plus',
defaultMessage: 'Upgrade to Modrinth+', defaultMessage: 'Upgrade to Modrinth+',
}, },
featureFlags: {
id: 'layout.nav.feature-flags',
defaultMessage: 'Feature flags',
},
projects: { projects: {
id: 'layout.nav.projects', id: 'layout.nav.projects',
defaultMessage: 'Projects', defaultMessage: 'Projects',
@@ -1045,7 +1044,7 @@ const userMenuOptions = computed(() => {
}, },
{ {
id: 'flags', id: 'flags',
link: '/flags', link: '/settings/flags',
shown: flags.value.developerMode, shown: flags.value.developerMode,
}, },
{ {

View File

@@ -1709,9 +1709,6 @@
"layout.nav.discover-content": { "layout.nav.discover-content": {
"message": "Discover content" "message": "Discover content"
}, },
"layout.nav.feature-flags": {
"message": "Feature flags"
},
"layout.nav.get-modrinth-app": { "layout.nav.get-modrinth-app": {
"message": "Get Modrinth App" "message": "Get Modrinth App"
}, },

View File

@@ -0,0 +1,6 @@
export default defineNuxtRouteMiddleware((to) => {
if (to.path.startsWith('/flags')) {
const target = to.fullPath.replace('/flags', '/settings/flags')
return navigateTo(target, { redirectCode: 301 })
}
})

View File

@@ -1,78 +0,0 @@
<script setup lang="ts">
import { SearchIcon } from '@modrinth/assets'
import { StyledInput, Toggle } from '@modrinth/ui'
import Fuse from 'fuse.js'
import { computed, ref, shallowReactive } from 'vue'
import {
DEFAULT_FEATURE_FLAGS,
type FeatureFlag,
saveFeatureFlags,
useFeatureFlags,
} from '~/composables/featureFlags.ts'
const flags = shallowReactive(useFeatureFlags().value)
const searchQuery = ref('')
const allFlags = computed(() => Object.keys(flags) as FeatureFlag[])
const fuse = computed(
() =>
new Fuse(allFlags.value, {
threshold: 0.4,
}),
)
const filteredFlags = computed(() => {
if (!searchQuery.value.trim()) {
return allFlags.value
}
return fuse.value.search(searchQuery.value).map((result) => result.item)
})
useSeoMeta({
robots: 'noindex',
})
</script>
<template>
<div class="mx-auto my-4 box-border w-[calc(100%-2rem)] max-w-[800px]">
<h1 class="mb-4 text-2xl font-bold text-contrast">Feature flags</h1>
<div class="mb-2">
<StyledInput
v-model="searchQuery"
type="search"
:icon="SearchIcon"
placeholder="Search flags..."
wrapper-class="w-full rounded-xl bg-bg-raised"
/>
</div>
<div class="flex flex-col gap-2">
<div
v-for="flag in filteredFlags"
:key="`flag-${flag}`"
class="flex flex-row flex-wrap items-center gap-2 rounded-2xl bg-bg-raised p-4"
>
<label :for="`toggle-${flag}`" class="flex-1">
<span class="block font-semibold capitalize">
{{ flag.replaceAll('_', ' ') }}
</span>
<p class="m-0 text-secondary">
Default:
<span :class="DEFAULT_FEATURE_FLAGS[flag] === false ? 'text-red' : 'text-green'">
{{ DEFAULT_FEATURE_FLAGS[flag] }}
</span>
</p>
</label>
<Toggle
:id="`toggle-${flag}`"
v-model="flags[flag]"
@update:model-value="() => saveFeatureFlags()"
/>
</div>
<p v-if="filteredFlags.length === 0" class="text-center text-secondary">
No flags found matching "{{ searchQuery }}"
</p>
</div>
</div>
</template>

View File

@@ -20,6 +20,13 @@
icon: LanguagesIcon, icon: LanguagesIcon,
badge: `${formatMessage(commonMessages.beta)}`, badge: `${formatMessage(commonMessages.beta)}`,
}, },
flags.developerMode
? {
link: '/settings/flags',
label: formatMessage(commonSettingsMessages.featureFlags),
icon: ToggleRightIcon,
}
: null,
auth.user ? { type: 'heading', label: formatMessage(messages.account) } : null, auth.user ? { type: 'heading', label: formatMessage(messages.account) } : null,
auth.user auth.user
? { ? {
@@ -91,6 +98,7 @@ import {
PaintbrushIcon, PaintbrushIcon,
ServerIcon, ServerIcon,
ShieldIcon, ShieldIcon,
ToggleRightIcon,
UserIcon, UserIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { commonMessages, commonSettingsMessages, defineMessages, useVIntl } from '@modrinth/ui' import { commonMessages, commonSettingsMessages, defineMessages, useVIntl } from '@modrinth/ui'
@@ -116,6 +124,7 @@ const messages = defineMessages({
const route = useNativeRoute() const route = useNativeRoute()
const auth = await useAuth() const auth = await useAuth()
const flags = useFeatureFlags()
useSeoMeta({ useSeoMeta({
robots: 'noindex', robots: 'noindex',

View File

@@ -0,0 +1,87 @@
<script setup lang="ts">
import { SearchIcon } from '@modrinth/assets'
import { ButtonStyled, StyledInput, Toggle } from '@modrinth/ui'
import Fuse from 'fuse.js'
import { computed, ref, shallowReactive } from 'vue'
import {
DEFAULT_FEATURE_FLAGS,
type FeatureFlag,
saveFeatureFlags,
useFeatureFlags,
} from '~/composables/featureFlags.ts'
const flags = shallowReactive(useFeatureFlags().value)
const searchQuery = ref('')
const allFlags = computed(() => Object.keys(flags) as FeatureFlag[])
function resetFlag(flag: FeatureFlag) {
flags[flag] = DEFAULT_FEATURE_FLAGS[flag]
saveFeatureFlags()
}
const fuse = computed(
() =>
new Fuse(allFlags.value, {
threshold: 0.4,
}),
)
const filteredFlags = computed(() => {
if (!searchQuery.value.trim()) {
return allFlags.value
}
return fuse.value.search(searchQuery.value).map((result) => result.item)
})
useSeoMeta({
robots: 'noindex',
})
</script>
<template>
<div class="mb-2">
<StyledInput
v-model="searchQuery"
type="search"
:icon="SearchIcon"
placeholder="Search flags..."
wrapper-class="w-full rounded-xl bg-bg-raised"
/>
</div>
<div class="flex flex-col gap-2">
<div
v-for="flag in filteredFlags"
:key="`flag-${flag}`"
class="flex flex-row flex-wrap items-center gap-2 rounded-2xl bg-bg-raised p-4"
>
<label :for="`toggle-${flag}`" class="flex-1">
<span class="block font-semibold capitalize">
{{ flag.replaceAll('_', ' ') }}
</span>
<p class="m-0 text-secondary">
Default:
<span :class="DEFAULT_FEATURE_FLAGS[flag] === false ? 'text-red' : 'text-green'">
{{ DEFAULT_FEATURE_FLAGS[flag] }}
</span>
</p>
</label>
<div class="flex items-center gap-2">
<ButtonStyled type="transparent">
<button :disabled="flags[flag] === DEFAULT_FEATURE_FLAGS[flag]" @click="resetFlag(flag)">
Reset to default
</button>
</ButtonStyled>
<Toggle
:id="`toggle-${flag}`"
v-model="flags[flag]"
@update:model-value="() => saveFeatureFlags()"
/>
</div>
</div>
<p v-if="filteredFlags.length === 0" class="text-center text-secondary">
No flags found matching "{{ searchQuery }}"
</p>
</div>
</template>

View File

@@ -2714,6 +2714,9 @@
"settings.display.theme.title": { "settings.display.theme.title": {
"defaultMessage": "Color theme" "defaultMessage": "Color theme"
}, },
"settings.feature-flags.title": {
"defaultMessage": "Feature flags"
},
"settings.language.categories.default": { "settings.language.categories.default": {
"defaultMessage": "Standard languages" "defaultMessage": "Standard languages"
}, },

View File

@@ -856,6 +856,10 @@ export const commonSettingsMessages = defineMessages({
id: 'settings.billing.title', id: 'settings.billing.title',
defaultMessage: 'Billing and subscriptions', defaultMessage: 'Billing and subscriptions',
}, },
featureFlags: {
id: 'settings.feature-flags.title',
defaultMessage: 'Feature flags',
},
language: { language: {
id: 'settings.language.title', id: 'settings.language.title',
defaultMessage: 'Language', defaultMessage: 'Language',