mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 18:52:19 +00:00
feat: scroll to specific setting when opening settings dialog (#8761)
## 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)
This commit is contained in:
committed by
GitHub
parent
19a724710c
commit
e411a104f4
@@ -18,17 +18,35 @@
|
|||||||
<div class="flex justify-end gap-4">
|
<div class="flex justify-end gap-4">
|
||||||
<div
|
<div
|
||||||
v-if="type === 'overwriteBlueprint'"
|
v-if="type === 'overwriteBlueprint'"
|
||||||
class="flex justify-start gap-4"
|
class="flex flex-col justify-start gap-1"
|
||||||
>
|
>
|
||||||
<Checkbox
|
<div class="flex gap-4">
|
||||||
v-model="doNotAskAgain"
|
<input
|
||||||
class="flex justify-start gap-4"
|
id="doNotAskAgain"
|
||||||
input-id="doNotAskAgain"
|
v-model="doNotAskAgain"
|
||||||
binary
|
type="checkbox"
|
||||||
/>
|
class="h-4 w-4 cursor-pointer"
|
||||||
<label for="doNotAskAgain" severity="secondary">{{
|
/>
|
||||||
t('missingModelsDialog.doNotAskAgain')
|
<label for="doNotAskAgain">{{
|
||||||
}}</label>
|
t('missingModelsDialog.doNotAskAgain')
|
||||||
|
}}</label>
|
||||||
|
</div>
|
||||||
|
<i18n-t
|
||||||
|
v-if="doNotAskAgain"
|
||||||
|
keypath="missingModelsDialog.reEnableInSettings"
|
||||||
|
tag="span"
|
||||||
|
class="text-sm text-muted-foreground ml-8"
|
||||||
|
>
|
||||||
|
<template #link>
|
||||||
|
<Button
|
||||||
|
variant="textonly"
|
||||||
|
class="underline cursor-pointer p-0 text-sm text-muted-foreground hover:bg-transparent"
|
||||||
|
@click="openBlueprintOverwriteSetting"
|
||||||
|
>
|
||||||
|
{{ t('missingModelsDialog.reEnableInSettingsLink') }}
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@@ -92,13 +110,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Checkbox from 'primevue/checkbox'
|
|
||||||
import Message from 'primevue/message'
|
import Message from 'primevue/message'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import Button from '@/components/ui/button/Button.vue'
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||||
|
import { useDialogService } from '@/services/dialogService'
|
||||||
import type { ConfirmationDialogType } from '@/services/dialogService'
|
import type { ConfirmationDialogType } from '@/services/dialogService'
|
||||||
import { useDialogStore } from '@/stores/dialogStore'
|
import { useDialogStore } from '@/stores/dialogStore'
|
||||||
|
|
||||||
@@ -114,6 +132,14 @@ const { t } = useI18n()
|
|||||||
|
|
||||||
const onCancel = () => useDialogStore().closeDialog()
|
const onCancel = () => useDialogStore().closeDialog()
|
||||||
|
|
||||||
|
function openBlueprintOverwriteSetting() {
|
||||||
|
useDialogStore().closeDialog()
|
||||||
|
void useDialogService().showSettingsDialog(
|
||||||
|
undefined,
|
||||||
|
'Comfy.Workflow.WarnBlueprintOverwrite'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const doNotAskAgain = ref(false)
|
const doNotAskAgain = ref(false)
|
||||||
|
|
||||||
const onDeny = () => {
|
const onDeny = () => {
|
||||||
|
|||||||
@@ -5,11 +5,34 @@
|
|||||||
:title="t('missingModelsDialog.missingModels')"
|
:title="t('missingModelsDialog.missingModels')"
|
||||||
:message="t('missingModelsDialog.missingModelsMessage')"
|
:message="t('missingModelsDialog.missingModelsMessage')"
|
||||||
/>
|
/>
|
||||||
<div class="mb-4 flex gap-1">
|
<div class="mb-4 flex flex-col gap-1">
|
||||||
<Checkbox v-model="doNotAskAgain" binary input-id="doNotAskAgain" />
|
<div class="flex gap-1">
|
||||||
<label for="doNotAskAgain">{{
|
<input
|
||||||
t('missingModelsDialog.doNotAskAgain')
|
id="doNotAskAgain"
|
||||||
}}</label>
|
v-model="doNotAskAgain"
|
||||||
|
type="checkbox"
|
||||||
|
class="h-4 w-4 cursor-pointer"
|
||||||
|
/>
|
||||||
|
<label for="doNotAskAgain">{{
|
||||||
|
t('missingModelsDialog.doNotAskAgain')
|
||||||
|
}}</label>
|
||||||
|
</div>
|
||||||
|
<i18n-t
|
||||||
|
v-if="doNotAskAgain"
|
||||||
|
keypath="missingModelsDialog.reEnableInSettings"
|
||||||
|
tag="span"
|
||||||
|
class="text-sm text-muted-foreground ml-6"
|
||||||
|
>
|
||||||
|
<template #link>
|
||||||
|
<Button
|
||||||
|
variant="textonly"
|
||||||
|
class="underline cursor-pointer p-0 text-sm text-muted-foreground hover:bg-transparent"
|
||||||
|
@click="openShowMissingModelsSetting"
|
||||||
|
>
|
||||||
|
{{ t('missingModelsDialog.reEnableInSettingsLink') }}
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
</div>
|
</div>
|
||||||
<ListBox :options="missingModels" class="comfy-missing-models">
|
<ListBox :options="missingModels" class="comfy-missing-models">
|
||||||
<template #option="{ option }">
|
<template #option="{ option }">
|
||||||
@@ -31,16 +54,18 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Checkbox from 'primevue/checkbox'
|
|
||||||
import ListBox from 'primevue/listbox'
|
import ListBox from 'primevue/listbox'
|
||||||
import { computed, onBeforeUnmount, ref } from 'vue'
|
import { computed, onBeforeUnmount, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
import ElectronFileDownload from '@/components/common/ElectronFileDownload.vue'
|
import ElectronFileDownload from '@/components/common/ElectronFileDownload.vue'
|
||||||
import FileDownload from '@/components/common/FileDownload.vue'
|
import FileDownload from '@/components/common/FileDownload.vue'
|
||||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
|
||||||
import { isDesktop } from '@/platform/distribution/types'
|
import { isDesktop } from '@/platform/distribution/types'
|
||||||
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||||
|
import { useDialogService } from '@/services/dialogService'
|
||||||
|
import { useDialogStore } from '@/stores/dialogStore'
|
||||||
|
|
||||||
// TODO: Read this from server internal API rather than hardcoding here
|
// TODO: Read this from server internal API rather than hardcoding here
|
||||||
// as some installations may wish to use custom sources
|
// as some installations may wish to use custom sources
|
||||||
@@ -78,6 +103,14 @@ const { t } = useI18n()
|
|||||||
|
|
||||||
const doNotAskAgain = ref(false)
|
const doNotAskAgain = ref(false)
|
||||||
|
|
||||||
|
function openShowMissingModelsSetting() {
|
||||||
|
useDialogStore().closeDialog({ key: 'global-missing-models-warning' })
|
||||||
|
void useDialogService().showSettingsDialog(
|
||||||
|
undefined,
|
||||||
|
'Comfy.Workflow.ShowMissingModelsWarning'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const modelDownloads = ref<Record<string, ModelInfo>>({})
|
const modelDownloads = ref<Record<string, ModelInfo>>({})
|
||||||
const missingModels = computed(() => {
|
const missingModels = computed(() => {
|
||||||
return props.missingModels.map((model) => {
|
return props.missingModels.map((model) => {
|
||||||
|
|||||||
@@ -1,55 +1,86 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- Cloud mode: Learn More + Got It buttons -->
|
<div class="flex w-full flex-col gap-2 py-2 px-4">
|
||||||
<div
|
<div class="flex flex-col gap-1 text-sm text-muted-foreground">
|
||||||
v-if="isCloud"
|
<div class="flex items-center gap-1">
|
||||||
class="flex w-full items-center justify-between gap-2 py-2 px-4"
|
<input
|
||||||
>
|
id="doNotAskAgainNodes"
|
||||||
<Button
|
v-model="doNotAskAgain"
|
||||||
variant="textonly"
|
type="checkbox"
|
||||||
size="sm"
|
class="h-4 w-4 cursor-pointer"
|
||||||
as="a"
|
/>
|
||||||
href="https://www.comfy.org/cloud"
|
<label for="doNotAskAgainNodes">{{
|
||||||
target="_blank"
|
$t('missingModelsDialog.doNotAskAgain')
|
||||||
rel="noopener noreferrer"
|
}}</label>
|
||||||
>
|
</div>
|
||||||
<i class="icon-[lucide--info]"></i>
|
<i18n-t
|
||||||
<span>{{ $t('missingNodes.cloud.learnMore') }}</span>
|
v-if="doNotAskAgain"
|
||||||
</Button>
|
keypath="missingModelsDialog.reEnableInSettings"
|
||||||
<Button variant="secondary" size="md" @click="handleGotItClick">{{
|
tag="span"
|
||||||
$t('missingNodes.cloud.gotIt')
|
class="text-sm text-muted-foreground ml-6"
|
||||||
}}</Button>
|
>
|
||||||
</div>
|
<template #link>
|
||||||
|
<Button
|
||||||
|
variant="textonly"
|
||||||
|
class="underline cursor-pointer p-0 text-sm text-muted-foreground hover:bg-transparent"
|
||||||
|
@click="openShowMissingNodesSetting"
|
||||||
|
>
|
||||||
|
{{ $t('missingModelsDialog.reEnableInSettingsLink') }}
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- OSS mode: Open Manager + Install All buttons -->
|
<!-- Cloud mode: Learn More + Got It buttons -->
|
||||||
<div v-else-if="showManagerButtons" class="flex justify-end gap-1 py-2 px-4">
|
<div v-if="isCloud" class="flex w-full items-center justify-between gap-2">
|
||||||
<Button variant="textonly" @click="openManager">{{
|
<Button
|
||||||
$t('g.openManager')
|
variant="textonly"
|
||||||
}}</Button>
|
size="sm"
|
||||||
<PackInstallButton
|
as="a"
|
||||||
v-if="showInstallAllButton"
|
href="https://www.comfy.org/cloud"
|
||||||
type="secondary"
|
target="_blank"
|
||||||
size="md"
|
rel="noopener noreferrer"
|
||||||
:disabled="
|
>
|
||||||
isLoading || !!error || missingNodePacks.length === 0 || isInstalling
|
<i class="icon-[lucide--info]"></i>
|
||||||
"
|
<span>{{ $t('missingNodes.cloud.learnMore') }}</span>
|
||||||
:is-loading="isLoading"
|
</Button>
|
||||||
:node-packs="missingNodePacks"
|
<Button variant="secondary" size="md" @click="handleGotItClick">{{
|
||||||
:label="
|
$t('missingNodes.cloud.gotIt')
|
||||||
isLoading
|
}}</Button>
|
||||||
? $t('manager.gettingInfo')
|
</div>
|
||||||
: $t('manager.installAllMissingNodes')
|
|
||||||
"
|
<!-- OSS mode: Open Manager + Install All buttons -->
|
||||||
/>
|
<div v-else-if="showManagerButtons" class="flex justify-end gap-1">
|
||||||
|
<Button variant="textonly" @click="openManager">{{
|
||||||
|
$t('g.openManager')
|
||||||
|
}}</Button>
|
||||||
|
<PackInstallButton
|
||||||
|
v-if="showInstallAllButton"
|
||||||
|
type="secondary"
|
||||||
|
size="md"
|
||||||
|
:disabled="
|
||||||
|
isLoading || !!error || missingNodePacks.length === 0 || isInstalling
|
||||||
|
"
|
||||||
|
:is-loading="isLoading"
|
||||||
|
:node-packs="missingNodePacks"
|
||||||
|
:label="
|
||||||
|
isLoading
|
||||||
|
? $t('manager.gettingInfo')
|
||||||
|
: $t('manager.installAllMissingNodes')
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, watch } from 'vue'
|
import { computed, nextTick, ref, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import Button from '@/components/ui/button/Button.vue'
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
import { isCloud } from '@/platform/distribution/types'
|
import { isCloud } from '@/platform/distribution/types'
|
||||||
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||||
|
import { useDialogService } from '@/services/dialogService'
|
||||||
import { useDialogStore } from '@/stores/dialogStore'
|
import { useDialogStore } from '@/stores/dialogStore'
|
||||||
import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue'
|
import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue'
|
||||||
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
|
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
|
||||||
@@ -60,10 +91,24 @@ import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTyp
|
|||||||
const dialogStore = useDialogStore()
|
const dialogStore = useDialogStore()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const doNotAskAgain = ref(false)
|
||||||
|
|
||||||
|
watch(doNotAskAgain, (value) => {
|
||||||
|
void useSettingStore().set('Comfy.Workflow.ShowMissingNodesWarning', !value)
|
||||||
|
})
|
||||||
|
|
||||||
const handleGotItClick = () => {
|
const handleGotItClick = () => {
|
||||||
dialogStore.closeDialog({ key: 'global-missing-nodes' })
|
dialogStore.closeDialog({ key: 'global-missing-nodes' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openShowMissingNodesSetting() {
|
||||||
|
dialogStore.closeDialog({ key: 'global-missing-nodes' })
|
||||||
|
void useDialogService().showSettingsDialog(
|
||||||
|
undefined,
|
||||||
|
'Comfy.Workflow.ShowMissingNodesWarning'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const { missingNodePacks, isLoading, error } = useMissingNodes()
|
const { missingNodePacks, isLoading, error } = useMissingNodes()
|
||||||
const comfyManagerStore = useComfyManagerStore()
|
const comfyManagerStore = useComfyManagerStore()
|
||||||
const managerState = useManagerState()
|
const managerState = useManagerState()
|
||||||
|
|||||||
@@ -1691,6 +1691,8 @@
|
|||||||
},
|
},
|
||||||
"missingModelsDialog": {
|
"missingModelsDialog": {
|
||||||
"doNotAskAgain": "Don't show this again",
|
"doNotAskAgain": "Don't show this again",
|
||||||
|
"reEnableInSettings": "Re-enable in {link}",
|
||||||
|
"reEnableInSettingsLink": "Settings",
|
||||||
"missingModels": "Missing Models",
|
"missingModels": "Missing Models",
|
||||||
"missingModelsMessage": "When loading the graph, the following models were not found"
|
"missingModelsMessage": "When loading the graph, the following models were not found"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ import Listbox from 'primevue/listbox'
|
|||||||
import ScrollPanel from 'primevue/scrollpanel'
|
import ScrollPanel from 'primevue/scrollpanel'
|
||||||
import TabPanels from 'primevue/tabpanels'
|
import TabPanels from 'primevue/tabpanels'
|
||||||
import Tabs from 'primevue/tabs'
|
import Tabs from 'primevue/tabs'
|
||||||
import { computed, watch } from 'vue'
|
import { computed, nextTick, onBeforeUnmount, watch } from 'vue'
|
||||||
|
|
||||||
import SearchBox from '@/components/common/SearchBox.vue'
|
import SearchBox from '@/components/common/SearchBox.vue'
|
||||||
import CurrentUserMessage from '@/components/dialog/content/setting/CurrentUserMessage.vue'
|
import CurrentUserMessage from '@/components/dialog/content/setting/CurrentUserMessage.vue'
|
||||||
@@ -129,7 +129,7 @@ import type { SettingTreeNode } from '@/platform/settings/settingStore'
|
|||||||
import type { ISettingGroup, SettingParams } from '@/platform/settings/types'
|
import type { ISettingGroup, SettingParams } from '@/platform/settings/types'
|
||||||
import { flattenTree } from '@/utils/treeUtil'
|
import { flattenTree } from '@/utils/treeUtil'
|
||||||
|
|
||||||
const { defaultPanel } = defineProps<{
|
const { defaultPanel, scrollToSettingId } = defineProps<{
|
||||||
defaultPanel?:
|
defaultPanel?:
|
||||||
| 'about'
|
| 'about'
|
||||||
| 'keybinding'
|
| 'keybinding'
|
||||||
@@ -140,6 +140,7 @@ const { defaultPanel } = defineProps<{
|
|||||||
| 'subscription'
|
| 'subscription'
|
||||||
| 'workspace'
|
| 'workspace'
|
||||||
| 'secrets'
|
| 'secrets'
|
||||||
|
scrollToSettingId?: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { flags } = useFeatureFlags()
|
const { flags } = useFeatureFlags()
|
||||||
@@ -153,7 +154,7 @@ const {
|
|||||||
settingCategories,
|
settingCategories,
|
||||||
groupedMenuTreeNodes,
|
groupedMenuTreeNodes,
|
||||||
panels
|
panels
|
||||||
} = useSettingUI(defaultPanel)
|
} = useSettingUI(defaultPanel, scrollToSettingId)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
searchQuery,
|
searchQuery,
|
||||||
@@ -202,6 +203,31 @@ const tabValue = computed<string>(() =>
|
|||||||
inSearch.value ? 'Search Results' : (activeCategory.value?.label ?? '')
|
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.
|
// 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.
|
// In search mode, the active category can be null to show all search results.
|
||||||
watch(activeCategory, (_, oldValue) => {
|
watch(activeCategory, (_, oldValue) => {
|
||||||
@@ -218,6 +244,26 @@ watch(activeCategory, (_, oldValue) => {
|
|||||||
.settings-tab-panels {
|
.settings-tab-panels {
|
||||||
padding-top: 0 !important;
|
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>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
<div
|
<div
|
||||||
v-for="setting in group.settings.filter((s) => !s.deprecated)"
|
v-for="setting in group.settings.filter((s) => !s.deprecated)"
|
||||||
:key="setting.id"
|
:key="setting.id"
|
||||||
|
:data-setting-id="setting.id"
|
||||||
class="setting-item mb-4"
|
class="setting-item mb-4"
|
||||||
>
|
>
|
||||||
<SettingItem :setting="setting" />
|
<SettingItem :setting="setting" />
|
||||||
|
|||||||
140
src/platform/settings/composables/useSettingUI.test.ts
Normal file
140
src/platform/settings/composables/useSettingUI.test.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import { createTestingPinia } from '@pinia/testing'
|
||||||
|
import { setActivePinia } from 'pinia'
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import {
|
||||||
|
getSettingInfo,
|
||||||
|
useSettingStore
|
||||||
|
} from '@/platform/settings/settingStore'
|
||||||
|
import type { SettingTreeNode } from '@/platform/settings/settingStore'
|
||||||
|
|
||||||
|
import { useSettingUI } from './useSettingUI'
|
||||||
|
|
||||||
|
vi.mock('vue-i18n', () => ({
|
||||||
|
useI18n: () => ({ t: (_: string, fallback: string) => fallback })
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/composables/auth/useCurrentUser', () => ({
|
||||||
|
useCurrentUser: () => ({ isLoggedIn: ref(false) })
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/composables/billing/useBillingContext', () => ({
|
||||||
|
useBillingContext: () => ({ isActiveSubscription: ref(false) })
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/composables/useFeatureFlags', () => ({
|
||||||
|
useFeatureFlags: () => ({
|
||||||
|
flags: { teamWorkspacesEnabled: false, userSecretsEnabled: false }
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/composables/useVueFeatureFlags', () => ({
|
||||||
|
useVueFeatureFlags: () => ({ shouldRenderVueNodes: ref(false) })
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/platform/distribution/types', () => ({
|
||||||
|
isCloud: false,
|
||||||
|
isDesktop: false
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/platform/settings/settingStore', () => ({
|
||||||
|
useSettingStore: vi.fn(),
|
||||||
|
getSettingInfo: vi.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
|
interface MockSettingParams {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
defaultValue: unknown
|
||||||
|
category?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('useSettingUI', () => {
|
||||||
|
const mockSettings: Record<string, MockSettingParams> = {
|
||||||
|
'Comfy.Locale': {
|
||||||
|
id: 'Comfy.Locale',
|
||||||
|
name: 'Locale',
|
||||||
|
type: 'combo',
|
||||||
|
defaultValue: 'en'
|
||||||
|
},
|
||||||
|
'LiteGraph.Zoom': {
|
||||||
|
id: 'LiteGraph.Zoom',
|
||||||
|
name: 'Zoom',
|
||||||
|
type: 'slider',
|
||||||
|
defaultValue: 1
|
||||||
|
},
|
||||||
|
'Appearance.Theme': {
|
||||||
|
id: 'Appearance.Theme',
|
||||||
|
name: 'Theme',
|
||||||
|
type: 'combo',
|
||||||
|
defaultValue: 'dark'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePinia(createTestingPinia())
|
||||||
|
vi.clearAllMocks()
|
||||||
|
|
||||||
|
vi.mocked(useSettingStore).mockReturnValue({
|
||||||
|
settingsById: mockSettings
|
||||||
|
} as ReturnType<typeof useSettingStore>)
|
||||||
|
|
||||||
|
vi.mocked(getSettingInfo).mockImplementation((setting) => {
|
||||||
|
const parts = setting.category || setting.id.split('.')
|
||||||
|
return {
|
||||||
|
category: parts[0] ?? 'Other',
|
||||||
|
subCategory: parts[1] ?? 'Other'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function findCategory(
|
||||||
|
categories: SettingTreeNode[],
|
||||||
|
label: string
|
||||||
|
): SettingTreeNode | undefined {
|
||||||
|
return categories.find((c) => c.label === label)
|
||||||
|
}
|
||||||
|
|
||||||
|
it('defaults to first category when no params are given', () => {
|
||||||
|
const { defaultCategory, settingCategories } = useSettingUI()
|
||||||
|
expect(defaultCategory.value).toBe(settingCategories.value[0])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('resolves category from scrollToSettingId', () => {
|
||||||
|
const { defaultCategory, settingCategories } = useSettingUI(
|
||||||
|
undefined,
|
||||||
|
'Comfy.Locale'
|
||||||
|
)
|
||||||
|
const comfyCategory = findCategory(settingCategories.value, 'Comfy')
|
||||||
|
expect(comfyCategory).toBeDefined()
|
||||||
|
expect(defaultCategory.value).toBe(comfyCategory)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('resolves different category from scrollToSettingId', () => {
|
||||||
|
const { defaultCategory, settingCategories } = useSettingUI(
|
||||||
|
undefined,
|
||||||
|
'Appearance.Theme'
|
||||||
|
)
|
||||||
|
const appearanceCategory = findCategory(
|
||||||
|
settingCategories.value,
|
||||||
|
'Appearance'
|
||||||
|
)
|
||||||
|
expect(appearanceCategory).toBeDefined()
|
||||||
|
expect(defaultCategory.value).toBe(appearanceCategory)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('falls back to first category for unknown scrollToSettingId', () => {
|
||||||
|
const { defaultCategory, settingCategories } = useSettingUI(
|
||||||
|
undefined,
|
||||||
|
'NonExistent.Setting'
|
||||||
|
)
|
||||||
|
expect(defaultCategory.value).toBe(settingCategories.value[0])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('gives defaultPanel precedence over scrollToSettingId', () => {
|
||||||
|
const { defaultCategory } = useSettingUI('about', 'Comfy.Locale')
|
||||||
|
expect(defaultCategory.value.key).toBe('about')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -7,10 +7,12 @@ import { useBillingContext } from '@/composables/billing/useBillingContext'
|
|||||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||||
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
|
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
|
||||||
import { isCloud, isDesktop } from '@/platform/distribution/types'
|
import { isCloud, isDesktop } from '@/platform/distribution/types'
|
||||||
|
import {
|
||||||
|
getSettingInfo,
|
||||||
|
useSettingStore
|
||||||
|
} from '@/platform/settings/settingStore'
|
||||||
import type { SettingTreeNode } from '@/platform/settings/settingStore'
|
import type { SettingTreeNode } from '@/platform/settings/settingStore'
|
||||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
|
||||||
import type { SettingParams } from '@/platform/settings/types'
|
import type { SettingParams } from '@/platform/settings/types'
|
||||||
|
|
||||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||||
import { buildTree } from '@/utils/treeUtil'
|
import { buildTree } from '@/utils/treeUtil'
|
||||||
|
|
||||||
@@ -30,7 +32,8 @@ export function useSettingUI(
|
|||||||
| 'credits'
|
| 'credits'
|
||||||
| 'subscription'
|
| 'subscription'
|
||||||
| 'workspace'
|
| 'workspace'
|
||||||
| 'secrets'
|
| 'secrets',
|
||||||
|
scrollToSettingId?: string
|
||||||
) {
|
) {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const { isLoggedIn } = useCurrentUser()
|
const { isLoggedIn } = useCurrentUser()
|
||||||
@@ -238,12 +241,23 @@ export function useSettingUI(
|
|||||||
* The default category to show when the dialog is opened.
|
* The default category to show when the dialog is opened.
|
||||||
*/
|
*/
|
||||||
const defaultCategory = computed<SettingTreeNode>(() => {
|
const defaultCategory = computed<SettingTreeNode>(() => {
|
||||||
if (!defaultPanel) return settingCategories.value[0]
|
if (defaultPanel) {
|
||||||
// Search through all groups in groupedMenuTreeNodes
|
for (const group of groupedMenuTreeNodes.value) {
|
||||||
for (const group of groupedMenuTreeNodes.value) {
|
const found = group.children?.find((node) => node.key === defaultPanel)
|
||||||
const found = group.children?.find((node) => node.key === defaultPanel)
|
if (found) return found
|
||||||
if (found) return found
|
}
|
||||||
|
return settingCategories.value[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (scrollToSettingId) {
|
||||||
|
const setting = settingStore.settingsById[scrollToSettingId]
|
||||||
|
if (setting) {
|
||||||
|
const { category } = getSettingInfo(setting)
|
||||||
|
const found = settingCategories.value.find((c) => c.label === category)
|
||||||
|
if (found) return found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return settingCategories.value[0]
|
return settingCategories.value[0]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -138,7 +138,8 @@ export const useDialogService = () => {
|
|||||||
| 'credits'
|
| 'credits'
|
||||||
| 'subscription'
|
| 'subscription'
|
||||||
| 'workspace'
|
| 'workspace'
|
||||||
| 'secrets'
|
| 'secrets',
|
||||||
|
settingId?: string
|
||||||
) {
|
) {
|
||||||
const [
|
const [
|
||||||
{ default: SettingDialogHeader },
|
{ default: SettingDialogHeader },
|
||||||
@@ -148,7 +149,15 @@ export const useDialogService = () => {
|
|||||||
lazySettingDialogContent()
|
lazySettingDialogContent()
|
||||||
])
|
])
|
||||||
|
|
||||||
const props = panel ? { props: { defaultPanel: panel } } : undefined
|
const props =
|
||||||
|
panel || settingId
|
||||||
|
? {
|
||||||
|
props: {
|
||||||
|
defaultPanel: panel,
|
||||||
|
scrollToSettingId: settingId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
|
||||||
dialogStore.showDialog({
|
dialogStore.showDialog({
|
||||||
key: 'global-settings',
|
key: 'global-settings',
|
||||||
|
|||||||
Reference in New Issue
Block a user