mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-19 22:34:15 +00:00
## Summary - Adds `settingId` parameter to `showSettingsDialog` that auto-navigates to the correct category tab, scrolls to the setting, and briefly highlights it with a CSS pulse animation - Adds `data-setting-id` attributes to setting items for stable DOM targeting - Adds "Don't show this again" checkbox with "Re-enable in Settings" deep-link to the missing nodes dialog - Adds "Re-enable in Settings" deep-link to missing models and blueprint overwrite "Don't show this again" checkboxes - Fixes #3437 ## Test plan - [x] `pnpm typecheck` passes - [x] `pnpm lint` passes - [x] Unit tests pass (59/59 including 5 new tests for `useSettingUI`) https://github.com/user-attachments/assets/a9e80aea-7b69-4686-b030-55a2e0570ff0 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8761-feat-scroll-to-specific-setting-when-opening-settings-dialog-3036d73d365081d18d9afe9f9ed41ebc) by [Unito](https://www.unito.io)
304 lines
8.7 KiB
Vue
304 lines
8.7 KiB
Vue
<template>
|
|
<div
|
|
data-testid="settings-dialog"
|
|
:class="
|
|
teamWorkspacesEnabled
|
|
? 'flex h-full w-full overflow-auto flex-col md:flex-row'
|
|
: 'settings-container'
|
|
"
|
|
>
|
|
<ScrollPanel
|
|
:class="
|
|
teamWorkspacesEnabled
|
|
? 'w-full md:w-64 md:min-w-64 md:max-w-64 shrink-0 p-2'
|
|
: 'settings-sidebar w-48 shrink-0 p-2 2xl:w-64'
|
|
"
|
|
>
|
|
<div :class="teamWorkspacesEnabled ? 'px-4' : ''">
|
|
<SearchBox
|
|
v-model:model-value="searchQuery"
|
|
class="settings-search-box mb-2 w-full"
|
|
:placeholder="
|
|
$t('g.searchPlaceholder', { subject: $t('g.settings') })
|
|
"
|
|
:debounce-time="128"
|
|
autofocus
|
|
@search="handleSearch"
|
|
/>
|
|
</div>
|
|
<Listbox
|
|
v-model="activeCategory"
|
|
:options="groupedMenuTreeNodes"
|
|
option-label="translatedLabel"
|
|
option-group-label="label"
|
|
option-group-children="children"
|
|
scroll-height="100%"
|
|
:option-disabled="
|
|
(option: SettingTreeNode) =>
|
|
!queryIsEmpty && !searchResultsCategories.has(option.label ?? '')
|
|
"
|
|
:class="
|
|
teamWorkspacesEnabled
|
|
? 'w-full border-none bg-transparent'
|
|
: 'w-full border-none'
|
|
"
|
|
>
|
|
<!-- Workspace mode: custom group headers -->
|
|
<template v-if="teamWorkspacesEnabled" #optiongroup="{ option }">
|
|
<h3 class="text-xs font-semibold uppercase text-muted m-0 pt-6 pb-2">
|
|
{{ option.translatedLabel ?? option.label }}
|
|
</h3>
|
|
</template>
|
|
<!-- Legacy mode: divider between groups -->
|
|
<template v-else #optiongroup>
|
|
<Divider class="my-0" />
|
|
</template>
|
|
<!-- Custom option template with data-testid for stable test selectors -->
|
|
<template #option="{ option }">
|
|
<span
|
|
:data-testid="`settings-tab-${option.key}`"
|
|
class="settings-tab-option"
|
|
>
|
|
<WorkspaceSidebarItem
|
|
v-if="teamWorkspacesEnabled && option.key === 'workspace'"
|
|
/>
|
|
<template v-else>{{ option.translatedLabel }}</template>
|
|
</span>
|
|
</template>
|
|
</Listbox>
|
|
</ScrollPanel>
|
|
<Divider layout="vertical" class="mx-1 hidden md:flex 2xl:mx-4" />
|
|
<Divider layout="horizontal" class="flex md:hidden" />
|
|
<Tabs
|
|
:value="tabValue"
|
|
:lazy="true"
|
|
:class="
|
|
teamWorkspacesEnabled
|
|
? 'h-full flex-1 overflow-auto scrollbar-custom'
|
|
: 'settings-content h-full w-full'
|
|
"
|
|
>
|
|
<TabPanels class="settings-tab-panels h-full w-full pr-0">
|
|
<PanelTemplate value="Search Results">
|
|
<SettingsPanel :setting-groups="searchResults" />
|
|
</PanelTemplate>
|
|
|
|
<PanelTemplate
|
|
v-for="category in settingCategories"
|
|
:key="category.key"
|
|
:value="category.label ?? ''"
|
|
>
|
|
<template #header>
|
|
<CurrentUserMessage v-if="tabValue === 'Comfy'" />
|
|
<ColorPaletteMessage v-if="tabValue === 'Appearance'" />
|
|
</template>
|
|
<SettingsPanel :setting-groups="sortedGroups(category)" />
|
|
</PanelTemplate>
|
|
|
|
<Suspense v-for="panel in panels" :key="panel.node.key">
|
|
<component :is="panel.component" v-bind="panel.props" />
|
|
<template #fallback>
|
|
<div>{{ $t('g.loadingPanel', { panel: panel.node.label }) }}</div>
|
|
</template>
|
|
</Suspense>
|
|
</TabPanels>
|
|
</Tabs>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import Divider from 'primevue/divider'
|
|
import Listbox from 'primevue/listbox'
|
|
import ScrollPanel from 'primevue/scrollpanel'
|
|
import TabPanels from 'primevue/tabpanels'
|
|
import Tabs from 'primevue/tabs'
|
|
import { computed, nextTick, onBeforeUnmount, watch } from 'vue'
|
|
|
|
import SearchBox from '@/components/common/SearchBox.vue'
|
|
import CurrentUserMessage from '@/components/dialog/content/setting/CurrentUserMessage.vue'
|
|
import PanelTemplate from '@/components/dialog/content/setting/PanelTemplate.vue'
|
|
import WorkspaceSidebarItem from '@/components/dialog/content/setting/WorkspaceSidebarItem.vue'
|
|
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
|
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
|
import { isCloud } from '@/platform/distribution/types'
|
|
import ColorPaletteMessage from '@/platform/settings/components/ColorPaletteMessage.vue'
|
|
import SettingsPanel from '@/platform/settings/components/SettingsPanel.vue'
|
|
import { useSettingSearch } from '@/platform/settings/composables/useSettingSearch'
|
|
import { useSettingUI } from '@/platform/settings/composables/useSettingUI'
|
|
import type { SettingTreeNode } from '@/platform/settings/settingStore'
|
|
import type { ISettingGroup, SettingParams } from '@/platform/settings/types'
|
|
import { flattenTree } from '@/utils/treeUtil'
|
|
|
|
const { defaultPanel, scrollToSettingId } = defineProps<{
|
|
defaultPanel?:
|
|
| 'about'
|
|
| 'keybinding'
|
|
| 'extension'
|
|
| 'server-config'
|
|
| 'user'
|
|
| 'credits'
|
|
| 'subscription'
|
|
| 'workspace'
|
|
| 'secrets'
|
|
scrollToSettingId?: string
|
|
}>()
|
|
|
|
const { flags } = useFeatureFlags()
|
|
const teamWorkspacesEnabled = computed(
|
|
() => isCloud && flags.teamWorkspacesEnabled
|
|
)
|
|
|
|
const {
|
|
activeCategory,
|
|
defaultCategory,
|
|
settingCategories,
|
|
groupedMenuTreeNodes,
|
|
panels
|
|
} = useSettingUI(defaultPanel, scrollToSettingId)
|
|
|
|
const {
|
|
searchQuery,
|
|
searchResultsCategories,
|
|
queryIsEmpty,
|
|
inSearch,
|
|
handleSearch: handleSearchBase,
|
|
getSearchResults
|
|
} = useSettingSearch()
|
|
|
|
const authActions = useFirebaseAuthActions()
|
|
|
|
// Get max sortOrder from settings in a group
|
|
const getGroupSortOrder = (group: SettingTreeNode): number =>
|
|
Math.max(0, ...flattenTree<SettingParams>(group).map((s) => s.sortOrder ?? 0))
|
|
|
|
// Sort groups for a category
|
|
const sortedGroups = (category: SettingTreeNode): ISettingGroup[] => {
|
|
return [...(category.children ?? [])]
|
|
.sort((a, b) => {
|
|
const orderDiff = getGroupSortOrder(b) - getGroupSortOrder(a)
|
|
return orderDiff !== 0 ? orderDiff : a.label.localeCompare(b.label)
|
|
})
|
|
.map((group) => ({
|
|
label: group.label,
|
|
settings: flattenTree<SettingParams>(group).sort((a, b) => {
|
|
const sortOrderA = a.sortOrder ?? 0
|
|
const sortOrderB = b.sortOrder ?? 0
|
|
|
|
return sortOrderB - sortOrderA
|
|
})
|
|
}))
|
|
}
|
|
|
|
const handleSearch = (query: string) => {
|
|
handleSearchBase(query.trim())
|
|
activeCategory.value = query ? null : defaultCategory.value
|
|
}
|
|
|
|
// Get search results
|
|
const searchResults = computed<ISettingGroup[]>(() =>
|
|
getSearchResults(activeCategory.value)
|
|
)
|
|
|
|
const tabValue = computed<string>(() =>
|
|
inSearch.value ? 'Search Results' : (activeCategory.value?.label ?? '')
|
|
)
|
|
|
|
// Scroll to and highlight the target setting once the correct tab renders.
|
|
if (scrollToSettingId) {
|
|
const stopScrollWatch = watch(
|
|
tabValue,
|
|
() => {
|
|
void nextTick(() => {
|
|
const el = document.querySelector(
|
|
`[data-setting-id="${CSS.escape(scrollToSettingId)}"]`
|
|
)
|
|
if (!el) return
|
|
stopScrollWatch()
|
|
el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
el.classList.add('setting-highlight')
|
|
el.addEventListener(
|
|
'animationend',
|
|
() => el.classList.remove('setting-highlight'),
|
|
{ once: true }
|
|
)
|
|
})
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
onBeforeUnmount(stopScrollWatch)
|
|
}
|
|
|
|
// Don't allow null category to be set outside of search.
|
|
// In search mode, the active category can be null to show all search results.
|
|
watch(activeCategory, (_, oldValue) => {
|
|
if (!tabValue.value) {
|
|
activeCategory.value = oldValue
|
|
}
|
|
if (activeCategory.value?.key === 'credits') {
|
|
void authActions.fetchBalance()
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<style>
|
|
.settings-tab-panels {
|
|
padding-top: 0 !important;
|
|
}
|
|
|
|
@media (prefers-reduced-motion: no-preference) {
|
|
.setting-highlight {
|
|
animation: setting-highlight-pulse 1.5s ease-in-out;
|
|
}
|
|
}
|
|
|
|
@keyframes setting-highlight-pulse {
|
|
0%,
|
|
100% {
|
|
background-color: transparent;
|
|
}
|
|
30% {
|
|
background-color: color-mix(
|
|
in srgb,
|
|
var(--p-primary-color) 15%,
|
|
transparent
|
|
);
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<style scoped>
|
|
/* Legacy mode styles (when teamWorkspacesEnabled is false) */
|
|
.settings-container {
|
|
display: flex;
|
|
height: 70vh;
|
|
width: 60vw;
|
|
max-width: 64rem;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.settings-content {
|
|
overflow-x: auto;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.settings-container {
|
|
flex-direction: column;
|
|
height: auto;
|
|
width: 80vw;
|
|
}
|
|
|
|
.settings-sidebar {
|
|
width: 100%;
|
|
}
|
|
|
|
.settings-content {
|
|
height: 350px;
|
|
}
|
|
}
|
|
|
|
/* Hide the first group separator in legacy mode */
|
|
.settings-sidebar :deep(.p-listbox-option-group:nth-child(1)) {
|
|
display: none;
|
|
}
|
|
</style>
|