mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-23 00:04:06 +00:00
refactor: setting dialog
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<TabPanel value="PlanCredits" class="subscription-container h-full">
|
||||
<div class="subscription-container h-full">
|
||||
<div class="flex h-full flex-col gap-6">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-2xl font-inter font-semibold leading-tight">
|
||||
@@ -63,11 +63,10 @@
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TabPanel from 'primevue/tabpanel'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import CloudBadge from '@/components/topbar/CloudBadge.vue'
|
||||
|
||||
@@ -1,31 +1,29 @@
|
||||
<template>
|
||||
<PanelTemplate value="Extension" class="extension-panel">
|
||||
<template #header>
|
||||
<SearchBox
|
||||
v-model="filters['global'].value"
|
||||
:placeholder="$t('g.searchExtensions') + '...'"
|
||||
/>
|
||||
<Message
|
||||
v-if="hasChanges"
|
||||
severity="info"
|
||||
pt:text="w-full"
|
||||
class="max-h-96 overflow-y-auto"
|
||||
>
|
||||
<ul>
|
||||
<li v-for="ext in changedExtensions" :key="ext.name">
|
||||
<span>
|
||||
{{ extensionStore.isExtensionEnabled(ext.name) ? '[-]' : '[+]' }}
|
||||
</span>
|
||||
{{ ext.name }}
|
||||
</li>
|
||||
</ul>
|
||||
<div class="flex justify-end">
|
||||
<Button variant="destructive" @click="applyChanges">
|
||||
{{ $t('g.reloadToApplyChanges') }}
|
||||
</Button>
|
||||
</div>
|
||||
</Message>
|
||||
</template>
|
||||
<div class="extension-panel flex flex-col gap-2">
|
||||
<SearchBox
|
||||
v-model="filters['global'].value"
|
||||
:placeholder="$t('g.searchExtensions') + '...'"
|
||||
/>
|
||||
<Message
|
||||
v-if="hasChanges"
|
||||
severity="info"
|
||||
pt:text="w-full"
|
||||
class="max-h-96 overflow-y-auto"
|
||||
>
|
||||
<ul>
|
||||
<li v-for="ext in changedExtensions" :key="ext.name">
|
||||
<span>
|
||||
{{ extensionStore.isExtensionEnabled(ext.name) ? '[-]' : '[+]' }}
|
||||
</span>
|
||||
{{ ext.name }}
|
||||
</li>
|
||||
</ul>
|
||||
<div class="flex justify-end">
|
||||
<Button variant="destructive" @click="applyChanges">
|
||||
{{ $t('g.reloadToApplyChanges') }}
|
||||
</Button>
|
||||
</div>
|
||||
</Message>
|
||||
<div class="mb-3 flex gap-2">
|
||||
<SelectButton
|
||||
v-model="filterType"
|
||||
@@ -79,7 +77,7 @@
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</PanelTemplate>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -95,7 +93,6 @@ import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import PanelTemplate from '@/components/dialog/content/setting/PanelTemplate.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useExtensionStore } from '@/stores/extensionStore'
|
||||
|
||||
@@ -1,46 +1,38 @@
|
||||
<template>
|
||||
<PanelTemplate value="Server-Config" class="server-config-panel">
|
||||
<template #header>
|
||||
<div class="flex flex-col gap-2">
|
||||
<Message
|
||||
v-if="modifiedConfigs.length > 0"
|
||||
severity="info"
|
||||
pt:text="w-full"
|
||||
>
|
||||
<p>
|
||||
{{ $t('serverConfig.modifiedConfigs') }}
|
||||
</p>
|
||||
<ul>
|
||||
<li v-for="config in modifiedConfigs" :key="config.id">
|
||||
{{ config.name }}: {{ config.initialValue }} → {{ config.value }}
|
||||
</li>
|
||||
</ul>
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button variant="secondary" @click="revertChanges">
|
||||
{{ $t('serverConfig.revertChanges') }}
|
||||
</Button>
|
||||
<Button variant="destructive" @click="restartApp">
|
||||
{{ $t('serverConfig.restart') }}
|
||||
</Button>
|
||||
</div>
|
||||
</Message>
|
||||
<Message v-if="commandLineArgs" severity="secondary" pt:text="w-full">
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--terminal] text-xl font-bold" />
|
||||
</template>
|
||||
<div class="flex items-center justify-between">
|
||||
<p>{{ commandLineArgs }}</p>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="muted-textonly"
|
||||
@click="copyCommandLineArgs"
|
||||
>
|
||||
<i class="pi pi-clipboard" />
|
||||
</Button>
|
||||
</div>
|
||||
</Message>
|
||||
<div class="server-config-panel flex flex-col gap-2">
|
||||
<Message v-if="modifiedConfigs.length > 0" severity="info" pt:text="w-full">
|
||||
<p>
|
||||
{{ $t('serverConfig.modifiedConfigs') }}
|
||||
</p>
|
||||
<ul>
|
||||
<li v-for="config in modifiedConfigs" :key="config.id">
|
||||
{{ config.name }}: {{ config.initialValue }} → {{ config.value }}
|
||||
</li>
|
||||
</ul>
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button variant="secondary" @click="revertChanges">
|
||||
{{ $t('serverConfig.revertChanges') }}
|
||||
</Button>
|
||||
<Button variant="destructive" @click="restartApp">
|
||||
{{ $t('serverConfig.restart') }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</Message>
|
||||
<Message v-if="commandLineArgs" severity="secondary" pt:text="w-full">
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--terminal] text-xl font-bold" />
|
||||
</template>
|
||||
<div class="flex items-center justify-between">
|
||||
<p>{{ commandLineArgs }}</p>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="muted-textonly"
|
||||
@click="copyCommandLineArgs"
|
||||
>
|
||||
<i class="pi pi-clipboard" />
|
||||
</Button>
|
||||
</div>
|
||||
</Message>
|
||||
<div
|
||||
v-for="([label, items], i) in Object.entries(serverConfigsByCategory)"
|
||||
:key="label"
|
||||
@@ -58,7 +50,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</PanelTemplate>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -69,7 +61,6 @@ import { onBeforeUnmount, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import FormItem from '@/components/common/FormItem.vue'
|
||||
import PanelTemplate from '@/components/dialog/content/setting/PanelTemplate.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
|
||||
import type { ServerConfig } from '@/constants/serverConfig'
|
||||
|
||||
195
src/platform/settings/components/SettingDialog.vue
Normal file
195
src/platform/settings/components/SettingDialog.vue
Normal file
@@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<BaseModalLayout content-title="" data-testid="settings-dialog">
|
||||
<template #leftPanel>
|
||||
<div class="flex h-full w-full flex-col bg-modal-panel-background">
|
||||
<PanelHeader>
|
||||
<template #icon>
|
||||
<WorkspaceProfilePic
|
||||
v-if="teamWorkspacesEnabled"
|
||||
class="size-6 text-xs"
|
||||
:workspace-name="workspaceName"
|
||||
/>
|
||||
<i v-else class="pi pi-cog" />
|
||||
</template>
|
||||
<span class="text-neutral text-base">
|
||||
{{ teamWorkspacesEnabled ? workspaceName : $t('g.settings') }}
|
||||
</span>
|
||||
<Tag
|
||||
v-if="isStaging"
|
||||
value="staging"
|
||||
severity="warn"
|
||||
class="ml-2 text-xs"
|
||||
/>
|
||||
</PanelHeader>
|
||||
|
||||
<div class="px-3">
|
||||
<SearchBox
|
||||
v-model:model-value="searchQuery"
|
||||
size="md"
|
||||
:placeholder="$t('g.searchSettings') + '...'"
|
||||
:debounce-time="128"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<nav
|
||||
class="scrollbar-hide flex flex-1 flex-col gap-1 overflow-y-auto px-3 py-4"
|
||||
>
|
||||
<div
|
||||
v-for="(group, index) in navGroups"
|
||||
:key="index"
|
||||
class="flex flex-col gap-2"
|
||||
>
|
||||
<NavTitle :title="group.title" />
|
||||
<NavItem
|
||||
v-for="item in group.items"
|
||||
:key="item.id"
|
||||
:icon="item.icon"
|
||||
:badge="item.badge"
|
||||
:active="activeCategoryKey === item.id"
|
||||
@click="activeCategoryKey = item.id"
|
||||
>
|
||||
{{ item.label }}
|
||||
</NavItem>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #header />
|
||||
|
||||
<template #content>
|
||||
<template v-if="inSearch">
|
||||
<SettingsPanel :setting-groups="searchResults" />
|
||||
</template>
|
||||
<template v-else-if="activeSettingCategory">
|
||||
<CurrentUserMessage v-if="activeSettingCategory.label === 'Comfy'" />
|
||||
<ColorPaletteMessage
|
||||
v-if="activeSettingCategory.label === 'Appearance'"
|
||||
/>
|
||||
<SettingsPanel :setting-groups="sortedGroups(activeSettingCategory)" />
|
||||
</template>
|
||||
<template v-else-if="activePanel">
|
||||
<Suspense>
|
||||
<component :is="activePanel.component" v-bind="activePanel.props" />
|
||||
<template #fallback>
|
||||
<div>
|
||||
{{ $t('g.loadingPanel', { panel: activePanel.node.label }) }}
|
||||
</div>
|
||||
</template>
|
||||
</Suspense>
|
||||
</template>
|
||||
</template>
|
||||
</BaseModalLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Tag from 'primevue/tag'
|
||||
import { computed, provide, ref, watch } from 'vue'
|
||||
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import WorkspaceProfilePic from '@/components/common/WorkspaceProfilePic.vue'
|
||||
import CurrentUserMessage from '@/components/dialog/content/setting/CurrentUserMessage.vue'
|
||||
import BaseModalLayout from '@/components/widget/layout/BaseModalLayout.vue'
|
||||
import NavItem from '@/components/widget/nav/NavItem.vue'
|
||||
import NavTitle from '@/components/widget/nav/NavTitle.vue'
|
||||
import PanelHeader from '@/components/widget/panel/PanelHeader.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { isStaging } from '@/config/staging'
|
||||
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,
|
||||
SettingPanelType,
|
||||
SettingParams
|
||||
} from '@/platform/settings/types'
|
||||
import { useTeamWorkspaceStore } from '@/platform/workspace/stores/teamWorkspaceStore'
|
||||
import { OnCloseKey } from '@/types/widgetTypes'
|
||||
import { flattenTree } from '@/utils/treeUtil'
|
||||
|
||||
const { onClose, defaultPanel } = defineProps<{
|
||||
onClose: () => void
|
||||
defaultPanel?: SettingPanelType
|
||||
}>()
|
||||
|
||||
provide(OnCloseKey, onClose)
|
||||
|
||||
const {
|
||||
defaultCategory,
|
||||
settingCategories,
|
||||
navGroups,
|
||||
teamWorkspacesEnabled,
|
||||
findCategoryByKey,
|
||||
findPanelByKey
|
||||
} = useSettingUI(defaultPanel)
|
||||
|
||||
const workspaceStore = useTeamWorkspaceStore()
|
||||
const workspaceName = computed(() => workspaceStore.workspaceName)
|
||||
|
||||
const {
|
||||
searchQuery,
|
||||
inSearch,
|
||||
handleSearch: handleSearchBase,
|
||||
getSearchResults
|
||||
} = useSettingSearch()
|
||||
|
||||
const authActions = useFirebaseAuthActions()
|
||||
|
||||
const activeCategoryKey = ref<string | null>(defaultCategory.value?.key ?? null)
|
||||
|
||||
const activeSettingCategory = computed<SettingTreeNode | null>(() => {
|
||||
if (!activeCategoryKey.value) return null
|
||||
return (
|
||||
settingCategories.value.find((c) => c.key === activeCategoryKey.value) ??
|
||||
null
|
||||
)
|
||||
})
|
||||
|
||||
const activePanel = computed(() => {
|
||||
if (!activeCategoryKey.value) return null
|
||||
return findPanelByKey(activeCategoryKey.value)
|
||||
})
|
||||
|
||||
const getGroupSortOrder = (group: SettingTreeNode): number =>
|
||||
Math.max(0, ...flattenTree<SettingParams>(group).map((s) => s.sortOrder ?? 0))
|
||||
|
||||
function 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
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
function handleSearch(query: string) {
|
||||
handleSearchBase(query.trim())
|
||||
activeCategoryKey.value = query ? null : (defaultCategory.value?.key ?? null)
|
||||
}
|
||||
|
||||
const searchResults = computed<ISettingGroup[]>(() => {
|
||||
const category = activeCategoryKey.value
|
||||
? findCategoryByKey(activeCategoryKey.value)
|
||||
: null
|
||||
return getSearchResults(category)
|
||||
})
|
||||
|
||||
watch(activeCategoryKey, (newKey, oldKey) => {
|
||||
if (!newKey && !inSearch.value) {
|
||||
activeCategoryKey.value = oldKey
|
||||
}
|
||||
if (newKey === 'credits') {
|
||||
void authActions.fetchBalance()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,244 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
:class="
|
||||
teamWorkspacesEnabled
|
||||
? 'flex h-[80vh] w-full overflow-hidden'
|
||||
: 'settings-container'
|
||||
"
|
||||
>
|
||||
<ScrollPanel
|
||||
:class="
|
||||
teamWorkspacesEnabled
|
||||
? 'w-48 shrink-0 p-2 2xl:w-64'
|
||||
: 'settings-sidebar w-48 shrink-0 p-2 2xl:w-64'
|
||||
"
|
||||
>
|
||||
<SearchBox
|
||||
v-model:model-value="searchQuery"
|
||||
class="settings-search-box mb-2 w-full"
|
||||
:placeholder="$t('g.searchSettings') + '...'"
|
||||
:debounce-time="128"
|
||||
autofocus
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<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>
|
||||
<!-- Workspace mode: custom workspace item -->
|
||||
<template v-if="teamWorkspacesEnabled" #option="{ option }">
|
||||
<WorkspaceSidebarItem v-if="option.key === 'workspace'" />
|
||||
<span v-else>{{ option.translatedLabel }}</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-x-auto'
|
||||
: '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, 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 } = defineProps<{
|
||||
defaultPanel?:
|
||||
| 'about'
|
||||
| 'keybinding'
|
||||
| 'extension'
|
||||
| 'server-config'
|
||||
| 'user'
|
||||
| 'credits'
|
||||
| 'subscription'
|
||||
| 'workspace'
|
||||
}>()
|
||||
|
||||
const { flags } = useFeatureFlags()
|
||||
const teamWorkspacesEnabled = computed(
|
||||
() => isCloud && flags.teamWorkspacesEnabled
|
||||
)
|
||||
|
||||
const {
|
||||
activeCategory,
|
||||
defaultCategory,
|
||||
settingCategories,
|
||||
groupedMenuTreeNodes,
|
||||
panels
|
||||
} = useSettingUI(defaultPanel)
|
||||
|
||||
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 ?? '')
|
||||
)
|
||||
|
||||
// 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;
|
||||
}
|
||||
</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>
|
||||
@@ -8,29 +8,37 @@ import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import type { SettingTreeNode } from '@/platform/settings/settingStore'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import type { SettingParams } from '@/platform/settings/types'
|
||||
import type { SettingPanelType, SettingParams } from '@/platform/settings/types'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
import type { NavGroupData } from '@/types/navTypes'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
import { buildTree } from '@/utils/treeUtil'
|
||||
|
||||
const CATEGORY_ICONS: Record<string, string> = {
|
||||
Comfy: 'icon-[lucide--settings]',
|
||||
LiteGraph: 'icon-[lucide--workflow]',
|
||||
Appearance: 'icon-[lucide--palette]',
|
||||
'3D': 'icon-[lucide--box]',
|
||||
'Mask Editor': 'icon-[lucide--pen-tool]',
|
||||
Other: 'icon-[lucide--ellipsis]',
|
||||
about: 'icon-[lucide--info]',
|
||||
credits: 'icon-[lucide--coins]',
|
||||
user: 'icon-[lucide--user]',
|
||||
workspace: 'icon-[lucide--building-2]',
|
||||
keybinding: 'icon-[lucide--keyboard]',
|
||||
extension: 'icon-[lucide--puzzle]',
|
||||
'server-config': 'icon-[lucide--server]',
|
||||
PlanCredits: 'icon-[lucide--credit-card]'
|
||||
}
|
||||
|
||||
interface SettingPanelItem {
|
||||
node: SettingTreeNode
|
||||
component: Component
|
||||
props?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export function useSettingUI(
|
||||
defaultPanel?:
|
||||
| 'about'
|
||||
| 'keybinding'
|
||||
| 'extension'
|
||||
| 'server-config'
|
||||
| 'user'
|
||||
| 'credits'
|
||||
| 'subscription'
|
||||
| 'workspace'
|
||||
) {
|
||||
export function useSettingUI(defaultPanel?: SettingPanelType) {
|
||||
const { t } = useI18n()
|
||||
const { isLoggedIn } = useCurrentUser()
|
||||
const settingStore = useSettingStore()
|
||||
@@ -321,6 +329,36 @@ export function useSettingUI(
|
||||
: legacyMenuTreeNodes.value
|
||||
)
|
||||
|
||||
const navGroups = computed<NavGroupData[]>(() =>
|
||||
groupedMenuTreeNodes.value.map((group) => ({
|
||||
title:
|
||||
(group as SettingTreeNode & { translatedLabel?: string })
|
||||
.translatedLabel ?? group.label,
|
||||
items: (group.children ?? []).map((child) => ({
|
||||
id: child.key,
|
||||
label:
|
||||
(child as SettingTreeNode & { translatedLabel?: string })
|
||||
.translatedLabel ?? child.label,
|
||||
icon:
|
||||
CATEGORY_ICONS[child.key] ??
|
||||
CATEGORY_ICONS[child.label] ??
|
||||
'icon-[lucide--plug]'
|
||||
}))
|
||||
}))
|
||||
)
|
||||
|
||||
function findCategoryByKey(key: string): SettingTreeNode | null {
|
||||
for (const group of groupedMenuTreeNodes.value) {
|
||||
const found = group.children?.find((node) => node.key === key)
|
||||
if (found) return found
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function findPanelByKey(key: string): SettingPanelItem | null {
|
||||
return panels.value.find((p) => p.node.key === key) ?? null
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
activeCategory.value = defaultCategory.value
|
||||
})
|
||||
@@ -330,6 +368,10 @@ export function useSettingUI(
|
||||
activeCategory,
|
||||
defaultCategory,
|
||||
groupedMenuTreeNodes,
|
||||
settingCategories
|
||||
settingCategories,
|
||||
navGroups,
|
||||
teamWorkspacesEnabled,
|
||||
findCategoryByKey,
|
||||
findPanelByKey
|
||||
}
|
||||
}
|
||||
|
||||
33
src/platform/settings/composables/useSettingsDialog.ts
Normal file
33
src/platform/settings/composables/useSettingsDialog.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
import SettingDialog from '@/platform/settings/components/SettingDialog.vue'
|
||||
import type { SettingPanelType } from '@/platform/settings/types'
|
||||
|
||||
const DIALOG_KEY = 'global-settings'
|
||||
|
||||
export function useSettingsDialog() {
|
||||
const dialogService = useDialogService()
|
||||
const dialogStore = useDialogStore()
|
||||
|
||||
function hide() {
|
||||
dialogStore.closeDialog({ key: DIALOG_KEY })
|
||||
}
|
||||
|
||||
function show(panel?: SettingPanelType) {
|
||||
dialogService.showLayoutDialog({
|
||||
key: DIALOG_KEY,
|
||||
component: SettingDialog,
|
||||
props: {
|
||||
onClose: hide,
|
||||
...(panel ? { defaultPanel: panel } : {})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function showAbout() {
|
||||
show('about')
|
||||
}
|
||||
|
||||
return { show, hide, showAbout }
|
||||
}
|
||||
@@ -64,3 +64,13 @@ export interface ISettingGroup {
|
||||
label: string
|
||||
settings: SettingParams[]
|
||||
}
|
||||
|
||||
export type SettingPanelType =
|
||||
| 'about'
|
||||
| 'keybinding'
|
||||
| 'extension'
|
||||
| 'server-config'
|
||||
| 'user'
|
||||
| 'credits'
|
||||
| 'subscription'
|
||||
| 'workspace'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { createSharedComposable } from '@vueuse/core'
|
||||
|
||||
import type { WorkspaceRole, WorkspaceType } from '../api/workspaceApi'
|
||||
@@ -81,13 +81,6 @@ function getUIConfig(
|
||||
function useWorkspaceUIInternal() {
|
||||
const store = useTeamWorkspaceStore()
|
||||
|
||||
// Tab management (shared UI state)
|
||||
const activeTab = ref<string>('plan')
|
||||
|
||||
function setActiveTab(tab: string | number) {
|
||||
activeTab.value = String(tab)
|
||||
}
|
||||
|
||||
const workspaceType = computed<WorkspaceType>(
|
||||
() => store.activeWorkspace?.type ?? 'personal'
|
||||
)
|
||||
@@ -105,10 +98,6 @@ function useWorkspaceUIInternal() {
|
||||
)
|
||||
|
||||
return {
|
||||
// Tab management
|
||||
activeTab: computed(() => activeTab.value),
|
||||
setActiveTab,
|
||||
|
||||
// Permissions and config
|
||||
permissions,
|
||||
uiConfig,
|
||||
|
||||
Reference in New Issue
Block a user