[refactor] Migrate SettingDialog to BaseModalLayout design system (#8270)

This commit is contained in:
Jin Yi
2026-02-12 16:27:11 +09:00
committed by GitHub
parent 995ebc4ba4
commit 553ea63357
54 changed files with 659 additions and 1013 deletions

View File

@@ -86,15 +86,15 @@ import { computed } from 'vue'
import Button from '@/components/ui/button/Button.vue'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { useDialogService } from '@/services/dialogService'
import { useSettingsDialog } from '@/platform/settings/composables/useSettingsDialog'
const { flags } = useFeatureFlags()
const dialogService = useDialogService()
const settingsDialog = useSettingsDialog()
const showSecretsHint = computed(() => flags.userSecretsEnabled)
function openSecretsSettings() {
dialogService.showSettingsDialog('secrets')
settingsDialog.show('secrets')
}
const props = defineProps<{

View File

@@ -1,7 +1,7 @@
import AssetBrowserModal from '@/platform/assets/components/AssetBrowserModal.vue'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import { useDialogService } from '@/services/dialogService'
import { useDialogStore } from '@/stores/dialogStore'
import type { DialogComponentProps } from '@/stores/dialogStore'
interface ShowOptions {
/** ComfyUI node type for context (e.g., 'CheckpointLoaderSimple') */
@@ -22,64 +22,51 @@ interface BrowseOptions {
onAssetSelected?: (asset: AssetItem) => void
}
const dialogComponentProps: DialogComponentProps = {
headless: true,
modal: true,
closable: true,
pt: {
root: {
class: 'rounded-2xl overflow-hidden asset-browser-dialog'
},
header: {
class: '!p-0 hidden'
},
content: {
class: '!p-0 !m-0 h-full w-full'
}
}
} as const
const DIALOG_KEY = 'global-asset-browser'
export const useAssetBrowserDialog = () => {
const dialogService = useDialogService()
const dialogStore = useDialogStore()
const dialogKey = 'global-asset-browser'
async function show(props: ShowOptions) {
function hide() {
dialogStore.closeDialog({ key: DIALOG_KEY })
}
function show(props: ShowOptions) {
const handleAssetSelected = (asset: AssetItem) => {
props.onAssetSelected?.(asset)
dialogStore.closeDialog({ key: dialogKey })
hide()
}
dialogStore.showDialog({
key: dialogKey,
dialogService.showLayoutDialog({
key: DIALOG_KEY,
component: AssetBrowserModal,
props: {
nodeType: props.nodeType,
inputName: props.inputName,
currentValue: props.currentValue,
onSelect: handleAssetSelected,
onClose: () => dialogStore.closeDialog({ key: dialogKey })
},
dialogComponentProps
onClose: hide
}
})
}
async function browse(options: BrowseOptions): Promise<void> {
function browse(options: BrowseOptions) {
const handleAssetSelected = (asset: AssetItem) => {
options.onAssetSelected?.(asset)
dialogStore.closeDialog({ key: dialogKey })
hide()
}
dialogStore.showDialog({
key: dialogKey,
dialogService.showLayoutDialog({
key: DIALOG_KEY,
component: AssetBrowserModal,
props: {
showLeftPanel: true,
assetType: options.assetType,
title: options.title,
onSelect: handleAssetSelected,
onClose: () => dialogStore.closeDialog({ key: dialogKey })
},
dialogComponentProps
onClose: hide
}
})
}

View File

@@ -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 { computed, defineAsyncComponent } from 'vue'
import CloudBadge from '@/components/topbar/CloudBadge.vue'

View File

@@ -1,33 +1,29 @@
<template>
<PanelTemplate value="Extension" class="extension-panel">
<template #header>
<SearchBox
v-model="filters['global'].value"
:placeholder="
$t('g.searchPlaceholder', { subject: $t('g.extensions') })
"
/>
<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.searchPlaceholder', { subject: $t('g.extensions') })"
/>
<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"
@@ -81,7 +77,7 @@
</template>
</Column>
</DataTable>
</PanelTemplate>
</div>
</template>
<script setup lang="ts">
@@ -97,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'

View File

@@ -1,46 +1,39 @@
<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"
:aria-label="$t('g.copyToClipboard')"
@click="copyCommandLineArgs"
>
<i class="pi pi-clipboard" />
</Button>
</div>
</Message>
<div
v-for="([label, items], i) in Object.entries(serverConfigsByCategory)"
:key="label"
@@ -58,7 +51,7 @@
/>
</div>
</div>
</PanelTemplate>
</div>
</template>
<script setup lang="ts">
@@ -69,7 +62,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, ServerConfigValue } from '@/constants/serverConfig'

View File

@@ -0,0 +1,200 @@
<template>
<BaseModalLayout content-title="" data-testid="settings-dialog" size="md">
<template #leftPanelHeaderTitle>
<i class="icon-[lucide--settings]" />
<h2 class="text-neutral text-base">{{ $t('g.settings') }}</h2>
</template>
<template #leftPanel>
<div class="px-3">
<SearchBox
v-model:model-value="searchQuery"
size="md"
:placeholder="$t('g.searchSettings') + '...'"
:debounce-time="128"
@search="handleSearch"
/>
</div>
<nav
ref="navRef"
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"
:data-nav-id="item.id"
:icon="item.icon"
:badge="item.badge"
:active="activeCategoryKey === item.id"
@click="onNavItemClick(item.id)"
>
{{ item.label }}
</NavItem>
</div>
</nav>
</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 { computed, nextTick, provide, ref, watch } from 'vue'
import SearchBox from '@/components/common/SearchBox.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 { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
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 { OnCloseKey } from '@/types/widgetTypes'
import { flattenTree } from '@/utils/treeUtil'
const { onClose, defaultPanel, scrollToSettingId } = defineProps<{
onClose: () => void
defaultPanel?: SettingPanelType
scrollToSettingId?: string
}>()
provide(OnCloseKey, onClose)
const {
defaultCategory,
settingCategories,
navGroups,
findCategoryByKey,
findPanelByKey
} = useSettingUI(defaultPanel, scrollToSettingId)
const {
searchQuery,
inSearch,
searchResultsCategories,
handleSearch: handleSearchBase,
getSearchResults
} = useSettingSearch()
const authActions = useFirebaseAuthActions()
const navRef = ref<HTMLElement | null>(null)
const activeCategoryKey = ref<string | null>(defaultCategory.value?.key ?? null)
watch(searchResultsCategories, (categories) => {
if (!inSearch.value || categories.size === 0) return
const firstMatch = navGroups.value
.flatMap((g) => g.items)
.find((item) => {
const node = findCategoryByKey(item.id)
return node && categories.has(node.label)
})
activeCategoryKey.value = firstMatch?.id ?? 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())
if (query) {
activeCategoryKey.value = null
} else if (!activeCategoryKey.value) {
activeCategoryKey.value = defaultCategory.value?.key ?? null
}
}
function onNavItemClick(id: string) {
activeCategoryKey.value = id
}
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()
}
if (newKey) {
void nextTick(() => {
navRef.value
?.querySelector(`[data-nav-id="${newKey}"]`)
?.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
})
}
})
</script>

View File

@@ -1,303 +0,0 @@
<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>

View File

@@ -12,10 +12,30 @@ import {
useSettingStore
} from '@/platform/settings/settingStore'
import type { SettingTreeNode } from '@/platform/settings/settingStore'
import type { SettingParams } from '@/platform/settings/types'
import type { SettingPanelType, SettingParams } from '@/platform/settings/types'
import type { NavGroupData } from '@/types/navTypes'
import { normalizeI18nKey } from '@/utils/formatUtil'
import { buildTree } from '@/utils/treeUtil'
const CATEGORY_ICONS: Record<string, string> = {
'3D': 'icon-[lucide--box]',
about: 'icon-[lucide--info]',
Appearance: 'icon-[lucide--palette]',
Comfy: 'icon-[lucide--settings]',
credits: 'icon-[lucide--coins]',
extension: 'icon-[lucide--puzzle]',
keybinding: 'icon-[lucide--keyboard]',
LiteGraph: 'icon-[lucide--workflow]',
'Mask Editor': 'icon-[lucide--pen-tool]',
Other: 'icon-[lucide--ellipsis]',
PlanCredits: 'icon-[lucide--credit-card]',
secrets: 'icon-[lucide--key-round]',
'server-config': 'icon-[lucide--server]',
subscription: 'icon-[lucide--credit-card]',
user: 'icon-[lucide--user]',
workspace: 'icon-[lucide--building-2]'
}
interface SettingPanelItem {
node: SettingTreeNode
component: Component
@@ -23,16 +43,7 @@ interface SettingPanelItem {
}
export function useSettingUI(
defaultPanel?:
| 'about'
| 'keybinding'
| 'extension'
| 'server-config'
| 'user'
| 'credits'
| 'subscription'
| 'workspace'
| 'secrets',
defaultPanel?: SettingPanelType,
scrollToSettingId?: string
) {
const { t } = useI18n()
@@ -165,7 +176,8 @@ export function useSettingUI(
children: []
},
component: defineAsyncComponent(
() => import('@/components/dialog/content/setting/WorkspacePanel.vue')
() =>
import('@/components/dialog/content/setting/WorkspacePanelContent.vue')
)
}
@@ -357,6 +369,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
})
@@ -366,6 +408,10 @@ export function useSettingUI(
activeCategory,
defaultCategory,
groupedMenuTreeNodes,
settingCategories
settingCategories,
navGroups,
teamWorkspacesEnabled,
findCategoryByKey,
findPanelByKey
}
}

View File

@@ -0,0 +1,34 @@
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, settingId?: string) {
dialogService.showLayoutDialog({
key: DIALOG_KEY,
component: SettingDialog,
props: {
onClose: hide,
...(panel ? { defaultPanel: panel } : {}),
...(settingId ? { scrollToSettingId: settingId } : {})
}
})
}
function showAbout() {
show('about')
}
return { show, hide, showAbout }
}

View File

@@ -64,3 +64,14 @@ export interface ISettingGroup {
label: string
settings: SettingParams[]
}
export type SettingPanelType =
| 'about'
| 'credits'
| 'extension'
| 'keybinding'
| 'secrets'
| 'server-config'
| 'subscription'
| 'user'
| 'workspace'

View File

@@ -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'
@@ -137,13 +137,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'
)
@@ -161,10 +154,6 @@ function useWorkspaceUIInternal() {
)
return {
// Tab management
activeTab: computed(() => activeTab.value),
setActiveTab,
// Permissions and config
permissions,
uiConfig,