mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
refactor: setting dialog
This commit is contained in:
@@ -9,12 +9,12 @@ export class SettingDialog {
|
||||
) {}
|
||||
|
||||
get root() {
|
||||
return this.page.locator('div.settings-container')
|
||||
return this.page.locator('[data-testid="settings-dialog"]')
|
||||
}
|
||||
|
||||
async open() {
|
||||
await this.comfyPage.executeCommand('Comfy.ShowSettingsDialog')
|
||||
await this.page.waitForSelector('div.settings-container')
|
||||
await this.page.waitForSelector('[data-testid="settings-dialog"]')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -23,9 +23,7 @@ export class SettingDialog {
|
||||
* @param value - The value to set
|
||||
*/
|
||||
async setStringSetting(id: string, value: string) {
|
||||
const settingInputDiv = this.page.locator(
|
||||
`div.settings-container div[id="${id}"]`
|
||||
)
|
||||
const settingInputDiv = this.root.locator(`div[id="${id}"]`)
|
||||
await settingInputDiv.locator('input').fill(value)
|
||||
}
|
||||
|
||||
@@ -34,15 +32,15 @@ export class SettingDialog {
|
||||
* @param id - The id of the setting
|
||||
*/
|
||||
async toggleBooleanSetting(id: string) {
|
||||
const settingInputDiv = this.page.locator(
|
||||
`div.settings-container div[id="${id}"]`
|
||||
)
|
||||
const settingInputDiv = this.root.locator(`div[id="${id}"]`)
|
||||
await settingInputDiv.locator('input').click()
|
||||
}
|
||||
|
||||
async goToAboutPanel() {
|
||||
const aboutButton = this.page.locator('li[aria-label="About"]')
|
||||
const aboutButton = this.root.locator('nav [role="button"]', {
|
||||
hasText: 'About'
|
||||
})
|
||||
await aboutButton.click()
|
||||
await this.page.waitForSelector('div.about-container')
|
||||
await this.page.waitForSelector('.about-container')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,9 +232,13 @@ test.describe('Missing models warning', () => {
|
||||
test.describe('Settings', () => {
|
||||
test('@mobile Should be visible on mobile', async ({ comfyPage }) => {
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsContent = comfyPage.page.locator('.settings-content')
|
||||
await expect(settingsContent).toBeVisible()
|
||||
const isUsableHeight = await settingsContent.evaluate(
|
||||
const settingsDialog = comfyPage.page.locator(
|
||||
'[data-testid="settings-dialog"]'
|
||||
)
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
const contentArea = settingsDialog.locator('main')
|
||||
await expect(contentArea).toBeVisible()
|
||||
const isUsableHeight = await contentArea.evaluate(
|
||||
(el) => el.clientHeight > 30
|
||||
)
|
||||
expect(isUsableHeight).toBeTruthy()
|
||||
@@ -244,7 +248,9 @@ test.describe('Settings', () => {
|
||||
await comfyPage.page.keyboard.down('ControlOrMeta')
|
||||
await comfyPage.page.keyboard.press(',')
|
||||
await comfyPage.page.keyboard.up('ControlOrMeta')
|
||||
const settingsLocator = comfyPage.page.locator('.settings-container')
|
||||
const settingsLocator = comfyPage.page.locator(
|
||||
'[data-testid="settings-dialog"]'
|
||||
)
|
||||
await expect(settingsLocator).toBeVisible()
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await expect(settingsLocator).not.toBeVisible()
|
||||
@@ -261,10 +267,15 @@ test.describe('Settings', () => {
|
||||
test('Should persist keybinding setting', async ({ comfyPage }) => {
|
||||
// Open the settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
await comfyPage.page.waitForSelector('.settings-container')
|
||||
await comfyPage.page.waitForSelector('[data-testid="settings-dialog"]')
|
||||
|
||||
// Open the keybinding tab
|
||||
await comfyPage.page.getByLabel('Keybinding').click()
|
||||
const settingsDialog = comfyPage.page.locator(
|
||||
'[data-testid="settings-dialog"]'
|
||||
)
|
||||
await settingsDialog
|
||||
.locator('nav [role="button"]', { hasText: 'Keybinding' })
|
||||
.click()
|
||||
await comfyPage.page.waitForSelector(
|
||||
'[placeholder="Search Keybindings..."]'
|
||||
)
|
||||
|
||||
@@ -794,7 +794,7 @@ test.describe('Subgraph Operations', () => {
|
||||
|
||||
// Open settings dialog using hotkey
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
await comfyPage.page.waitForSelector('.settings-container', {
|
||||
await comfyPage.page.waitForSelector('[data-testid="settings-dialog"]', {
|
||||
state: 'visible'
|
||||
})
|
||||
|
||||
@@ -804,7 +804,7 @@ test.describe('Subgraph Operations', () => {
|
||||
|
||||
// Dialog should be closed
|
||||
await expect(
|
||||
comfyPage.page.locator('.settings-container')
|
||||
comfyPage.page.locator('[data-testid="settings-dialog"]')
|
||||
).not.toBeVisible()
|
||||
|
||||
// Should still be in subgraph
|
||||
|
||||
@@ -41,16 +41,15 @@ test.describe('Settings Search functionality', () => {
|
||||
})
|
||||
|
||||
test('can open settings dialog and use search box', async ({ comfyPage }) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
const settingsDialog = comfyPage.page.locator(
|
||||
'[data-testid="settings-dialog"]'
|
||||
)
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
|
||||
// Find the search box
|
||||
const searchBox = comfyPage.page.locator('.settings-search-box input')
|
||||
const searchBox = settingsDialog.locator('input[placeholder*="Search"]')
|
||||
await expect(searchBox).toBeVisible()
|
||||
|
||||
// Verify search box has the correct placeholder
|
||||
await expect(searchBox).toHaveAttribute(
|
||||
'placeholder',
|
||||
expect.stringContaining('Search')
|
||||
@@ -58,226 +57,198 @@ test.describe('Settings Search functionality', () => {
|
||||
})
|
||||
|
||||
test('search box is functional and accepts input', async ({ comfyPage }) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
const settingsDialog = comfyPage.page.locator(
|
||||
'[data-testid="settings-dialog"]'
|
||||
)
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
|
||||
// Find and interact with the search box
|
||||
const searchBox = comfyPage.page.locator('.settings-search-box input')
|
||||
const searchBox = settingsDialog.locator('input[placeholder*="Search"]')
|
||||
await searchBox.fill('Comfy')
|
||||
|
||||
// Verify the input was accepted
|
||||
await expect(searchBox).toHaveValue('Comfy')
|
||||
})
|
||||
|
||||
test('search box clears properly', async ({ comfyPage }) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
const settingsDialog = comfyPage.page.locator(
|
||||
'[data-testid="settings-dialog"]'
|
||||
)
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
|
||||
// Find and interact with the search box
|
||||
const searchBox = comfyPage.page.locator('.settings-search-box input')
|
||||
const searchBox = settingsDialog.locator('input[placeholder*="Search"]')
|
||||
await searchBox.fill('test')
|
||||
await expect(searchBox).toHaveValue('test')
|
||||
|
||||
// Clear the search box
|
||||
await searchBox.clear()
|
||||
await expect(searchBox).toHaveValue('')
|
||||
})
|
||||
|
||||
test('settings categories are visible in sidebar', async ({ comfyPage }) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
const settingsDialog = comfyPage.page.locator(
|
||||
'[data-testid="settings-dialog"]'
|
||||
)
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
|
||||
// Check that the sidebar has categories
|
||||
const categories = comfyPage.page.locator(
|
||||
'.settings-sidebar .p-listbox-option'
|
||||
)
|
||||
const categories = settingsDialog.locator('nav [role="button"]')
|
||||
expect(await categories.count()).toBeGreaterThan(0)
|
||||
|
||||
// Check that at least one category is visible
|
||||
await expect(categories.first()).toBeVisible()
|
||||
})
|
||||
|
||||
test('can select different categories in sidebar', async ({ comfyPage }) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
const settingsDialog = comfyPage.page.locator(
|
||||
'[data-testid="settings-dialog"]'
|
||||
)
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
|
||||
// Get categories and click on different ones
|
||||
const categories = comfyPage.page.locator(
|
||||
'.settings-sidebar .p-listbox-option'
|
||||
)
|
||||
const categories = settingsDialog.locator('nav [role="button"]')
|
||||
const categoryCount = await categories.count()
|
||||
|
||||
if (categoryCount > 1) {
|
||||
// Click on the second category
|
||||
await categories.nth(1).click()
|
||||
|
||||
// Verify the category is selected
|
||||
await expect(categories.nth(1)).toHaveClass(/p-listbox-option-selected/)
|
||||
await expect(categories.nth(1)).toHaveClass(
|
||||
/bg-interface-menu-component-surface-selected/
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
test('settings content area is visible', async ({ comfyPage }) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
const settingsDialog = comfyPage.page.locator(
|
||||
'[data-testid="settings-dialog"]'
|
||||
)
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
|
||||
// Check that the content area is visible
|
||||
const contentArea = comfyPage.page.locator('.settings-content')
|
||||
const contentArea = settingsDialog.locator('main')
|
||||
await expect(contentArea).toBeVisible()
|
||||
|
||||
// Check that tab panels are visible
|
||||
const tabPanels = comfyPage.page.locator('.settings-tab-panels')
|
||||
await expect(tabPanels).toBeVisible()
|
||||
})
|
||||
|
||||
test('search functionality affects UI state', async ({ comfyPage }) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
const settingsDialog = comfyPage.page.locator(
|
||||
'[data-testid="settings-dialog"]'
|
||||
)
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
|
||||
// Find the search box
|
||||
const searchBox = comfyPage.page.locator('.settings-search-box input')
|
||||
|
||||
// Type in search box
|
||||
const searchBox = settingsDialog.locator('input[placeholder*="Search"]')
|
||||
await searchBox.fill('graph')
|
||||
|
||||
// Verify that the search input is handled
|
||||
await expect(searchBox).toHaveValue('graph')
|
||||
})
|
||||
|
||||
test('settings dialog can be closed', async ({ comfyPage }) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
const settingsDialog = comfyPage.page.locator(
|
||||
'[data-testid="settings-dialog"]'
|
||||
)
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
|
||||
// Close with escape key
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
|
||||
// Verify dialog is closed
|
||||
await expect(settingsDialog).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('search box has proper debouncing behavior', async ({ comfyPage }) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
const settingsDialog = comfyPage.page.locator(
|
||||
'[data-testid="settings-dialog"]'
|
||||
)
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
|
||||
// Type rapidly in search box
|
||||
const searchBox = comfyPage.page.locator('.settings-search-box input')
|
||||
const searchBox = settingsDialog.locator('input[placeholder*="Search"]')
|
||||
await searchBox.fill('a')
|
||||
await searchBox.fill('ab')
|
||||
await searchBox.fill('abc')
|
||||
await searchBox.fill('abcd')
|
||||
|
||||
// Verify final value
|
||||
await expect(searchBox).toHaveValue('abcd')
|
||||
})
|
||||
|
||||
test('search excludes hidden settings from results', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
const settingsDialog = comfyPage.page.locator(
|
||||
'[data-testid="settings-dialog"]'
|
||||
)
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
|
||||
// Search for our test settings
|
||||
const searchBox = comfyPage.page.locator('.settings-search-box input')
|
||||
const searchBox = settingsDialog.locator('input[placeholder*="Search"]')
|
||||
await searchBox.fill('Test')
|
||||
|
||||
// Get all settings content
|
||||
const settingsContent = comfyPage.page.locator('.settings-tab-panels')
|
||||
const contentArea = settingsDialog.locator('main')
|
||||
|
||||
// Should show visible setting but not hidden setting
|
||||
await expect(settingsContent).toContainText('Test Visible Setting')
|
||||
await expect(settingsContent).not.toContainText('Test Hidden Setting')
|
||||
await expect(contentArea).toContainText('Test Visible Setting')
|
||||
await expect(contentArea).not.toContainText('Test Hidden Setting')
|
||||
})
|
||||
|
||||
test('search excludes deprecated settings from results', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
const settingsDialog = comfyPage.page.locator(
|
||||
'[data-testid="settings-dialog"]'
|
||||
)
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
|
||||
// Search for our test settings
|
||||
const searchBox = comfyPage.page.locator('.settings-search-box input')
|
||||
const searchBox = settingsDialog.locator('input[placeholder*="Search"]')
|
||||
await searchBox.fill('Test')
|
||||
|
||||
// Get all settings content
|
||||
const settingsContent = comfyPage.page.locator('.settings-tab-panels')
|
||||
const contentArea = settingsDialog.locator('main')
|
||||
|
||||
// Should show visible setting but not deprecated setting
|
||||
await expect(settingsContent).toContainText('Test Visible Setting')
|
||||
await expect(settingsContent).not.toContainText('Test Deprecated Setting')
|
||||
await expect(contentArea).toContainText('Test Visible Setting')
|
||||
await expect(contentArea).not.toContainText('Test Deprecated Setting')
|
||||
})
|
||||
|
||||
test('search shows visible settings but excludes hidden and deprecated', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
const settingsDialog = comfyPage.page.locator(
|
||||
'[data-testid="settings-dialog"]'
|
||||
)
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
|
||||
// Search for our test settings
|
||||
const searchBox = comfyPage.page.locator('.settings-search-box input')
|
||||
const searchBox = settingsDialog.locator('input[placeholder*="Search"]')
|
||||
await searchBox.fill('Test')
|
||||
|
||||
// Get all settings content
|
||||
const settingsContent = comfyPage.page.locator('.settings-tab-panels')
|
||||
const contentArea = settingsDialog.locator('main')
|
||||
|
||||
// Should only show the visible setting
|
||||
await expect(settingsContent).toContainText('Test Visible Setting')
|
||||
await expect(contentArea).toContainText('Test Visible Setting')
|
||||
|
||||
// Should not show hidden or deprecated settings
|
||||
await expect(settingsContent).not.toContainText('Test Hidden Setting')
|
||||
await expect(settingsContent).not.toContainText('Test Deprecated Setting')
|
||||
await expect(contentArea).not.toContainText('Test Hidden Setting')
|
||||
await expect(contentArea).not.toContainText('Test Deprecated Setting')
|
||||
})
|
||||
|
||||
test('search by setting name excludes hidden and deprecated', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
const settingsDialog = comfyPage.page.locator('.settings-container')
|
||||
const settingsDialog = comfyPage.page.locator(
|
||||
'[data-testid="settings-dialog"]'
|
||||
)
|
||||
await expect(settingsDialog).toBeVisible()
|
||||
|
||||
const searchBox = comfyPage.page.locator('.settings-search-box input')
|
||||
const settingsContent = comfyPage.page.locator('.settings-tab-panels')
|
||||
const searchBox = settingsDialog.locator('input[placeholder*="Search"]')
|
||||
const contentArea = settingsDialog.locator('main')
|
||||
|
||||
// Search specifically for hidden setting by name
|
||||
await searchBox.clear()
|
||||
await searchBox.fill('Hidden')
|
||||
|
||||
// Should not show the hidden setting even when searching by name
|
||||
await expect(settingsContent).not.toContainText('Test Hidden Setting')
|
||||
await expect(contentArea).not.toContainText('Test Hidden Setting')
|
||||
|
||||
// Search specifically for deprecated setting by name
|
||||
await searchBox.clear()
|
||||
await searchBox.fill('Deprecated')
|
||||
|
||||
// Should not show the deprecated setting even when searching by name
|
||||
await expect(settingsContent).not.toContainText('Test Deprecated Setting')
|
||||
await expect(contentArea).not.toContainText('Test Deprecated Setting')
|
||||
|
||||
// Search for visible setting by name - should work
|
||||
await searchBox.clear()
|
||||
await searchBox.fill('Visible')
|
||||
|
||||
// Should show the visible setting
|
||||
await expect(settingsContent).toContainText('Test Visible Setting')
|
||||
await expect(contentArea).toContainText('Test Visible Setting')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -89,12 +89,12 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useSettingsDialog } from '@/platform/settings/composables/useSettingsDialog'
|
||||
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||
import type { BottomPanelExtension } from '@/types/extensionTypes'
|
||||
|
||||
const bottomPanelStore = useBottomPanelStore()
|
||||
const dialogService = useDialogService()
|
||||
const settingsDialog = useSettingsDialog()
|
||||
const { t } = useI18n()
|
||||
|
||||
const isShortcutsTabActive = computed(() => {
|
||||
@@ -115,7 +115,7 @@ const getTabDisplayTitle = (tab: BottomPanelExtension): string => {
|
||||
}
|
||||
|
||||
const openKeybindingSettings = async () => {
|
||||
dialogService.showSettingsDialog('keybinding')
|
||||
settingsDialog.show('keybinding')
|
||||
}
|
||||
|
||||
const closeBottomPanel = () => {
|
||||
|
||||
@@ -4,12 +4,7 @@
|
||||
v-for="item in dialogStore.dialogStack"
|
||||
:key="item.key"
|
||||
v-model:visible="item.visible"
|
||||
:class="[
|
||||
'global-dialog',
|
||||
item.key === 'global-settings' && teamWorkspacesEnabled
|
||||
? 'settings-dialog-workspace'
|
||||
: ''
|
||||
]"
|
||||
class="global-dialog"
|
||||
v-bind="item.dialogComponentProps"
|
||||
:pt="item.dialogComponentProps.pt"
|
||||
:aria-labelledby="item.key"
|
||||
@@ -43,15 +38,7 @@
|
||||
<script setup lang="ts">
|
||||
import Dialog from 'primevue/dialog'
|
||||
|
||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const { flags } = useFeatureFlags()
|
||||
const teamWorkspacesEnabled = computed(
|
||||
() => isCloud && flags.teamWorkspacesEnabled
|
||||
)
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
</script>
|
||||
@@ -69,16 +56,6 @@ const dialogStore = useDialogStore()
|
||||
@apply pt-0;
|
||||
}
|
||||
|
||||
/* Workspace mode: wider settings dialog */
|
||||
.settings-dialog-workspace {
|
||||
width: 100%;
|
||||
max-width: 1440px;
|
||||
}
|
||||
|
||||
.settings-dialog-workspace .p-dialog-content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.manager-dialog {
|
||||
height: 80vh;
|
||||
max-width: 1724px;
|
||||
|
||||
@@ -161,7 +161,7 @@ import { useExternalLink } from '@/composables/useExternalLink'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { clearTopupTracking } from '@/platform/telemetry/topupTracker'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useSettingsDialog } from '@/platform/settings/composables/useSettingsDialog'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
@@ -172,7 +172,7 @@ const { isInsufficientCredits = false } = defineProps<{
|
||||
const { t } = useI18n()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const dialogStore = useDialogStore()
|
||||
const dialogService = useDialogService()
|
||||
const settingsDialog = useSettingsDialog()
|
||||
const telemetry = useTelemetry()
|
||||
const toast = useToast()
|
||||
const { buildDocsUrl, docsPaths } = useExternalLink()
|
||||
@@ -256,9 +256,7 @@ async function handleBuy() {
|
||||
|
||||
// Close top-up dialog (keep tracking) and open credits panel to show updated balance
|
||||
handleClose(false)
|
||||
dialogService.showSettingsDialog(
|
||||
isSubscriptionEnabled() ? 'subscription' : 'credits'
|
||||
)
|
||||
settingsDialog.show(isSubscriptionEnabled() ? 'subscription' : 'credits')
|
||||
} catch (error) {
|
||||
console.error('Purchase failed:', error)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<PanelTemplate value="About" class="about-container">
|
||||
<div class="about-container flex flex-col gap-2">
|
||||
<h2 class="mb-2 text-2xl font-bold">
|
||||
{{ $t('g.about') }}
|
||||
</h2>
|
||||
@@ -28,7 +28,7 @@
|
||||
v-if="systemStatsStore.systemStats"
|
||||
:stats="systemStatsStore.systemStats"
|
||||
/>
|
||||
</PanelTemplate>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -39,8 +39,6 @@ import SystemStatsPanel from '@/components/common/SystemStatsPanel.vue'
|
||||
import { useAboutPanelStore } from '@/stores/aboutPanelStore'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
|
||||
import PanelTemplate from './PanelTemplate.vue'
|
||||
|
||||
const systemStatsStore = useSystemStatsStore()
|
||||
const aboutPanelStore = useAboutPanelStore()
|
||||
</script>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<template>
|
||||
<PanelTemplate value="Keybinding" class="keybinding-panel">
|
||||
<template #header>
|
||||
<SearchBox
|
||||
v-model="filters['global'].value"
|
||||
:placeholder="$t('g.searchKeybindings') + '...'"
|
||||
/>
|
||||
</template>
|
||||
<div class="keybinding-panel flex flex-col gap-2">
|
||||
<SearchBox
|
||||
v-model="filters['global'].value"
|
||||
:placeholder="$t('g.searchKeybindings') + '...'"
|
||||
/>
|
||||
|
||||
<DataTable
|
||||
v-model:selection="selectedCommandData"
|
||||
@@ -133,7 +131,7 @@
|
||||
<i class="pi pi-replay" />
|
||||
{{ $t('g.resetAll') }}
|
||||
</Button>
|
||||
</PanelTemplate>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -159,7 +157,6 @@ import {
|
||||
} from '@/stores/keybindingStore'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
|
||||
import PanelTemplate from './PanelTemplate.vue'
|
||||
import KeyComboDisplay from './keybinding/KeyComboDisplay.vue'
|
||||
|
||||
const filters = ref({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<TabPanel value="Credits" class="credits-container h-full">
|
||||
<div class="credits-container h-full">
|
||||
<!-- Legacy Design -->
|
||||
<div class="flex h-full flex-col">
|
||||
<h2 class="mb-2 text-2xl font-bold">
|
||||
@@ -102,7 +102,7 @@
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -110,7 +110,6 @@ import Column from 'primevue/column'
|
||||
import DataTable from 'primevue/datatable'
|
||||
import Divider from 'primevue/divider'
|
||||
import Skeleton from 'primevue/skeleton'
|
||||
import TabPanel from 'primevue/tabpanel'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import UserCredit from '@/components/common/UserCredit.vue'
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<template>
|
||||
<TabPanel :value="props.value" class="h-full w-full" :class="props.class">
|
||||
<div class="flex h-full w-full flex-col gap-2">
|
||||
<slot name="header" />
|
||||
<ScrollPanel class="h-0 grow pr-2">
|
||||
<slot />
|
||||
</ScrollPanel>
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</TabPanel>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
import TabPanel from 'primevue/tabpanel'
|
||||
|
||||
const props = defineProps<{
|
||||
value: string
|
||||
class?: string
|
||||
}>()
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<TabPanel value="User" class="user-settings-container h-full">
|
||||
<div class="user-settings-container h-full">
|
||||
<div class="flex h-full flex-col">
|
||||
<h2 class="mb-2 text-2xl font-bold">{{ $t('userSettings.title') }}</h2>
|
||||
<Divider class="mb-3" />
|
||||
@@ -91,13 +91,12 @@
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Divider from 'primevue/divider'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import TabPanel from 'primevue/tabpanel'
|
||||
|
||||
import UserAvatar from '@/components/common/UserAvatar.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<template>
|
||||
<TabPanel value="Workspace" class="h-full">
|
||||
<div class="h-full">
|
||||
<WorkspacePanelContent />
|
||||
</TabPanel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TabPanel from 'primevue/tabpanel'
|
||||
|
||||
import WorkspacePanelContent from '@/components/dialog/content/setting/WorkspacePanelContent.vue'
|
||||
</script>
|
||||
|
||||
@@ -9,70 +9,64 @@
|
||||
{{ workspaceName }}
|
||||
</h1>
|
||||
</div>
|
||||
<Tabs :value="activeTab" @update:value="setActiveTab">
|
||||
<div class="flex w-full items-center">
|
||||
<TabList class="w-full">
|
||||
<Tab value="plan">{{ $t('workspacePanel.tabs.planCredits') }}</Tab>
|
||||
</TabList>
|
||||
<div class="flex w-full items-center">
|
||||
<TabList v-model="activeTab">
|
||||
<Tab value="plan">{{ $t('workspacePanel.tabs.planCredits') }}</Tab>
|
||||
</TabList>
|
||||
|
||||
<template v-if="permissions.canAccessWorkspaceMenu">
|
||||
<Button
|
||||
v-tooltip="{ value: $t('g.moreOptions'), showDelay: 300 }"
|
||||
variant="muted-textonly"
|
||||
size="icon"
|
||||
:aria-label="$t('g.moreOptions')"
|
||||
@click="menu?.toggle($event)"
|
||||
>
|
||||
<i class="pi pi-ellipsis-h" />
|
||||
</Button>
|
||||
<Menu ref="menu" :model="menuItems" :popup="true">
|
||||
<template #item="{ item }">
|
||||
<div
|
||||
v-tooltip="
|
||||
item.disabled && deleteTooltip
|
||||
? { value: deleteTooltip, showDelay: 0 }
|
||||
: null
|
||||
"
|
||||
:class="[
|
||||
'flex items-center gap-2 px-3 py-2',
|
||||
item.class,
|
||||
item.disabled ? 'pointer-events-auto' : ''
|
||||
]"
|
||||
@click="
|
||||
item.command?.({
|
||||
originalEvent: $event,
|
||||
item
|
||||
})
|
||||
"
|
||||
>
|
||||
<i :class="item.icon" />
|
||||
<span>{{ item.label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</Menu>
|
||||
</template>
|
||||
</div>
|
||||
<template v-if="permissions.canAccessWorkspaceMenu">
|
||||
<Button
|
||||
v-tooltip="{ value: $t('g.moreOptions'), showDelay: 300 }"
|
||||
variant="muted-textonly"
|
||||
size="icon"
|
||||
:aria-label="$t('g.moreOptions')"
|
||||
@click="menu?.toggle($event)"
|
||||
>
|
||||
<i class="pi pi-ellipsis-h" />
|
||||
</Button>
|
||||
<Menu ref="menu" :model="menuItems" :popup="true">
|
||||
<template #item="{ item }">
|
||||
<div
|
||||
v-tooltip="
|
||||
item.disabled && deleteTooltip
|
||||
? { value: deleteTooltip, showDelay: 0 }
|
||||
: null
|
||||
"
|
||||
:class="[
|
||||
'flex items-center gap-2 px-3 py-2',
|
||||
item.class,
|
||||
item.disabled ? 'pointer-events-auto' : ''
|
||||
]"
|
||||
@click="
|
||||
item.command?.({
|
||||
originalEvent: $event,
|
||||
item
|
||||
})
|
||||
"
|
||||
>
|
||||
<i :class="item.icon" />
|
||||
<span>{{ item.label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</Menu>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<TabPanels>
|
||||
<TabPanel value="plan">
|
||||
<SubscriptionPanelContent />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
<div v-if="activeTab === 'plan'" class="pt-4">
|
||||
<SubscriptionPanelContent />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import Menu from 'primevue/menu'
|
||||
import Tab from 'primevue/tab'
|
||||
import TabList from 'primevue/tablist'
|
||||
import TabPanel from 'primevue/tabpanel'
|
||||
import TabPanels from 'primevue/tabpanels'
|
||||
import Tabs from 'primevue/tabs'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Tab from '@/components/tab/Tab.vue'
|
||||
import TabList from '@/components/tab/TabList.vue'
|
||||
|
||||
import WorkspaceProfilePic from '@/components/common/WorkspaceProfilePic.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import SubscriptionPanelContent from '@/platform/cloud/subscription/components/SubscriptionPanelContentWorkspace.vue'
|
||||
@@ -93,7 +87,8 @@ const {
|
||||
const workspaceStore = useTeamWorkspaceStore()
|
||||
const { workspaceName, isWorkspaceSubscribed } = storeToRefs(workspaceStore)
|
||||
|
||||
const { activeTab, setActiveTab, permissions, uiConfig } = useWorkspaceUI()
|
||||
const { permissions, uiConfig } = useWorkspaceUI()
|
||||
const activeTab = ref(defaultTab)
|
||||
|
||||
const menu = ref<InstanceType<typeof Menu> | null>(null)
|
||||
|
||||
@@ -157,7 +152,4 @@ const menuItems = computed(() => {
|
||||
return items
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
setActiveTab(defaultTab)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
<template>
|
||||
<div class="flex items-center gap-2">
|
||||
<WorkspaceProfilePic
|
||||
class="size-6 text-xs"
|
||||
:workspace-name="workspaceName"
|
||||
/>
|
||||
|
||||
<span>{{ workspaceName }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
import WorkspaceProfilePic from '@/components/common/WorkspaceProfilePic.vue'
|
||||
import { useTeamWorkspaceStore } from '@/platform/workspace/stores/teamWorkspaceStore'
|
||||
|
||||
const { workspaceName } = storeToRefs(useTeamWorkspaceStore())
|
||||
</script>
|
||||
@@ -1,29 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="px-4">
|
||||
<i class="pi pi-cog" />
|
||||
<span>{{ $t('g.settings') }}</span>
|
||||
<Tag
|
||||
v-if="isStaging"
|
||||
value="staging"
|
||||
severity="warn"
|
||||
class="ml-2 text-xs"
|
||||
/>
|
||||
</h2>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import Tag from 'primevue/tag'
|
||||
|
||||
import { isStaging } from '@/config/staging'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pi-cog {
|
||||
font-size: 1.25rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
.version-tag {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -11,7 +11,7 @@ import type { LinkRenderType } from '@/lib/litegraph/src/types/globalEnums'
|
||||
import { LinkMarkerShape } from '@/lib/litegraph/src/types/globalEnums'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { WidgetInputBaseClass } from '@/renderer/extensions/vueNodes/widgets/components/layout'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useSettingsDialog } from '@/platform/settings/composables/useSettingsDialog'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import PropertiesAccordionItem from '../layout/PropertiesAccordionItem.vue'
|
||||
@@ -20,7 +20,7 @@ import LayoutField from './LayoutField.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const settingStore = useSettingStore()
|
||||
const dialogService = useDialogService()
|
||||
const settingsDialog = useSettingsDialog()
|
||||
|
||||
// NODES settings
|
||||
const showAdvancedParameters = ref(false) // Placeholder for future implementation
|
||||
@@ -88,7 +88,7 @@ function updateGridSpacingFromInput(value: number | null | undefined) {
|
||||
}
|
||||
|
||||
function openFullSettings() {
|
||||
dialogService.showSettingsDialog()
|
||||
settingsDialog.show()
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -108,15 +108,13 @@ import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.vue'
|
||||
import ComfyLogo from '@/components/icons/ComfyLogo.vue'
|
||||
import { useWorkflowTemplateSelectorDialog } from '@/composables/useWorkflowTemplateSelectorDialog'
|
||||
import SettingDialogContent from '@/platform/settings/components/SettingDialogContent.vue'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useColorPaletteService } from '@/services/colorPaletteService'
|
||||
import { useSettingsDialog } from '@/platform/settings/composables/useSettingsDialog'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useMenuItemStore } from '@/stores/menuItemStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
@@ -129,7 +127,7 @@ const commandStore = useCommandStore()
|
||||
const menuItemStore = useMenuItemStore()
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
const colorPaletteService = useColorPaletteService()
|
||||
const dialogStore = useDialogStore()
|
||||
const settingsDialog = useSettingsDialog()
|
||||
const managerState = useManagerState()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
@@ -167,14 +165,9 @@ const translateMenuItem = (item: MenuItem): MenuItem => {
|
||||
}
|
||||
|
||||
const showSettings = (defaultPanel?: string) => {
|
||||
dialogStore.showDialog({
|
||||
key: 'global-settings',
|
||||
headerComponent: SettingDialogHeader,
|
||||
component: SettingDialogContent,
|
||||
props: {
|
||||
defaultPanel
|
||||
}
|
||||
})
|
||||
settingsDialog.show(
|
||||
defaultPanel as Parameters<typeof settingsDialog.show>[0]
|
||||
)
|
||||
}
|
||||
|
||||
const showManageExtensions = async () => {
|
||||
|
||||
@@ -31,6 +31,15 @@ vi.mock('pinia')
|
||||
const mockShowSettingsDialog = vi.fn()
|
||||
const mockShowTopUpCreditsDialog = vi.fn()
|
||||
|
||||
// Mock the settings dialog composable
|
||||
vi.mock('@/platform/settings/composables/useSettingsDialog', () => ({
|
||||
useSettingsDialog: vi.fn(() => ({
|
||||
show: mockShowSettingsDialog,
|
||||
hide: vi.fn(),
|
||||
showAbout: vi.fn()
|
||||
}))
|
||||
}))
|
||||
|
||||
// Mock window.open
|
||||
const originalWindowOpen = window.open
|
||||
beforeEach(() => {
|
||||
@@ -64,7 +73,6 @@ vi.mock('@/composables/auth/useFirebaseAuthActions', () => ({
|
||||
// Mock the dialog service
|
||||
vi.mock('@/services/dialogService', () => ({
|
||||
useDialogService: vi.fn(() => ({
|
||||
showSettingsDialog: mockShowSettingsDialog,
|
||||
showTopUpCreditsDialog: mockShowTopUpCreditsDialog
|
||||
}))
|
||||
}))
|
||||
|
||||
@@ -152,6 +152,7 @@ import { useSubscription } from '@/platform/cloud/subscription/composables/useSu
|
||||
import { useSubscriptionDialog } from '@/platform/cloud/subscription/composables/useSubscriptionDialog'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useSettingsDialog } from '@/platform/settings/composables/useSettingsDialog'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
@@ -165,6 +166,7 @@ const { userDisplayName, userEmail, userPhotoUrl, handleSignOut } =
|
||||
useCurrentUser()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const settingsDialog = useSettingsDialog()
|
||||
const dialogService = useDialogService()
|
||||
const {
|
||||
isActiveSubscription,
|
||||
@@ -198,7 +200,7 @@ const canUpgrade = computed(() => {
|
||||
})
|
||||
|
||||
const handleOpenUserSettings = () => {
|
||||
dialogService.showSettingsDialog('user')
|
||||
settingsDialog.show('user')
|
||||
emit('close')
|
||||
}
|
||||
|
||||
@@ -209,9 +211,9 @@ const handleOpenPlansAndPricing = () => {
|
||||
|
||||
const handleOpenPlanAndCreditsSettings = () => {
|
||||
if (isCloud) {
|
||||
dialogService.showSettingsDialog('subscription')
|
||||
settingsDialog.show('subscription')
|
||||
} else {
|
||||
dialogService.showSettingsDialog('credits')
|
||||
settingsDialog.show('credits')
|
||||
}
|
||||
|
||||
emit('close')
|
||||
|
||||
@@ -215,6 +215,7 @@ import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useWorkspaceUI } from '@/platform/workspace/composables/useWorkspaceUI'
|
||||
import { useTeamWorkspaceStore } from '@/platform/workspace/stores/teamWorkspaceStore'
|
||||
import { useSettingsDialog } from '@/platform/settings/composables/useSettingsDialog'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
|
||||
const workspaceStore = useTeamWorkspaceStore()
|
||||
@@ -236,6 +237,7 @@ const { buildDocsUrl, docsPaths } = useExternalLink()
|
||||
const { userDisplayName, userEmail, userPhotoUrl, handleSignOut } =
|
||||
useCurrentUser()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const settingsDialog = useSettingsDialog()
|
||||
const dialogService = useDialogService()
|
||||
const { isActiveSubscription } = useSubscription()
|
||||
const { totalCredits, isLoadingBalance } = useSubscriptionCredits()
|
||||
@@ -277,12 +279,12 @@ const showCreditsSection = computed(
|
||||
)
|
||||
|
||||
const handleOpenUserSettings = () => {
|
||||
dialogService.showSettingsDialog('user')
|
||||
settingsDialog.show('user')
|
||||
emit('close')
|
||||
}
|
||||
|
||||
const handleOpenWorkspaceSettings = () => {
|
||||
dialogService.showSettingsDialog('workspace')
|
||||
settingsDialog.show('workspace')
|
||||
emit('close')
|
||||
}
|
||||
|
||||
@@ -293,9 +295,9 @@ const handleOpenPlansAndPricing = () => {
|
||||
|
||||
const handleOpenPlanAndCreditsSettings = () => {
|
||||
if (isCloud) {
|
||||
dialogService.showSettingsDialog('workspace')
|
||||
settingsDialog.show('workspace')
|
||||
} else {
|
||||
dialogService.showSettingsDialog('credits')
|
||||
settingsDialog.show('credits')
|
||||
}
|
||||
|
||||
emit('close')
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
} from '@/renderer/core/canvas/canvasStore'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useSettingsDialog } from '@/platform/settings/composables/useSettingsDialog'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import type { ComfyCommand } from '@/stores/commandStore'
|
||||
@@ -73,6 +74,7 @@ const moveSelectedNodesVersionAdded = '1.22.2'
|
||||
export function useCoreCommands(): ComfyCommand[] {
|
||||
const workflowService = useWorkflowService()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const settingsDialog = useSettingsDialog()
|
||||
const dialogService = useDialogService()
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
const firebaseAuthActions = useFirebaseAuthActions()
|
||||
@@ -581,7 +583,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
versionAdded: '1.3.7',
|
||||
category: 'view-controls' as const,
|
||||
function: () => {
|
||||
dialogService.showSettingsDialog()
|
||||
settingsDialog.show()
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -830,7 +832,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
menubarLabel: 'About ComfyUI',
|
||||
versionAdded: '1.6.4',
|
||||
function: () => {
|
||||
dialogService.showSettingsDialog('about')
|
||||
settingsDialog.showAbout()
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { WORKFLOW_ACCEPT_STRING } from '@/platform/workflow/core/types/formats'
|
||||
import { type StatusWsMessageStatus } from '@/schemas/apiSchema'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useSettingsDialog } from '@/platform/settings/composables/useSettingsDialog'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
@@ -469,7 +469,7 @@ export class ComfyUI {
|
||||
$el('button.comfy-settings-btn', {
|
||||
textContent: '⚙️',
|
||||
onclick: () => {
|
||||
useDialogService().showSettingsDialog()
|
||||
useSettingsDialog().show()
|
||||
}
|
||||
}),
|
||||
$el('button.comfy-close-menu-btn', {
|
||||
|
||||
@@ -13,12 +13,10 @@ import SignInContent from '@/components/dialog/content/SignInContent.vue'
|
||||
import TopUpCreditsDialogContent from '@/components/dialog/content/TopUpCreditsDialogContent.vue'
|
||||
import UpdatePasswordContent from '@/components/dialog/content/UpdatePasswordContent.vue'
|
||||
import ComfyOrgHeader from '@/components/dialog/header/ComfyOrgHeader.vue'
|
||||
import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.vue'
|
||||
import { t } from '@/i18n'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
import SettingDialogContent from '@/platform/settings/components/SettingDialogContent.vue'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import type {
|
||||
DialogComponentProps,
|
||||
@@ -93,38 +91,6 @@ export const useDialogService = () => {
|
||||
})
|
||||
}
|
||||
|
||||
function showSettingsDialog(
|
||||
panel?:
|
||||
| 'about'
|
||||
| 'keybinding'
|
||||
| 'extension'
|
||||
| 'server-config'
|
||||
| 'user'
|
||||
| 'credits'
|
||||
| 'subscription'
|
||||
| 'workspace'
|
||||
) {
|
||||
const props = panel ? { props: { defaultPanel: panel } } : undefined
|
||||
|
||||
dialogStore.showDialog({
|
||||
key: 'global-settings',
|
||||
headerComponent: SettingDialogHeader,
|
||||
component: SettingDialogContent,
|
||||
...props
|
||||
})
|
||||
}
|
||||
|
||||
function showAboutDialog() {
|
||||
dialogStore.showDialog({
|
||||
key: 'global-settings',
|
||||
headerComponent: SettingDialogHeader,
|
||||
component: SettingDialogContent,
|
||||
props: {
|
||||
defaultPanel: 'about'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function showExecutionErrorDialog(executionError: ExecutionErrorDialogInput) {
|
||||
const props: ComponentAttrs<typeof ErrorDialogContent> = {
|
||||
error: {
|
||||
@@ -592,8 +558,6 @@ export const useDialogService = () => {
|
||||
return {
|
||||
showLoadWorkflowWarning,
|
||||
showMissingModelsWarning,
|
||||
showSettingsDialog,
|
||||
showAboutDialog,
|
||||
showExecutionErrorDialog,
|
||||
showApiNodesSignInDialog,
|
||||
showSignInDialog,
|
||||
|
||||
@@ -102,7 +102,6 @@ describe('useFirebaseAuthStore', () => {
|
||||
|
||||
// Setup dialog service mock
|
||||
vi.mocked(useDialogService, { partial: true }).mockReturnValue({
|
||||
showSettingsDialog: vi.fn(),
|
||||
showErrorDialog: vi.fn()
|
||||
})
|
||||
|
||||
|
||||
@@ -33,11 +33,11 @@ vi.mock('@/stores/systemStatsStore', () => ({
|
||||
useSystemStatsStore: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/services/dialogService', () => ({
|
||||
useDialogService: vi.fn(() => ({
|
||||
showManagerPopup: vi.fn(),
|
||||
showLegacyManagerPopup: vi.fn(),
|
||||
showSettingsDialog: vi.fn()
|
||||
vi.mock('@/platform/settings/composables/useSettingsDialog', () => ({
|
||||
useSettingsDialog: vi.fn(() => ({
|
||||
show: vi.fn(),
|
||||
hide: vi.fn(),
|
||||
showAbout: vi.fn()
|
||||
}))
|
||||
}))
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { computed, readonly } from 'vue'
|
||||
import { t } from '@/i18n'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useSettingsDialog } from '@/platform/settings/composables/useSettingsDialog'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
import { useManagerDialog } from '@/workbench/extensions/manager/composables/useManagerDialog'
|
||||
@@ -148,12 +148,12 @@ export function useManagerState() {
|
||||
isLegacyOnly?: boolean
|
||||
}): Promise<void> => {
|
||||
const state = managerUIState.value
|
||||
const dialogService = useDialogService()
|
||||
const settingsDialog = useSettingsDialog()
|
||||
const commandStore = useCommandStore()
|
||||
|
||||
switch (state) {
|
||||
case ManagerUIState.DISABLED:
|
||||
dialogService.showSettingsDialog('extension')
|
||||
settingsDialog.show('extension')
|
||||
break
|
||||
|
||||
case ManagerUIState.LEGACY_UI: {
|
||||
@@ -173,7 +173,7 @@ export function useManagerState() {
|
||||
}
|
||||
// Fallback to extensions panel if not showing toast
|
||||
if (options?.showToastOnLegacyError === false) {
|
||||
dialogService.showSettingsDialog('extension')
|
||||
settingsDialog.show('extension')
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
Reference in New Issue
Block a user