mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 19:21:54 +00:00
[backport 1.25] Keyboard Shortcut Bottom Panel (#4813)
Co-authored-by: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com>
This commit is contained in:
280
browser_tests/tests/bottomPanelShortcuts.spec.ts
Normal file
280
browser_tests/tests/bottomPanelShortcuts.spec.ts
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
import { expect } from '@playwright/test'
|
||||||
|
|
||||||
|
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||||
|
|
||||||
|
test.describe('Bottom Panel Shortcuts', () => {
|
||||||
|
test.beforeEach(async ({ comfyPage }) => {
|
||||||
|
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should toggle shortcuts panel visibility', async ({ comfyPage }) => {
|
||||||
|
// Initially shortcuts panel should be hidden
|
||||||
|
await expect(comfyPage.page.locator('.bottom-panel')).not.toBeVisible()
|
||||||
|
|
||||||
|
// Click shortcuts toggle button in sidebar
|
||||||
|
await comfyPage.page
|
||||||
|
.locator('button[aria-label*="Keyboard Shortcuts"]')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
// Shortcuts panel should now be visible
|
||||||
|
await expect(comfyPage.page.locator('.bottom-panel')).toBeVisible()
|
||||||
|
|
||||||
|
// Click toggle button again to hide
|
||||||
|
await comfyPage.page
|
||||||
|
.locator('button[aria-label*="Keyboard Shortcuts"]')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
// Panel should be hidden again
|
||||||
|
await expect(comfyPage.page.locator('.bottom-panel')).not.toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should display essentials shortcuts tab', async ({ comfyPage }) => {
|
||||||
|
// Open shortcuts panel
|
||||||
|
await comfyPage.page
|
||||||
|
.locator('button[aria-label*="Keyboard Shortcuts"]')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
// Essentials tab should be visible and active by default
|
||||||
|
await expect(
|
||||||
|
comfyPage.page.getByRole('tab', { name: /Essential/i })
|
||||||
|
).toBeVisible()
|
||||||
|
await expect(
|
||||||
|
comfyPage.page.getByRole('tab', { name: /Essential/i })
|
||||||
|
).toHaveAttribute('aria-selected', 'true')
|
||||||
|
|
||||||
|
// Should display shortcut categories
|
||||||
|
await expect(
|
||||||
|
comfyPage.page.locator('.subcategory-title').first()
|
||||||
|
).toBeVisible()
|
||||||
|
|
||||||
|
// Should display some keyboard shortcuts
|
||||||
|
await expect(comfyPage.page.locator('.key-badge').first()).toBeVisible()
|
||||||
|
|
||||||
|
// Should have workflow, node, and queue sections
|
||||||
|
await expect(
|
||||||
|
comfyPage.page.getByRole('heading', { name: 'Workflow' })
|
||||||
|
).toBeVisible()
|
||||||
|
await expect(
|
||||||
|
comfyPage.page.getByRole('heading', { name: 'Node' })
|
||||||
|
).toBeVisible()
|
||||||
|
await expect(
|
||||||
|
comfyPage.page.getByRole('heading', { name: 'Queue' })
|
||||||
|
).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should display view controls shortcuts tab', async ({ comfyPage }) => {
|
||||||
|
// Open shortcuts panel
|
||||||
|
await comfyPage.page
|
||||||
|
.locator('button[aria-label*="Keyboard Shortcuts"]')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
// Click view controls tab
|
||||||
|
await comfyPage.page.getByRole('tab', { name: /View Controls/i }).click()
|
||||||
|
|
||||||
|
// View controls tab should be active
|
||||||
|
await expect(
|
||||||
|
comfyPage.page.getByRole('tab', { name: /View Controls/i })
|
||||||
|
).toHaveAttribute('aria-selected', 'true')
|
||||||
|
|
||||||
|
// Should display view controls shortcuts
|
||||||
|
await expect(comfyPage.page.locator('.key-badge').first()).toBeVisible()
|
||||||
|
|
||||||
|
// Should have view and panel controls sections
|
||||||
|
await expect(
|
||||||
|
comfyPage.page.getByRole('heading', { name: 'View' })
|
||||||
|
).toBeVisible()
|
||||||
|
await expect(
|
||||||
|
comfyPage.page.getByRole('heading', { name: 'Panel Controls' })
|
||||||
|
).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should switch between shortcuts tabs', async ({ comfyPage }) => {
|
||||||
|
// Open shortcuts panel
|
||||||
|
await comfyPage.page
|
||||||
|
.locator('button[aria-label*="Keyboard Shortcuts"]')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
// Essentials should be active initially
|
||||||
|
await expect(
|
||||||
|
comfyPage.page.getByRole('tab', { name: /Essential/i })
|
||||||
|
).toHaveAttribute('aria-selected', 'true')
|
||||||
|
|
||||||
|
// Click view controls tab
|
||||||
|
await comfyPage.page.getByRole('tab', { name: /View Controls/i }).click()
|
||||||
|
|
||||||
|
// View controls should now be active
|
||||||
|
await expect(
|
||||||
|
comfyPage.page.getByRole('tab', { name: /View Controls/i })
|
||||||
|
).toHaveAttribute('aria-selected', 'true')
|
||||||
|
await expect(
|
||||||
|
comfyPage.page.getByRole('tab', { name: /Essential/i })
|
||||||
|
).not.toHaveAttribute('aria-selected', 'true')
|
||||||
|
|
||||||
|
// Switch back to essentials
|
||||||
|
await comfyPage.page.getByRole('tab', { name: /Essential/i }).click()
|
||||||
|
|
||||||
|
// Essentials should be active again
|
||||||
|
await expect(
|
||||||
|
comfyPage.page.getByRole('tab', { name: /Essential/i })
|
||||||
|
).toHaveAttribute('aria-selected', 'true')
|
||||||
|
await expect(
|
||||||
|
comfyPage.page.getByRole('tab', { name: /View Controls/i })
|
||||||
|
).not.toHaveAttribute('aria-selected', 'true')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should display formatted keyboard shortcuts', async ({ comfyPage }) => {
|
||||||
|
// Open shortcuts panel
|
||||||
|
await comfyPage.page
|
||||||
|
.locator('button[aria-label*="Keyboard Shortcuts"]')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
// Wait for shortcuts to load
|
||||||
|
await comfyPage.page.waitForSelector('.key-badge')
|
||||||
|
|
||||||
|
// Check for common formatted keys
|
||||||
|
const keyBadges = comfyPage.page.locator('.key-badge')
|
||||||
|
const count = await keyBadges.count()
|
||||||
|
expect(count).toBeGreaterThanOrEqual(1)
|
||||||
|
|
||||||
|
// Should show formatted modifier keys
|
||||||
|
const badgeText = await keyBadges.allTextContents()
|
||||||
|
const hasModifiers = badgeText.some((text) =>
|
||||||
|
['Ctrl', 'Cmd', 'Shift', 'Alt'].includes(text)
|
||||||
|
)
|
||||||
|
expect(hasModifiers).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should maintain panel state when switching to terminal', async ({
|
||||||
|
comfyPage
|
||||||
|
}) => {
|
||||||
|
// Open shortcuts panel first
|
||||||
|
await comfyPage.page
|
||||||
|
.locator('button[aria-label*="Keyboard Shortcuts"]')
|
||||||
|
.click()
|
||||||
|
await expect(comfyPage.page.locator('.bottom-panel')).toBeVisible()
|
||||||
|
|
||||||
|
// Open terminal panel (should switch panels)
|
||||||
|
await comfyPage.page
|
||||||
|
.locator('button[aria-label*="Toggle Bottom Panel"]')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
// Panel should still be visible but showing terminal content
|
||||||
|
await expect(comfyPage.page.locator('.bottom-panel')).toBeVisible()
|
||||||
|
|
||||||
|
// Switch back to shortcuts
|
||||||
|
await comfyPage.page
|
||||||
|
.locator('button[aria-label*="Keyboard Shortcuts"]')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
// Should show shortcuts content again
|
||||||
|
await expect(
|
||||||
|
comfyPage.page.locator('[id*="tab_shortcuts-essentials"]')
|
||||||
|
).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle keyboard navigation', async ({ comfyPage }) => {
|
||||||
|
// Open shortcuts panel
|
||||||
|
await comfyPage.page
|
||||||
|
.locator('button[aria-label*="Keyboard Shortcuts"]')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
// Focus the first tab
|
||||||
|
await comfyPage.page.getByRole('tab', { name: /Essential/i }).focus()
|
||||||
|
|
||||||
|
// Use arrow keys to navigate between tabs
|
||||||
|
await comfyPage.page.keyboard.press('ArrowRight')
|
||||||
|
|
||||||
|
// View controls tab should now have focus
|
||||||
|
await expect(
|
||||||
|
comfyPage.page.getByRole('tab', { name: /View Controls/i })
|
||||||
|
).toBeFocused()
|
||||||
|
|
||||||
|
// Press Enter to activate the tab
|
||||||
|
await comfyPage.page.keyboard.press('Enter')
|
||||||
|
|
||||||
|
// Tab should be selected
|
||||||
|
await expect(
|
||||||
|
comfyPage.page.getByRole('tab', { name: /View Controls/i })
|
||||||
|
).toHaveAttribute('aria-selected', 'true')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should close panel by clicking shortcuts button again', async ({
|
||||||
|
comfyPage
|
||||||
|
}) => {
|
||||||
|
// Open shortcuts panel
|
||||||
|
await comfyPage.page
|
||||||
|
.locator('button[aria-label*="Keyboard Shortcuts"]')
|
||||||
|
.click()
|
||||||
|
await expect(comfyPage.page.locator('.bottom-panel')).toBeVisible()
|
||||||
|
|
||||||
|
// Click shortcuts button again to close
|
||||||
|
await comfyPage.page
|
||||||
|
.locator('button[aria-label*="Keyboard Shortcuts"]')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
// Panel should be hidden
|
||||||
|
await expect(comfyPage.page.locator('.bottom-panel')).not.toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should display shortcuts in organized columns', async ({
|
||||||
|
comfyPage
|
||||||
|
}) => {
|
||||||
|
// Open shortcuts panel
|
||||||
|
await comfyPage.page
|
||||||
|
.locator('button[aria-label*="Keyboard Shortcuts"]')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
// Should have 3-column grid layout
|
||||||
|
await expect(comfyPage.page.locator('.md\\:grid-cols-3')).toBeVisible()
|
||||||
|
|
||||||
|
// Should have multiple subcategory sections
|
||||||
|
const subcategoryTitles = comfyPage.page.locator('.subcategory-title')
|
||||||
|
const titleCount = await subcategoryTitles.count()
|
||||||
|
expect(titleCount).toBeGreaterThanOrEqual(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should open shortcuts panel with Ctrl+Shift+K', async ({
|
||||||
|
comfyPage
|
||||||
|
}) => {
|
||||||
|
// Initially shortcuts panel should be hidden
|
||||||
|
await expect(comfyPage.page.locator('.bottom-panel')).not.toBeVisible()
|
||||||
|
|
||||||
|
// Press Ctrl+Shift+K to open shortcuts panel
|
||||||
|
await comfyPage.page.keyboard.press('Control+Shift+KeyK')
|
||||||
|
|
||||||
|
// Shortcuts panel should now be visible
|
||||||
|
await expect(comfyPage.page.locator('.bottom-panel')).toBeVisible()
|
||||||
|
|
||||||
|
// Should show essentials tab by default
|
||||||
|
await expect(
|
||||||
|
comfyPage.page.getByRole('tab', { name: /Essential/i })
|
||||||
|
).toHaveAttribute('aria-selected', 'true')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should open settings dialog when clicking manage shortcuts button', async ({
|
||||||
|
comfyPage
|
||||||
|
}) => {
|
||||||
|
// Open shortcuts panel
|
||||||
|
await comfyPage.page
|
||||||
|
.locator('button[aria-label*="Keyboard Shortcuts"]')
|
||||||
|
.click()
|
||||||
|
|
||||||
|
// Manage shortcuts button should be visible
|
||||||
|
await expect(
|
||||||
|
comfyPage.page.getByRole('button', { name: /Manage Shortcuts/i })
|
||||||
|
).toBeVisible()
|
||||||
|
|
||||||
|
// Click manage shortcuts button
|
||||||
|
await comfyPage.page
|
||||||
|
.getByRole('button', { name: /Manage Shortcuts/i })
|
||||||
|
.click()
|
||||||
|
|
||||||
|
// Settings dialog should open with keybinding tab
|
||||||
|
await expect(comfyPage.page.getByRole('dialog')).toBeVisible()
|
||||||
|
|
||||||
|
// Should show keybinding settings (check for keybinding-related content)
|
||||||
|
await expect(
|
||||||
|
comfyPage.page.getByRole('option', { name: 'Keybinding' })
|
||||||
|
).toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -11,18 +11,33 @@
|
|||||||
class="p-3 border-none"
|
class="p-3 border-none"
|
||||||
>
|
>
|
||||||
<span class="font-bold">
|
<span class="font-bold">
|
||||||
{{ tab.title.toUpperCase() }}
|
{{
|
||||||
|
shouldCapitalizeTab(tab.id)
|
||||||
|
? tab.title.toUpperCase()
|
||||||
|
: tab.title
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
</Tab>
|
</Tab>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<div class="flex items-center gap-2">
|
||||||
class="justify-self-end"
|
<Button
|
||||||
icon="pi pi-times"
|
v-if="isShortcutsTabActive"
|
||||||
severity="secondary"
|
:label="$t('shortcuts.manageShortcuts')"
|
||||||
size="small"
|
icon="pi pi-cog"
|
||||||
text
|
severity="secondary"
|
||||||
@click="bottomPanelStore.bottomPanelVisible = false"
|
size="small"
|
||||||
/>
|
text
|
||||||
|
@click="openKeybindingSettings"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
class="justify-self-end"
|
||||||
|
icon="pi pi-times"
|
||||||
|
severity="secondary"
|
||||||
|
size="small"
|
||||||
|
text
|
||||||
|
@click="closeBottomPanel"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabList>
|
</TabList>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
@@ -44,9 +59,32 @@ import Button from 'primevue/button'
|
|||||||
import Tab from 'primevue/tab'
|
import Tab from 'primevue/tab'
|
||||||
import TabList from 'primevue/tablist'
|
import TabList from 'primevue/tablist'
|
||||||
import Tabs from 'primevue/tabs'
|
import Tabs from 'primevue/tabs'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
|
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
|
||||||
|
import { useDialogService } from '@/services/dialogService'
|
||||||
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||||
|
|
||||||
const bottomPanelStore = useBottomPanelStore()
|
const bottomPanelStore = useBottomPanelStore()
|
||||||
|
const dialogService = useDialogService()
|
||||||
|
|
||||||
|
const isShortcutsTabActive = computed(() => {
|
||||||
|
const activeTabId = bottomPanelStore.activeBottomPanelTabId
|
||||||
|
return (
|
||||||
|
activeTabId === 'shortcuts-essentials' ||
|
||||||
|
activeTabId === 'shortcuts-view-controls'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const shouldCapitalizeTab = (tabId: string): boolean => {
|
||||||
|
return tabId !== 'shortcuts-essentials' && tabId !== 'shortcuts-view-controls'
|
||||||
|
}
|
||||||
|
|
||||||
|
const openKeybindingSettings = async () => {
|
||||||
|
dialogService.showSettingsDialog('keybinding')
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeBottomPanel = () => {
|
||||||
|
bottomPanelStore.activePanel = null
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-full flex flex-col p-4">
|
||||||
|
<div class="flex-1 min-h-0 overflow-auto">
|
||||||
|
<ShortcutsList
|
||||||
|
:commands="essentialsCommands"
|
||||||
|
:subcategories="essentialsSubcategories"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import {
|
||||||
|
ESSENTIALS_CONFIG,
|
||||||
|
useCommandSubcategories
|
||||||
|
} from '@/composables/bottomPanelTabs/useCommandSubcategories'
|
||||||
|
import { useCommandStore } from '@/stores/commandStore'
|
||||||
|
|
||||||
|
import ShortcutsList from './ShortcutsList.vue'
|
||||||
|
|
||||||
|
const commandStore = useCommandStore()
|
||||||
|
|
||||||
|
const essentialsCommands = computed(() =>
|
||||||
|
commandStore.commands.filter((cmd) => cmd.category === 'essentials')
|
||||||
|
)
|
||||||
|
|
||||||
|
const { subcategories: essentialsSubcategories } = useCommandSubcategories(
|
||||||
|
essentialsCommands,
|
||||||
|
ESSENTIALS_CONFIG
|
||||||
|
)
|
||||||
|
</script>
|
||||||
119
src/components/bottomPanel/tabs/shortcuts/ShortcutsList.vue
Normal file
119
src/components/bottomPanel/tabs/shortcuts/ShortcutsList.vue
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<template>
|
||||||
|
<div class="shortcuts-list flex justify-center">
|
||||||
|
<div class="grid gap-4 md:gap-24 h-full grid-cols-1 md:grid-cols-3 w-[90%]">
|
||||||
|
<div
|
||||||
|
v-for="(subcategoryCommands, subcategory) in filteredSubcategories"
|
||||||
|
:key="subcategory"
|
||||||
|
class="flex flex-col"
|
||||||
|
>
|
||||||
|
<h3
|
||||||
|
class="subcategory-title text-xs font-bold uppercase tracking-wide text-surface-600 dark-theme:text-surface-400 mb-4"
|
||||||
|
>
|
||||||
|
{{ getSubcategoryTitle(subcategory) }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<div
|
||||||
|
v-for="command in subcategoryCommands"
|
||||||
|
:key="command.id"
|
||||||
|
class="shortcut-item flex justify-between items-center py-2 rounded hover:bg-surface-100 dark-theme:hover:bg-surface-700 transition-colors duration-200"
|
||||||
|
>
|
||||||
|
<div class="shortcut-info flex-grow pr-4">
|
||||||
|
<div class="shortcut-name text-sm font-medium">
|
||||||
|
{{ command.label || command.id }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="keybinding-display flex-shrink-0">
|
||||||
|
<div
|
||||||
|
class="keybinding-combo flex gap-1"
|
||||||
|
:aria-label="`Keyboard shortcut: ${command.keybinding!.combo.getKeySequences().join(' + ')}`"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-for="key in command.keybinding!.combo.getKeySequences()"
|
||||||
|
:key="key"
|
||||||
|
class="key-badge px-2 py-1 text-xs font-mono bg-surface-200 dark-theme:bg-surface-600 rounded border min-w-6 text-center"
|
||||||
|
>
|
||||||
|
{{ formatKey(key) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import type { ComfyCommandImpl } from '@/stores/commandStore'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const { subcategories } = defineProps<{
|
||||||
|
commands: ComfyCommandImpl[]
|
||||||
|
subcategories: Record<string, ComfyCommandImpl[]>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const filteredSubcategories = computed(() => {
|
||||||
|
const result: Record<string, ComfyCommandImpl[]> = {}
|
||||||
|
|
||||||
|
for (const [subcategory, commands] of Object.entries(subcategories)) {
|
||||||
|
result[subcategory] = commands.filter((cmd) => !!cmd.keybinding)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
const getSubcategoryTitle = (subcategory: string): string => {
|
||||||
|
const titleMap: Record<string, string> = {
|
||||||
|
workflow: t('shortcuts.subcategories.workflow'),
|
||||||
|
node: t('shortcuts.subcategories.node'),
|
||||||
|
queue: t('shortcuts.subcategories.queue'),
|
||||||
|
view: t('shortcuts.subcategories.view'),
|
||||||
|
'panel-controls': t('shortcuts.subcategories.panelControls')
|
||||||
|
}
|
||||||
|
|
||||||
|
return titleMap[subcategory] || subcategory
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatKey = (key: string): string => {
|
||||||
|
const keyMap: Record<string, string> = {
|
||||||
|
Control: 'Ctrl',
|
||||||
|
Meta: 'Cmd',
|
||||||
|
ArrowUp: '↑',
|
||||||
|
ArrowDown: '↓',
|
||||||
|
ArrowLeft: '←',
|
||||||
|
ArrowRight: '→',
|
||||||
|
Backspace: '⌫',
|
||||||
|
Delete: '⌦',
|
||||||
|
Enter: '↵',
|
||||||
|
Escape: 'Esc',
|
||||||
|
Tab: '⇥',
|
||||||
|
' ': 'Space'
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyMap[key] || key
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.subcategory-title {
|
||||||
|
color: var(--p-text-muted-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-badge {
|
||||||
|
background-color: var(--p-surface-200);
|
||||||
|
border: 1px solid var(--p-surface-300);
|
||||||
|
min-width: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .key-badge {
|
||||||
|
background-color: var(--p-surface-600);
|
||||||
|
border-color: var(--p-surface-500);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-full flex flex-col p-4">
|
||||||
|
<div class="flex-1 min-h-0 overflow-auto">
|
||||||
|
<ShortcutsList
|
||||||
|
:commands="viewControlsCommands"
|
||||||
|
:subcategories="viewControlsSubcategories"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import {
|
||||||
|
VIEW_CONTROLS_CONFIG,
|
||||||
|
useCommandSubcategories
|
||||||
|
} from '@/composables/bottomPanelTabs/useCommandSubcategories'
|
||||||
|
import { useCommandStore } from '@/stores/commandStore'
|
||||||
|
|
||||||
|
import ShortcutsList from './ShortcutsList.vue'
|
||||||
|
|
||||||
|
const commandStore = useCommandStore()
|
||||||
|
|
||||||
|
const viewControlsCommands = computed(() =>
|
||||||
|
commandStore.commands.filter((cmd) => cmd.category === 'view-controls')
|
||||||
|
)
|
||||||
|
|
||||||
|
const { subcategories: viewControlsSubcategories } = useCommandSubcategories(
|
||||||
|
viewControlsCommands,
|
||||||
|
VIEW_CONTROLS_CONFIG
|
||||||
|
)
|
||||||
|
</script>
|
||||||
@@ -2,7 +2,11 @@
|
|||||||
<div
|
<div
|
||||||
v-if="visible && initialized"
|
v-if="visible && initialized"
|
||||||
ref="containerRef"
|
ref="containerRef"
|
||||||
class="litegraph-minimap absolute bottom-[20px] right-[90px] z-[1000]"
|
class="litegraph-minimap absolute right-[90px] z-[1000]"
|
||||||
|
:class="{
|
||||||
|
'bottom-[20px]': !bottomPanelStore.bottomPanelVisible,
|
||||||
|
'bottom-[280px]': bottomPanelStore.bottomPanelVisible
|
||||||
|
}"
|
||||||
:style="containerStyles"
|
:style="containerStyles"
|
||||||
@pointerdown="handlePointerDown"
|
@pointerdown="handlePointerDown"
|
||||||
@pointermove="handlePointerMove"
|
@pointermove="handlePointerMove"
|
||||||
@@ -25,9 +29,11 @@ import { onMounted, onUnmounted, watch } from 'vue'
|
|||||||
|
|
||||||
import { useMinimap } from '@/composables/useMinimap'
|
import { useMinimap } from '@/composables/useMinimap'
|
||||||
import { useCanvasStore } from '@/stores/graphStore'
|
import { useCanvasStore } from '@/stores/graphStore'
|
||||||
|
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||||
|
|
||||||
const minimap = useMinimap()
|
const minimap = useMinimap()
|
||||||
const canvasStore = useCanvasStore()
|
const canvasStore = useCanvasStore()
|
||||||
|
const bottomPanelStore = useBottomPanelStore()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
initialized,
|
initialized,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
<SidebarLogoutIcon v-if="userStore.isMultiUserServer" />
|
<SidebarLogoutIcon v-if="userStore.isMultiUserServer" />
|
||||||
<SidebarHelpCenterIcon />
|
<SidebarHelpCenterIcon />
|
||||||
<SidebarBottomPanelToggleButton />
|
<SidebarBottomPanelToggleButton />
|
||||||
|
<SidebarShortcutsToggleButton />
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</teleport>
|
</teleport>
|
||||||
@@ -32,6 +33,7 @@ import { computed } from 'vue'
|
|||||||
|
|
||||||
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
|
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
|
||||||
import SidebarBottomPanelToggleButton from '@/components/sidebar/SidebarBottomPanelToggleButton.vue'
|
import SidebarBottomPanelToggleButton from '@/components/sidebar/SidebarBottomPanelToggleButton.vue'
|
||||||
|
import SidebarShortcutsToggleButton from '@/components/sidebar/SidebarShortcutsToggleButton.vue'
|
||||||
import { useKeybindingStore } from '@/stores/keybindingStore'
|
import { useKeybindingStore } from '@/stores/keybindingStore'
|
||||||
import { useSettingStore } from '@/stores/settingStore'
|
import { useSettingStore } from '@/stores/settingStore'
|
||||||
import { useUserStore } from '@/stores/userStore'
|
import { useUserStore } from '@/stores/userStore'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<SidebarIcon
|
<SidebarIcon
|
||||||
:tooltip="$t('menu.toggleBottomPanel')"
|
:tooltip="$t('menu.toggleBottomPanel')"
|
||||||
:selected="bottomPanelStore.bottomPanelVisible"
|
:selected="bottomPanelStore.activePanel == 'terminal'"
|
||||||
@click="bottomPanelStore.toggleBottomPanel"
|
@click="bottomPanelStore.toggleBottomPanel"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
|
|||||||
44
src/components/sidebar/SidebarShortcutsToggleButton.vue
Normal file
44
src/components/sidebar/SidebarShortcutsToggleButton.vue
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<template>
|
||||||
|
<SidebarIcon
|
||||||
|
:tooltip="
|
||||||
|
$t('shortcuts.keyboardShortcuts') +
|
||||||
|
' (' +
|
||||||
|
formatKeySequence(command.keybinding!.combo.getKeySequences()) +
|
||||||
|
')'
|
||||||
|
"
|
||||||
|
:selected="isShortcutsPanelVisible"
|
||||||
|
@click="toggleShortcutsPanel"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<i-lucide:keyboard />
|
||||||
|
</template>
|
||||||
|
</SidebarIcon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import { useCommandStore } from '@/stores/commandStore'
|
||||||
|
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||||
|
|
||||||
|
import SidebarIcon from './SidebarIcon.vue'
|
||||||
|
|
||||||
|
const bottomPanelStore = useBottomPanelStore()
|
||||||
|
const command = useCommandStore().getCommand(
|
||||||
|
'Workspace.ToggleBottomPanel.Shortcuts'
|
||||||
|
)
|
||||||
|
|
||||||
|
const isShortcutsPanelVisible = computed(
|
||||||
|
() => bottomPanelStore.activePanel === 'shortcuts'
|
||||||
|
)
|
||||||
|
|
||||||
|
const toggleShortcutsPanel = () => {
|
||||||
|
bottomPanelStore.togglePanel('shortcuts')
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatKeySequence = (sequences: string[]): string => {
|
||||||
|
return sequences
|
||||||
|
.map((seq) => seq.replace(/Control/g, 'Ctrl').replace(/Shift/g, 'Shift'))
|
||||||
|
.join(' + ')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
78
src/composables/bottomPanelTabs/useCommandSubcategories.ts
Normal file
78
src/composables/bottomPanelTabs/useCommandSubcategories.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { type ComputedRef, computed } from 'vue'
|
||||||
|
|
||||||
|
import { type ComfyCommandImpl } from '@/stores/commandStore'
|
||||||
|
|
||||||
|
export type SubcategoryRule = {
|
||||||
|
pattern: string | RegExp
|
||||||
|
subcategory: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SubcategoryConfig = {
|
||||||
|
defaultSubcategory: string
|
||||||
|
rules: SubcategoryRule[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable for grouping commands by subcategory based on configurable rules
|
||||||
|
*/
|
||||||
|
export function useCommandSubcategories(
|
||||||
|
commands: ComputedRef<ComfyCommandImpl[]>,
|
||||||
|
config: SubcategoryConfig
|
||||||
|
) {
|
||||||
|
const subcategories = computed(() => {
|
||||||
|
const result: Record<string, ComfyCommandImpl[]> = {}
|
||||||
|
|
||||||
|
for (const command of commands.value) {
|
||||||
|
let subcategory = config.defaultSubcategory
|
||||||
|
|
||||||
|
// Find the first matching rule
|
||||||
|
for (const rule of config.rules) {
|
||||||
|
const matches =
|
||||||
|
typeof rule.pattern === 'string'
|
||||||
|
? command.id.includes(rule.pattern)
|
||||||
|
: rule.pattern.test(command.id)
|
||||||
|
|
||||||
|
if (matches) {
|
||||||
|
subcategory = rule.subcategory
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result[subcategory]) {
|
||||||
|
result[subcategory] = []
|
||||||
|
}
|
||||||
|
result[subcategory].push(command)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
subcategories
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Predefined configuration for view controls subcategories
|
||||||
|
*/
|
||||||
|
export const VIEW_CONTROLS_CONFIG: SubcategoryConfig = {
|
||||||
|
defaultSubcategory: 'view',
|
||||||
|
rules: [
|
||||||
|
{ pattern: 'Zoom', subcategory: 'view' },
|
||||||
|
{ pattern: 'Fit', subcategory: 'view' },
|
||||||
|
{ pattern: 'Panel', subcategory: 'panel-controls' },
|
||||||
|
{ pattern: 'Sidebar', subcategory: 'panel-controls' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Predefined configuration for essentials subcategories
|
||||||
|
*/
|
||||||
|
export const ESSENTIALS_CONFIG: SubcategoryConfig = {
|
||||||
|
defaultSubcategory: 'workflow',
|
||||||
|
rules: [
|
||||||
|
{ pattern: 'Workflow', subcategory: 'workflow' },
|
||||||
|
{ pattern: 'Node', subcategory: 'node' },
|
||||||
|
{ pattern: 'Queue', subcategory: 'queue' }
|
||||||
|
]
|
||||||
|
}
|
||||||
27
src/composables/bottomPanelTabs/useShortcutsTab.ts
Normal file
27
src/composables/bottomPanelTabs/useShortcutsTab.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { markRaw } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import EssentialsPanel from '@/components/bottomPanel/tabs/shortcuts/EssentialsPanel.vue'
|
||||||
|
import ViewControlsPanel from '@/components/bottomPanel/tabs/shortcuts/ViewControlsPanel.vue'
|
||||||
|
import { BottomPanelExtension } from '@/types/extensionTypes'
|
||||||
|
|
||||||
|
export const useShortcutsTab = (): BottomPanelExtension[] => {
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'shortcuts-essentials',
|
||||||
|
title: t('shortcuts.essentials'),
|
||||||
|
component: markRaw(EssentialsPanel),
|
||||||
|
type: 'vue',
|
||||||
|
targetPanel: 'shortcuts'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'shortcuts-view-controls',
|
||||||
|
title: t('shortcuts.viewControls'),
|
||||||
|
component: markRaw(ViewControlsPanel),
|
||||||
|
type: 'vue',
|
||||||
|
targetPanel: 'shortcuts'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -46,6 +46,9 @@ export function useCoreCommands(): ComfyCommand[] {
|
|||||||
const toastStore = useToastStore()
|
const toastStore = useToastStore()
|
||||||
const canvasStore = useCanvasStore()
|
const canvasStore = useCanvasStore()
|
||||||
const executionStore = useExecutionStore()
|
const executionStore = useExecutionStore()
|
||||||
|
|
||||||
|
const bottomPanelStore = useBottomPanelStore()
|
||||||
|
|
||||||
const { getSelectedNodes, toggleSelectedNodesMode } =
|
const { getSelectedNodes, toggleSelectedNodesMode } =
|
||||||
useSelectedLiteGraphItems()
|
useSelectedLiteGraphItems()
|
||||||
const getTracker = () => workflowStore.activeWorkflow?.changeTracker
|
const getTracker = () => workflowStore.activeWorkflow?.changeTracker
|
||||||
@@ -70,6 +73,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
|||||||
icon: 'pi pi-plus',
|
icon: 'pi pi-plus',
|
||||||
label: 'New Blank Workflow',
|
label: 'New Blank Workflow',
|
||||||
menubarLabel: 'New',
|
menubarLabel: 'New',
|
||||||
|
category: 'essentials' as const,
|
||||||
function: () => workflowService.loadBlankWorkflow()
|
function: () => workflowService.loadBlankWorkflow()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -77,6 +81,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
|||||||
icon: 'pi pi-folder-open',
|
icon: 'pi pi-folder-open',
|
||||||
label: 'Open Workflow',
|
label: 'Open Workflow',
|
||||||
menubarLabel: 'Open',
|
menubarLabel: 'Open',
|
||||||
|
category: 'essentials' as const,
|
||||||
function: () => {
|
function: () => {
|
||||||
app.ui.loadFile()
|
app.ui.loadFile()
|
||||||
}
|
}
|
||||||
@@ -92,6 +97,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
|||||||
icon: 'pi pi-save',
|
icon: 'pi pi-save',
|
||||||
label: 'Save Workflow',
|
label: 'Save Workflow',
|
||||||
menubarLabel: 'Save',
|
menubarLabel: 'Save',
|
||||||
|
category: 'essentials' as const,
|
||||||
function: async () => {
|
function: async () => {
|
||||||
const workflow = useWorkflowStore().activeWorkflow as ComfyWorkflow
|
const workflow = useWorkflowStore().activeWorkflow as ComfyWorkflow
|
||||||
if (!workflow) return
|
if (!workflow) return
|
||||||
@@ -104,6 +110,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
|||||||
icon: 'pi pi-save',
|
icon: 'pi pi-save',
|
||||||
label: 'Save Workflow As',
|
label: 'Save Workflow As',
|
||||||
menubarLabel: 'Save As',
|
menubarLabel: 'Save As',
|
||||||
|
category: 'essentials' as const,
|
||||||
function: async () => {
|
function: async () => {
|
||||||
const workflow = useWorkflowStore().activeWorkflow as ComfyWorkflow
|
const workflow = useWorkflowStore().activeWorkflow as ComfyWorkflow
|
||||||
if (!workflow) return
|
if (!workflow) return
|
||||||
@@ -116,6 +123,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
|||||||
icon: 'pi pi-download',
|
icon: 'pi pi-download',
|
||||||
label: 'Export Workflow',
|
label: 'Export Workflow',
|
||||||
menubarLabel: 'Export',
|
menubarLabel: 'Export',
|
||||||
|
category: 'essentials' as const,
|
||||||
function: async () => {
|
function: async () => {
|
||||||
await workflowService.exportWorkflow('workflow', 'workflow')
|
await workflowService.exportWorkflow('workflow', 'workflow')
|
||||||
}
|
}
|
||||||
@@ -133,6 +141,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
|||||||
id: 'Comfy.Undo',
|
id: 'Comfy.Undo',
|
||||||
icon: 'pi pi-undo',
|
icon: 'pi pi-undo',
|
||||||
label: 'Undo',
|
label: 'Undo',
|
||||||
|
category: 'essentials' as const,
|
||||||
function: async () => {
|
function: async () => {
|
||||||
await getTracker()?.undo?.()
|
await getTracker()?.undo?.()
|
||||||
}
|
}
|
||||||
@@ -141,6 +150,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
|||||||
id: 'Comfy.Redo',
|
id: 'Comfy.Redo',
|
||||||
icon: 'pi pi-refresh',
|
icon: 'pi pi-refresh',
|
||||||
label: 'Redo',
|
label: 'Redo',
|
||||||
|
category: 'essentials' as const,
|
||||||
function: async () => {
|
function: async () => {
|
||||||
await getTracker()?.redo?.()
|
await getTracker()?.redo?.()
|
||||||
}
|
}
|
||||||
@@ -149,6 +159,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
|||||||
id: 'Comfy.ClearWorkflow',
|
id: 'Comfy.ClearWorkflow',
|
||||||
icon: 'pi pi-trash',
|
icon: 'pi pi-trash',
|
||||||
label: 'Clear Workflow',
|
label: 'Clear Workflow',
|
||||||
|
category: 'essentials' as const,
|
||||||
function: () => {
|
function: () => {
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
if (
|
if (
|
||||||
@@ -190,6 +201,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
|||||||
id: 'Comfy.RefreshNodeDefinitions',
|
id: 'Comfy.RefreshNodeDefinitions',
|
||||||
icon: 'pi pi-refresh',
|
icon: 'pi pi-refresh',
|
||||||
label: 'Refresh Node Definitions',
|
label: 'Refresh Node Definitions',
|
||||||
|
category: 'essentials' as const,
|
||||||
function: async () => {
|
function: async () => {
|
||||||
await app.refreshComboInNodes()
|
await app.refreshComboInNodes()
|
||||||
}
|
}
|
||||||
@@ -198,6 +210,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
|||||||
id: 'Comfy.Interrupt',
|
id: 'Comfy.Interrupt',
|
||||||
icon: 'pi pi-stop',
|
icon: 'pi pi-stop',
|
||||||
label: 'Interrupt',
|
label: 'Interrupt',
|
||||||
|
category: 'essentials' as const,
|
||||||
function: async () => {
|
function: async () => {
|
||||||
await api.interrupt(executionStore.activePromptId)
|
await api.interrupt(executionStore.activePromptId)
|
||||||
toastStore.add({
|
toastStore.add({
|
||||||
@@ -212,6 +225,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
|||||||
id: 'Comfy.ClearPendingTasks',
|
id: 'Comfy.ClearPendingTasks',
|
||||||
icon: 'pi pi-stop',
|
icon: 'pi pi-stop',
|
||||||
label: 'Clear Pending Tasks',
|
label: 'Clear Pending Tasks',
|
||||||
|
category: 'essentials' as const,
|
||||||
function: async () => {
|
function: async () => {
|
||||||
await useQueueStore().clear(['queue'])
|
await useQueueStore().clear(['queue'])
|
||||||
toastStore.add({
|
toastStore.add({
|
||||||
@@ -234,6 +248,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
|||||||
id: 'Comfy.Canvas.ZoomIn',
|
id: 'Comfy.Canvas.ZoomIn',
|
||||||
icon: 'pi pi-plus',
|
icon: 'pi pi-plus',
|
||||||
label: 'Zoom In',
|
label: 'Zoom In',
|
||||||
|
category: 'view-controls' as const,
|
||||||
function: () => {
|
function: () => {
|
||||||
const ds = app.canvas.ds
|
const ds = app.canvas.ds
|
||||||
ds.changeScale(
|
ds.changeScale(
|
||||||
@@ -247,6 +262,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
|||||||
id: 'Comfy.Canvas.ZoomOut',
|
id: 'Comfy.Canvas.ZoomOut',
|
||||||
icon: 'pi pi-minus',
|
icon: 'pi pi-minus',
|
||||||
label: 'Zoom Out',
|
label: 'Zoom Out',
|
||||||
|
category: 'view-controls' as const,
|
||||||
function: () => {
|
function: () => {
|
||||||
const ds = app.canvas.ds
|
const ds = app.canvas.ds
|
||||||
ds.changeScale(
|
ds.changeScale(
|
||||||
@@ -260,6 +276,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
|||||||
id: 'Comfy.Canvas.FitView',
|
id: 'Comfy.Canvas.FitView',
|
||||||
icon: 'pi pi-expand',
|
icon: 'pi pi-expand',
|
||||||
label: 'Fit view to selected nodes',
|
label: 'Fit view to selected nodes',
|
||||||
|
category: 'view-controls' as const,
|
||||||
function: () => {
|
function: () => {
|
||||||
if (app.canvas.empty) {
|
if (app.canvas.empty) {
|
||||||
toastStore.add({
|
toastStore.add({
|
||||||
@@ -325,6 +342,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
|||||||
icon: 'pi pi-play',
|
icon: 'pi pi-play',
|
||||||
label: 'Queue Prompt',
|
label: 'Queue Prompt',
|
||||||
versionAdded: '1.3.7',
|
versionAdded: '1.3.7',
|
||||||
|
category: 'essentials' as const,
|
||||||
function: async () => {
|
function: async () => {
|
||||||
const batchCount = useQueueSettingsStore().batchCount
|
const batchCount = useQueueSettingsStore().batchCount
|
||||||
await app.queuePrompt(0, batchCount)
|
await app.queuePrompt(0, batchCount)
|
||||||
@@ -335,6 +353,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
|||||||
icon: 'pi pi-play',
|
icon: 'pi pi-play',
|
||||||
label: 'Queue Prompt (Front)',
|
label: 'Queue Prompt (Front)',
|
||||||
versionAdded: '1.3.7',
|
versionAdded: '1.3.7',
|
||||||
|
category: 'essentials' as const,
|
||||||
function: async () => {
|
function: async () => {
|
||||||
const batchCount = useQueueSettingsStore().batchCount
|
const batchCount = useQueueSettingsStore().batchCount
|
||||||
await app.queuePrompt(-1, batchCount)
|
await app.queuePrompt(-1, batchCount)
|
||||||
@@ -371,6 +390,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
|||||||
icon: 'pi pi-cog',
|
icon: 'pi pi-cog',
|
||||||
label: 'Show Settings Dialog',
|
label: 'Show Settings Dialog',
|
||||||
versionAdded: '1.3.7',
|
versionAdded: '1.3.7',
|
||||||
|
category: 'view-controls' as const,
|
||||||
function: () => {
|
function: () => {
|
||||||
dialogService.showSettingsDialog()
|
dialogService.showSettingsDialog()
|
||||||
}
|
}
|
||||||
@@ -380,6 +400,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
|||||||
icon: 'pi pi-sitemap',
|
icon: 'pi pi-sitemap',
|
||||||
label: 'Group Selected Nodes',
|
label: 'Group Selected Nodes',
|
||||||
versionAdded: '1.3.7',
|
versionAdded: '1.3.7',
|
||||||
|
category: 'essentials' as const,
|
||||||
function: () => {
|
function: () => {
|
||||||
const { canvas } = app
|
const { canvas } = app
|
||||||
if (!canvas.selectedItems?.size) {
|
if (!canvas.selectedItems?.size) {
|
||||||
@@ -423,6 +444,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
|||||||
icon: 'pi pi-volume-off',
|
icon: 'pi pi-volume-off',
|
||||||
label: 'Mute/Unmute Selected Nodes',
|
label: 'Mute/Unmute Selected Nodes',
|
||||||
versionAdded: '1.3.11',
|
versionAdded: '1.3.11',
|
||||||
|
category: 'essentials' as const,
|
||||||
function: () => {
|
function: () => {
|
||||||
toggleSelectedNodesMode(LGraphEventMode.NEVER)
|
toggleSelectedNodesMode(LGraphEventMode.NEVER)
|
||||||
app.canvas.setDirty(true, true)
|
app.canvas.setDirty(true, true)
|
||||||
@@ -433,6 +455,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
|||||||
icon: 'pi pi-shield',
|
icon: 'pi pi-shield',
|
||||||
label: 'Bypass/Unbypass Selected Nodes',
|
label: 'Bypass/Unbypass Selected Nodes',
|
||||||
versionAdded: '1.3.11',
|
versionAdded: '1.3.11',
|
||||||
|
category: 'essentials' as const,
|
||||||
function: () => {
|
function: () => {
|
||||||
toggleSelectedNodesMode(LGraphEventMode.BYPASS)
|
toggleSelectedNodesMode(LGraphEventMode.BYPASS)
|
||||||
app.canvas.setDirty(true, true)
|
app.canvas.setDirty(true, true)
|
||||||
@@ -443,6 +466,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
|||||||
icon: 'pi pi-pin',
|
icon: 'pi pi-pin',
|
||||||
label: 'Pin/Unpin Selected Nodes',
|
label: 'Pin/Unpin Selected Nodes',
|
||||||
versionAdded: '1.3.11',
|
versionAdded: '1.3.11',
|
||||||
|
category: 'essentials' as const,
|
||||||
function: () => {
|
function: () => {
|
||||||
getSelectedNodes().forEach((node) => {
|
getSelectedNodes().forEach((node) => {
|
||||||
node.pin(!node.pinned)
|
node.pin(!node.pinned)
|
||||||
@@ -516,8 +540,9 @@ export function useCoreCommands(): ComfyCommand[] {
|
|||||||
icon: 'pi pi-list',
|
icon: 'pi pi-list',
|
||||||
label: 'Toggle Bottom Panel',
|
label: 'Toggle Bottom Panel',
|
||||||
versionAdded: '1.3.22',
|
versionAdded: '1.3.22',
|
||||||
|
category: 'view-controls' as const,
|
||||||
function: () => {
|
function: () => {
|
||||||
useBottomPanelStore().toggleBottomPanel()
|
bottomPanelStore.toggleBottomPanel()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -525,6 +550,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
|||||||
icon: 'pi pi-eye',
|
icon: 'pi pi-eye',
|
||||||
label: 'Toggle Focus Mode',
|
label: 'Toggle Focus Mode',
|
||||||
versionAdded: '1.3.27',
|
versionAdded: '1.3.27',
|
||||||
|
category: 'view-controls' as const,
|
||||||
function: () => {
|
function: () => {
|
||||||
useWorkspaceStore().toggleFocusMode()
|
useWorkspaceStore().toggleFocusMode()
|
||||||
}
|
}
|
||||||
@@ -750,6 +776,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
|||||||
icon: 'pi pi-sitemap',
|
icon: 'pi pi-sitemap',
|
||||||
label: 'Convert Selection to Subgraph',
|
label: 'Convert Selection to Subgraph',
|
||||||
versionAdded: '1.20.1',
|
versionAdded: '1.20.1',
|
||||||
|
category: 'essentials' as const,
|
||||||
function: () => {
|
function: () => {
|
||||||
const canvas = canvasStore.getCanvas()
|
const canvas = canvasStore.getCanvas()
|
||||||
const graph = canvas.subgraph ?? canvas.graph
|
const graph = canvas.subgraph ?? canvas.graph
|
||||||
@@ -768,6 +795,16 @@ export function useCoreCommands(): ComfyCommand[] {
|
|||||||
const { node } = res
|
const { node } = res
|
||||||
canvas.select(node)
|
canvas.select(node)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Workspace.ToggleBottomPanel.Shortcuts',
|
||||||
|
icon: 'pi pi-key',
|
||||||
|
label: 'Show Keybindings Dialog',
|
||||||
|
versionAdded: '1.24.1',
|
||||||
|
category: 'view-controls' as const,
|
||||||
|
function: () => {
|
||||||
|
bottomPanelStore.togglePanel('shortcuts')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -182,5 +182,13 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
|
|||||||
alt: true
|
alt: true
|
||||||
},
|
},
|
||||||
commandId: 'Comfy.Canvas.ToggleMinimap'
|
commandId: 'Comfy.Canvas.ToggleMinimap'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
combo: {
|
||||||
|
ctrl: true,
|
||||||
|
shift: true,
|
||||||
|
key: 'k'
|
||||||
|
},
|
||||||
|
commandId: 'Workspace.ToggleBottomPanel.Shortcuts'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1630,5 +1630,19 @@
|
|||||||
"clearWorkflow": "Clear Workflow",
|
"clearWorkflow": "Clear Workflow",
|
||||||
"deleteWorkflow": "Delete Workflow",
|
"deleteWorkflow": "Delete Workflow",
|
||||||
"enterNewName": "Enter new name"
|
"enterNewName": "Enter new name"
|
||||||
|
},
|
||||||
|
"shortcuts": {
|
||||||
|
"essentials": "Essential",
|
||||||
|
"viewControls": "View Controls",
|
||||||
|
"manageShortcuts": "Manage Shortcuts",
|
||||||
|
"noKeybinding": "No keybinding",
|
||||||
|
"keyboardShortcuts": "Keyboard Shortcuts",
|
||||||
|
"subcategories": {
|
||||||
|
"workflow": "Workflow",
|
||||||
|
"node": "Node",
|
||||||
|
"queue": "Queue",
|
||||||
|
"view": "View",
|
||||||
|
"panelControls": "Panel Controls"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,6 +17,7 @@ export interface ComfyCommand {
|
|||||||
versionAdded?: string
|
versionAdded?: string
|
||||||
confirmation?: string // If non-nullish, this command will prompt for confirmation
|
confirmation?: string // If non-nullish, this command will prompt for confirmation
|
||||||
source?: string
|
source?: string
|
||||||
|
category?: 'essentials' | 'view-controls' // For shortcuts panel organization
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ComfyCommandImpl implements ComfyCommand {
|
export class ComfyCommandImpl implements ComfyCommand {
|
||||||
@@ -29,6 +30,7 @@ export class ComfyCommandImpl implements ComfyCommand {
|
|||||||
versionAdded?: string
|
versionAdded?: string
|
||||||
confirmation?: string
|
confirmation?: string
|
||||||
source?: string
|
source?: string
|
||||||
|
category?: 'essentials' | 'view-controls'
|
||||||
|
|
||||||
constructor(command: ComfyCommand) {
|
constructor(command: ComfyCommand) {
|
||||||
this.id = command.id
|
this.id = command.id
|
||||||
@@ -40,6 +42,7 @@ export class ComfyCommandImpl implements ComfyCommand {
|
|||||||
this.versionAdded = command.versionAdded
|
this.versionAdded = command.versionAdded
|
||||||
this.confirmation = command.confirmation
|
this.confirmation = command.confirmation
|
||||||
this.source = command.source
|
this.source = command.source
|
||||||
|
this.category = command.category
|
||||||
}
|
}
|
||||||
|
|
||||||
get label() {
|
get label() {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
import { useShortcutsTab } from '@/composables/bottomPanelTabs/useShortcutsTab'
|
||||||
import {
|
import {
|
||||||
useCommandTerminalTab,
|
useCommandTerminalTab,
|
||||||
useLogsTerminalTab
|
useLogsTerminalTab
|
||||||
@@ -10,45 +11,110 @@ import { ComfyExtension } from '@/types/comfy'
|
|||||||
import type { BottomPanelExtension } from '@/types/extensionTypes'
|
import type { BottomPanelExtension } from '@/types/extensionTypes'
|
||||||
import { isElectron } from '@/utils/envUtil'
|
import { isElectron } from '@/utils/envUtil'
|
||||||
|
|
||||||
|
type PanelType = 'terminal' | 'shortcuts'
|
||||||
|
|
||||||
|
interface PanelState {
|
||||||
|
tabs: BottomPanelExtension[]
|
||||||
|
activeTabId: string
|
||||||
|
visible: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export const useBottomPanelStore = defineStore('bottomPanel', () => {
|
export const useBottomPanelStore = defineStore('bottomPanel', () => {
|
||||||
const bottomPanelVisible = ref(false)
|
// Multi-panel state
|
||||||
const toggleBottomPanel = () => {
|
const panels = ref<Record<PanelType, PanelState>>({
|
||||||
// If there are no tabs, don't show the bottom panel
|
terminal: { tabs: [], activeTabId: '', visible: false },
|
||||||
if (bottomPanelTabs.value.length === 0) {
|
shortcuts: { tabs: [], activeTabId: '', visible: false }
|
||||||
return
|
})
|
||||||
|
|
||||||
|
const activePanel = ref<PanelType | null>(null)
|
||||||
|
|
||||||
|
// Computed properties for active panel
|
||||||
|
const activePanelState = computed(() =>
|
||||||
|
activePanel.value ? panels.value[activePanel.value] : null
|
||||||
|
)
|
||||||
|
|
||||||
|
const activeBottomPanelTab = computed<BottomPanelExtension | null>(() => {
|
||||||
|
const state = activePanelState.value
|
||||||
|
if (!state) return null
|
||||||
|
return state.tabs.find((tab) => tab.id === state.activeTabId) ?? null
|
||||||
|
})
|
||||||
|
|
||||||
|
const bottomPanelVisible = computed({
|
||||||
|
get: () => !!activePanel.value,
|
||||||
|
set: (visible: boolean) => {
|
||||||
|
if (!visible) {
|
||||||
|
activePanel.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const bottomPanelTabs = computed(() => activePanelState.value?.tabs ?? [])
|
||||||
|
const activeBottomPanelTabId = computed({
|
||||||
|
get: () => activePanelState.value?.activeTabId ?? '',
|
||||||
|
set: (tabId: string) => {
|
||||||
|
const state = activePanelState.value
|
||||||
|
if (state) {
|
||||||
|
state.activeTabId = tabId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const togglePanel = (panelType: PanelType) => {
|
||||||
|
const panel = panels.value[panelType]
|
||||||
|
if (panel.tabs.length === 0) return
|
||||||
|
|
||||||
|
if (activePanel.value === panelType) {
|
||||||
|
// Hide current panel
|
||||||
|
activePanel.value = null
|
||||||
|
} else {
|
||||||
|
// Show target panel
|
||||||
|
activePanel.value = panelType
|
||||||
|
if (!panel.activeTabId && panel.tabs.length > 0) {
|
||||||
|
panel.activeTabId = panel.tabs[0].id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
bottomPanelVisible.value = !bottomPanelVisible.value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const bottomPanelTabs = ref<BottomPanelExtension[]>([])
|
const toggleBottomPanel = () => {
|
||||||
const activeBottomPanelTabId = ref<string>('')
|
// Legacy method - toggles terminal panel
|
||||||
const activeBottomPanelTab = computed<BottomPanelExtension | null>(() => {
|
togglePanel('terminal')
|
||||||
return (
|
|
||||||
bottomPanelTabs.value.find(
|
|
||||||
(tab) => tab.id === activeBottomPanelTabId.value
|
|
||||||
) ?? null
|
|
||||||
)
|
|
||||||
})
|
|
||||||
const setActiveTab = (tabId: string) => {
|
|
||||||
activeBottomPanelTabId.value = tabId
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setActiveTab = (tabId: string) => {
|
||||||
|
const state = activePanelState.value
|
||||||
|
if (state) {
|
||||||
|
state.activeTabId = tabId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const toggleBottomPanelTab = (tabId: string) => {
|
const toggleBottomPanelTab = (tabId: string) => {
|
||||||
if (activeBottomPanelTabId.value === tabId && bottomPanelVisible.value) {
|
// Find which panel contains this tab
|
||||||
bottomPanelVisible.value = false
|
for (const [panelType, panel] of Object.entries(panels.value)) {
|
||||||
} else {
|
const tab = panel.tabs.find((t) => t.id === tabId)
|
||||||
activeBottomPanelTabId.value = tabId
|
if (tab) {
|
||||||
bottomPanelVisible.value = true
|
if (activePanel.value === panelType && panel.activeTabId === tabId) {
|
||||||
|
activePanel.value = null
|
||||||
|
} else {
|
||||||
|
activePanel.value = panelType as PanelType
|
||||||
|
panel.activeTabId = tabId
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const registerBottomPanelTab = (tab: BottomPanelExtension) => {
|
const registerBottomPanelTab = (tab: BottomPanelExtension) => {
|
||||||
bottomPanelTabs.value = [...bottomPanelTabs.value, tab]
|
const targetPanel = tab.targetPanel ?? 'terminal'
|
||||||
if (bottomPanelTabs.value.length === 1) {
|
const panel = panels.value[targetPanel]
|
||||||
activeBottomPanelTabId.value = tab.id
|
|
||||||
|
panel.tabs = [...panel.tabs, tab]
|
||||||
|
if (panel.tabs.length === 1) {
|
||||||
|
panel.activeTabId = tab.id
|
||||||
}
|
}
|
||||||
|
|
||||||
useCommandStore().registerCommand({
|
useCommandStore().registerCommand({
|
||||||
id: `Workspace.ToggleBottomPanelTab.${tab.id}`,
|
id: `Workspace.ToggleBottomPanelTab.${tab.id}`,
|
||||||
icon: 'pi pi-list',
|
icon: 'pi pi-list',
|
||||||
label: `Toggle ${tab.title} Bottom Panel`,
|
label: `Toggle ${tab.title} Bottom Panel`,
|
||||||
|
category: 'view-controls' as const,
|
||||||
function: () => toggleBottomPanelTab(tab.id),
|
function: () => toggleBottomPanelTab(tab.id),
|
||||||
source: 'System'
|
source: 'System'
|
||||||
})
|
})
|
||||||
@@ -59,6 +125,7 @@ export const useBottomPanelStore = defineStore('bottomPanel', () => {
|
|||||||
if (isElectron()) {
|
if (isElectron()) {
|
||||||
registerBottomPanelTab(useCommandTerminalTab())
|
registerBottomPanelTab(useCommandTerminalTab())
|
||||||
}
|
}
|
||||||
|
useShortcutsTab().forEach(registerBottomPanelTab)
|
||||||
}
|
}
|
||||||
|
|
||||||
const registerExtensionBottomPanelTabs = (extension: ComfyExtension) => {
|
const registerExtensionBottomPanelTabs = (extension: ComfyExtension) => {
|
||||||
@@ -68,6 +135,11 @@ export const useBottomPanelStore = defineStore('bottomPanel', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
// Multi-panel API
|
||||||
|
panels,
|
||||||
|
activePanel,
|
||||||
|
togglePanel,
|
||||||
|
|
||||||
bottomPanelVisible,
|
bottomPanelVisible,
|
||||||
toggleBottomPanel,
|
toggleBottomPanel,
|
||||||
bottomPanelTabs,
|
bottomPanelTabs,
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export const useSidebarTabStore = defineStore('sidebarTab', () => {
|
|||||||
label: labelFunction,
|
label: labelFunction,
|
||||||
tooltip: tooltipFunction,
|
tooltip: tooltipFunction,
|
||||||
versionAdded: '1.3.9',
|
versionAdded: '1.3.9',
|
||||||
|
category: 'view-controls' as const,
|
||||||
function: () => {
|
function: () => {
|
||||||
toggleSidebarTab(tab.id)
|
toggleSidebarTab(tab.id)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export interface BaseSidebarTabExtension {
|
|||||||
export interface BaseBottomPanelExtension {
|
export interface BaseBottomPanelExtension {
|
||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
|
targetPanel?: 'terminal' | 'shortcuts'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VueExtension {
|
export interface VueExtension {
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import { createPinia, setActivePinia } from 'pinia'
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
import EssentialsPanel from '@/components/bottomPanel/tabs/shortcuts/EssentialsPanel.vue'
|
||||||
|
import ShortcutsList from '@/components/bottomPanel/tabs/shortcuts/ShortcutsList.vue'
|
||||||
|
import type { ComfyCommandImpl } from '@/stores/commandStore'
|
||||||
|
|
||||||
|
// Mock ShortcutsList component
|
||||||
|
vi.mock('@/components/bottomPanel/tabs/shortcuts/ShortcutsList.vue', () => ({
|
||||||
|
default: {
|
||||||
|
name: 'ShortcutsList',
|
||||||
|
props: ['commands', 'subcategories', 'columns'],
|
||||||
|
template:
|
||||||
|
'<div class="shortcuts-list-mock">{{ commands.length }} commands</div>'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock command store
|
||||||
|
const mockCommands: ComfyCommandImpl[] = [
|
||||||
|
{
|
||||||
|
id: 'Workflow.New',
|
||||||
|
label: 'New Workflow',
|
||||||
|
category: 'essentials'
|
||||||
|
} as ComfyCommandImpl,
|
||||||
|
{
|
||||||
|
id: 'Node.Add',
|
||||||
|
label: 'Add Node',
|
||||||
|
category: 'essentials'
|
||||||
|
} as ComfyCommandImpl,
|
||||||
|
{
|
||||||
|
id: 'Queue.Clear',
|
||||||
|
label: 'Clear Queue',
|
||||||
|
category: 'essentials'
|
||||||
|
} as ComfyCommandImpl,
|
||||||
|
{
|
||||||
|
id: 'Other.Command',
|
||||||
|
label: 'Other Command',
|
||||||
|
category: 'view-controls',
|
||||||
|
function: vi.fn(),
|
||||||
|
icon: 'pi pi-test',
|
||||||
|
tooltip: 'Test tooltip',
|
||||||
|
menubarLabel: 'Other Command',
|
||||||
|
keybinding: null
|
||||||
|
} as ComfyCommandImpl
|
||||||
|
]
|
||||||
|
|
||||||
|
vi.mock('@/stores/commandStore', () => ({
|
||||||
|
useCommandStore: () => ({
|
||||||
|
commands: mockCommands
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('EssentialsPanel', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePinia(createPinia())
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render ShortcutsList with essentials commands', () => {
|
||||||
|
const wrapper = mount(EssentialsPanel)
|
||||||
|
|
||||||
|
const shortcutsList = wrapper.findComponent(ShortcutsList)
|
||||||
|
expect(shortcutsList.exists()).toBe(true)
|
||||||
|
|
||||||
|
// Should pass only essentials commands
|
||||||
|
const commands = shortcutsList.props('commands')
|
||||||
|
expect(commands).toHaveLength(3)
|
||||||
|
commands.forEach((cmd: ComfyCommandImpl) => {
|
||||||
|
expect(cmd.category).toBe('essentials')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should categorize commands into subcategories', () => {
|
||||||
|
const wrapper = mount(EssentialsPanel)
|
||||||
|
|
||||||
|
const shortcutsList = wrapper.findComponent(ShortcutsList)
|
||||||
|
const subcategories = shortcutsList.props('subcategories')
|
||||||
|
|
||||||
|
expect(subcategories).toHaveProperty('workflow')
|
||||||
|
expect(subcategories).toHaveProperty('node')
|
||||||
|
expect(subcategories).toHaveProperty('queue')
|
||||||
|
|
||||||
|
expect(subcategories.workflow).toContain(mockCommands[0])
|
||||||
|
expect(subcategories.node).toContain(mockCommands[1])
|
||||||
|
expect(subcategories.queue).toContain(mockCommands[2])
|
||||||
|
})
|
||||||
|
})
|
||||||
165
tests-ui/tests/components/bottomPanel/ShortcutsList.spec.ts
Normal file
165
tests-ui/tests/components/bottomPanel/ShortcutsList.spec.ts
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
import ShortcutsList from '@/components/bottomPanel/tabs/shortcuts/ShortcutsList.vue'
|
||||||
|
import type { ComfyCommandImpl } from '@/stores/commandStore'
|
||||||
|
|
||||||
|
// Mock vue-i18n
|
||||||
|
const mockT = vi.fn((key: string) => {
|
||||||
|
const translations: Record<string, string> = {
|
||||||
|
'shortcuts.subcategories.workflow': 'Workflow',
|
||||||
|
'shortcuts.subcategories.node': 'Node',
|
||||||
|
'shortcuts.subcategories.queue': 'Queue',
|
||||||
|
'shortcuts.subcategories.view': 'View',
|
||||||
|
'shortcuts.subcategories.panelControls': 'Panel Controls'
|
||||||
|
}
|
||||||
|
return translations[key] || key
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mock('vue-i18n', () => ({
|
||||||
|
useI18n: () => ({
|
||||||
|
t: mockT
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('ShortcutsList', () => {
|
||||||
|
const mockCommands: ComfyCommandImpl[] = [
|
||||||
|
{
|
||||||
|
id: 'Workflow.New',
|
||||||
|
label: 'New Workflow',
|
||||||
|
category: 'essentials',
|
||||||
|
keybinding: {
|
||||||
|
combo: {
|
||||||
|
getKeySequences: () => ['Control', 'n']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as ComfyCommandImpl,
|
||||||
|
{
|
||||||
|
id: 'Node.Add',
|
||||||
|
label: 'Add Node',
|
||||||
|
category: 'essentials',
|
||||||
|
keybinding: {
|
||||||
|
combo: {
|
||||||
|
getKeySequences: () => ['Shift', 'a']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as ComfyCommandImpl,
|
||||||
|
{
|
||||||
|
id: 'Queue.Clear',
|
||||||
|
label: 'Clear Queue',
|
||||||
|
category: 'essentials',
|
||||||
|
keybinding: {
|
||||||
|
combo: {
|
||||||
|
getKeySequences: () => ['Control', 'Shift', 'c']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as ComfyCommandImpl
|
||||||
|
]
|
||||||
|
|
||||||
|
const mockSubcategories = {
|
||||||
|
workflow: [mockCommands[0]],
|
||||||
|
node: [mockCommands[1]],
|
||||||
|
queue: [mockCommands[2]]
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should render shortcuts organized by subcategories', () => {
|
||||||
|
const wrapper = mount(ShortcutsList, {
|
||||||
|
props: {
|
||||||
|
commands: mockCommands,
|
||||||
|
subcategories: mockSubcategories
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check that subcategories are rendered
|
||||||
|
expect(wrapper.text()).toContain('Workflow')
|
||||||
|
expect(wrapper.text()).toContain('Node')
|
||||||
|
expect(wrapper.text()).toContain('Queue')
|
||||||
|
|
||||||
|
// Check that commands are rendered
|
||||||
|
expect(wrapper.text()).toContain('New Workflow')
|
||||||
|
expect(wrapper.text()).toContain('Add Node')
|
||||||
|
expect(wrapper.text()).toContain('Clear Queue')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should format keyboard shortcuts correctly', () => {
|
||||||
|
const wrapper = mount(ShortcutsList, {
|
||||||
|
props: {
|
||||||
|
commands: mockCommands,
|
||||||
|
subcategories: mockSubcategories
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check for formatted keys
|
||||||
|
expect(wrapper.text()).toContain('Ctrl')
|
||||||
|
expect(wrapper.text()).toContain('n')
|
||||||
|
expect(wrapper.text()).toContain('Shift')
|
||||||
|
expect(wrapper.text()).toContain('a')
|
||||||
|
expect(wrapper.text()).toContain('c')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should filter out commands without keybindings', () => {
|
||||||
|
const commandsWithoutKeybinding: ComfyCommandImpl[] = [
|
||||||
|
...mockCommands,
|
||||||
|
{
|
||||||
|
id: 'No.Keybinding',
|
||||||
|
label: 'No Keybinding',
|
||||||
|
category: 'essentials',
|
||||||
|
keybinding: null
|
||||||
|
} as ComfyCommandImpl
|
||||||
|
]
|
||||||
|
|
||||||
|
const wrapper = mount(ShortcutsList, {
|
||||||
|
props: {
|
||||||
|
commands: commandsWithoutKeybinding,
|
||||||
|
subcategories: {
|
||||||
|
...mockSubcategories,
|
||||||
|
other: [commandsWithoutKeybinding[3]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.text()).not.toContain('No Keybinding')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle special key formatting', () => {
|
||||||
|
const specialKeyCommand: ComfyCommandImpl = {
|
||||||
|
id: 'Special.Keys',
|
||||||
|
label: 'Special Keys',
|
||||||
|
category: 'essentials',
|
||||||
|
keybinding: {
|
||||||
|
combo: {
|
||||||
|
getKeySequences: () => ['Meta', 'ArrowUp', 'Enter', 'Escape', ' ']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as ComfyCommandImpl
|
||||||
|
|
||||||
|
const wrapper = mount(ShortcutsList, {
|
||||||
|
props: {
|
||||||
|
commands: [specialKeyCommand],
|
||||||
|
subcategories: {
|
||||||
|
special: [specialKeyCommand]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const text = wrapper.text()
|
||||||
|
expect(text).toContain('Cmd') // Meta -> Cmd
|
||||||
|
expect(text).toContain('↑') // ArrowUp -> ↑
|
||||||
|
expect(text).toContain('↵') // Enter -> ↵
|
||||||
|
expect(text).toContain('Esc') // Escape -> Esc
|
||||||
|
expect(text).toContain('Space') // ' ' -> Space
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use fallback subcategory titles', () => {
|
||||||
|
const wrapper = mount(ShortcutsList, {
|
||||||
|
props: {
|
||||||
|
commands: mockCommands,
|
||||||
|
subcategories: {
|
||||||
|
unknown: [mockCommands[0]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.text()).toContain('unknown')
|
||||||
|
})
|
||||||
|
})
|
||||||
166
tests-ui/tests/store/bottomPanelStore.test.ts
Normal file
166
tests-ui/tests/store/bottomPanelStore.test.ts
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import { createPinia, setActivePinia } from 'pinia'
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||||
|
import type { BottomPanelExtension } from '@/types/extensionTypes'
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
vi.mock('@/composables/bottomPanelTabs/useShortcutsTab', () => ({
|
||||||
|
useShortcutsTab: () => [
|
||||||
|
{
|
||||||
|
id: 'shortcuts-essentials',
|
||||||
|
title: 'Essentials',
|
||||||
|
component: {},
|
||||||
|
type: 'vue',
|
||||||
|
targetPanel: 'shortcuts'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'shortcuts-view-controls',
|
||||||
|
title: 'View Controls',
|
||||||
|
component: {},
|
||||||
|
type: 'vue',
|
||||||
|
targetPanel: 'shortcuts'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/composables/bottomPanelTabs/useTerminalTabs', () => ({
|
||||||
|
useLogsTerminalTab: () => ({
|
||||||
|
id: 'logs',
|
||||||
|
title: 'Logs',
|
||||||
|
component: {},
|
||||||
|
type: 'vue',
|
||||||
|
targetPanel: 'terminal'
|
||||||
|
}),
|
||||||
|
useCommandTerminalTab: () => ({
|
||||||
|
id: 'command',
|
||||||
|
title: 'Command',
|
||||||
|
component: {},
|
||||||
|
type: 'vue',
|
||||||
|
targetPanel: 'terminal'
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/stores/commandStore', () => ({
|
||||||
|
useCommandStore: () => ({
|
||||||
|
registerCommand: vi.fn()
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/utils/envUtil', () => ({
|
||||||
|
isElectron: () => false
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('useBottomPanelStore', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePinia(createPinia())
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should initialize with empty panels', () => {
|
||||||
|
const store = useBottomPanelStore()
|
||||||
|
|
||||||
|
expect(store.activePanel).toBeNull()
|
||||||
|
expect(store.bottomPanelVisible).toBe(false)
|
||||||
|
expect(store.bottomPanelTabs).toEqual([])
|
||||||
|
expect(store.activeBottomPanelTab).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should register bottom panel tabs', () => {
|
||||||
|
const store = useBottomPanelStore()
|
||||||
|
const tab: BottomPanelExtension = {
|
||||||
|
id: 'test-tab',
|
||||||
|
title: 'Test Tab',
|
||||||
|
component: {},
|
||||||
|
type: 'vue',
|
||||||
|
targetPanel: 'terminal'
|
||||||
|
}
|
||||||
|
|
||||||
|
store.registerBottomPanelTab(tab)
|
||||||
|
|
||||||
|
expect(store.panels.terminal.tabs.find((t) => t.id === 'test-tab')).toEqual(
|
||||||
|
tab
|
||||||
|
)
|
||||||
|
expect(store.panels.terminal.activeTabId).toBe('test-tab')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should toggle panel visibility', () => {
|
||||||
|
const store = useBottomPanelStore()
|
||||||
|
const tab: BottomPanelExtension = {
|
||||||
|
id: 'test-tab',
|
||||||
|
title: 'Test Tab',
|
||||||
|
component: {},
|
||||||
|
type: 'vue',
|
||||||
|
targetPanel: 'shortcuts'
|
||||||
|
}
|
||||||
|
|
||||||
|
store.registerBottomPanelTab(tab)
|
||||||
|
|
||||||
|
// Panel should be hidden initially
|
||||||
|
expect(store.activePanel).toBeNull()
|
||||||
|
|
||||||
|
// Toggle should show panel
|
||||||
|
store.togglePanel('shortcuts')
|
||||||
|
expect(store.activePanel).toBe('shortcuts')
|
||||||
|
expect(store.bottomPanelVisible).toBe(true)
|
||||||
|
|
||||||
|
// Toggle again should hide panel
|
||||||
|
store.togglePanel('shortcuts')
|
||||||
|
expect(store.activePanel).toBeNull()
|
||||||
|
expect(store.bottomPanelVisible).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should switch between panel types', () => {
|
||||||
|
const store = useBottomPanelStore()
|
||||||
|
|
||||||
|
const terminalTab: BottomPanelExtension = {
|
||||||
|
id: 'terminal-tab',
|
||||||
|
title: 'Terminal',
|
||||||
|
component: {},
|
||||||
|
type: 'vue',
|
||||||
|
targetPanel: 'terminal'
|
||||||
|
}
|
||||||
|
|
||||||
|
const shortcutsTab: BottomPanelExtension = {
|
||||||
|
id: 'shortcuts-tab',
|
||||||
|
title: 'Shortcuts',
|
||||||
|
component: {},
|
||||||
|
type: 'vue',
|
||||||
|
targetPanel: 'shortcuts'
|
||||||
|
}
|
||||||
|
|
||||||
|
store.registerBottomPanelTab(terminalTab)
|
||||||
|
store.registerBottomPanelTab(shortcutsTab)
|
||||||
|
|
||||||
|
// Show terminal panel
|
||||||
|
store.togglePanel('terminal')
|
||||||
|
expect(store.activePanel).toBe('terminal')
|
||||||
|
expect(store.activeBottomPanelTab?.id).toBe('terminal-tab')
|
||||||
|
|
||||||
|
// Switch to shortcuts panel
|
||||||
|
store.togglePanel('shortcuts')
|
||||||
|
expect(store.activePanel).toBe('shortcuts')
|
||||||
|
expect(store.activeBottomPanelTab?.id).toBe('shortcuts-tab')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should toggle specific tabs', () => {
|
||||||
|
const store = useBottomPanelStore()
|
||||||
|
const tab: BottomPanelExtension = {
|
||||||
|
id: 'specific-tab',
|
||||||
|
title: 'Specific Tab',
|
||||||
|
component: {},
|
||||||
|
type: 'vue',
|
||||||
|
targetPanel: 'shortcuts'
|
||||||
|
}
|
||||||
|
|
||||||
|
store.registerBottomPanelTab(tab)
|
||||||
|
|
||||||
|
// Toggle specific tab should show it
|
||||||
|
store.toggleBottomPanelTab('specific-tab')
|
||||||
|
expect(store.activePanel).toBe('shortcuts')
|
||||||
|
expect(store.panels.shortcuts.activeTabId).toBe('specific-tab')
|
||||||
|
|
||||||
|
// Toggle same tab again should hide panel
|
||||||
|
store.toggleBottomPanelTab('specific-tab')
|
||||||
|
expect(store.activePanel).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user