Skip to content

Commit ff579d9

Browse files
authored
fix(core): hide dock mode commands in popup mode (#502)
1 parent 0d8ee87 commit ff579d9

5 files changed

Lines changed: 132 additions & 14 deletions

File tree

packages/core/src/client/webcomponents/components/views-builtin/SettingsShortcuts.vue

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { DevToolsCommandEntry, DevToolsCommandKeybinding } from '@vitejs/de
33
import type { DocksContext } from '@vitejs/devtools-kit/client'
44
import { computed, nextTick, ref, watch } from 'vue'
55
import { sharedStateToRef } from '../../state/docks'
6-
import { formatKeybinding, isKeybindingOverrideDifferentFromDefault, isMac, KNOWN_BROWSER_SHORTCUTS } from '../../state/keybindings'
6+
import { filterCommandsByWhen, formatKeybinding, isKeybindingOverrideDifferentFromDefault, isMac, KNOWN_BROWSER_SHORTCUTS } from '../../state/keybindings'
77
import KeybindingBadge from '../command-palette/KeybindingBadge.vue'
88
import DockIcon from '../dock/DockIcon.vue'
99
@@ -22,9 +22,14 @@ interface ShortcutRow {
2222
indent: boolean
2323
}
2424
25+
// Only offer to bind commands that are actually reachable right now — binding a
26+
// key to something the current context rules out (e.g. the dock-mode commands
27+
// while the dock is detached into a popup) would silently do nothing.
28+
const availableCommands = computed(() => filterCommandsByWhen(commandsCtx.commands, props.context.when.context))
29+
2530
const shortcutRows = computed<ShortcutRow[]>(() => {
2631
const rows: ShortcutRow[] = []
27-
for (const cmd of commandsCtx.commands) {
32+
for (const cmd of availableCommands.value) {
2833
rows.push({ command: cmd, indent: false })
2934
if (cmd.children) {
3035
for (const child of cmd.children) {

packages/core/src/client/webcomponents/state/__tests__/keybindings.test.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { DevToolsCommandEntry, DevToolsCommandKeybinding } from '@vitejs/devtools-kit'
2+
import type { WhenContext } from '../keybindings'
23
import { describe, expect, it } from 'vitest'
3-
import { areKeybindingsEqual, collectAllKeybindings, formatKeybinding, isKeybindingOverrideDifferentFromDefault, KNOWN_BROWSER_SHORTCUTS, normalizeKeyEvent } from '../keybindings'
4+
import { areKeybindingsEqual, collectAllKeybindings, filterCommandsByWhen, formatKeybinding, isKeybindingOverrideDifferentFromDefault, KNOWN_BROWSER_SHORTCUTS, normalizeKeyEvent } from '../keybindings'
45

56
describe('formatKeybinding', () => {
67
it('splits key string into parts', () => {
@@ -105,6 +106,78 @@ describe('collectAllKeybindings', () => {
105106
})
106107
})
107108

109+
describe('filterCommandsByWhen', () => {
110+
function makeContext(overrides: Partial<WhenContext> = {}): WhenContext {
111+
return {
112+
clientType: 'embedded',
113+
dockOpen: false,
114+
paletteOpen: false,
115+
dockSelectedId: '',
116+
popupOpen: false,
117+
...overrides,
118+
}
119+
}
120+
121+
function makeDockMode(): DevToolsCommandEntry[] {
122+
return [
123+
{
124+
id: 'devtools:dock-mode',
125+
source: 'client' as const,
126+
title: 'Dock Mode',
127+
when: 'clientType == embedded && !popupOpen',
128+
children: [
129+
{ id: 'devtools:dock-mode:float', source: 'client' as const, title: 'Float Mode', when: '!popupOpen' },
130+
{ id: 'devtools:dock-mode:edge', source: 'client' as const, title: 'Edge Mode', when: '!popupOpen' },
131+
],
132+
},
133+
] as DevToolsCommandEntry[]
134+
}
135+
136+
it('passes through commands without a when clause', () => {
137+
const commands = [
138+
{ id: 'cmd1', source: 'client' as const, title: 'Cmd 1' },
139+
{ id: 'cmd2', source: 'client' as const, title: 'Cmd 2' },
140+
] as DevToolsCommandEntry[]
141+
142+
expect(filterCommandsByWhen(commands, makeContext())).toEqual(commands)
143+
})
144+
145+
it('drops a parent whose when clause fails, children included', () => {
146+
const result = filterCommandsByWhen(makeDockMode(), makeContext({ popupOpen: true }))
147+
expect(result).toHaveLength(0)
148+
})
149+
150+
it('keeps a passing parent but removes children whose own when clause fails', () => {
151+
const commands = [
152+
{
153+
id: 'parent',
154+
source: 'client' as const,
155+
title: 'Parent',
156+
children: [
157+
{ id: 'parent:always', source: 'client' as const, title: 'Always' },
158+
{ id: 'parent:embedded', source: 'client' as const, title: 'Embedded only', when: 'clientType == embedded' },
159+
],
160+
},
161+
] as DevToolsCommandEntry[]
162+
163+
const result = filterCommandsByWhen(commands, makeContext({ clientType: 'standalone' }))
164+
expect(result).toHaveLength(1)
165+
expect(result[0]!.children?.map(c => c.id)).toEqual(['parent:always'])
166+
})
167+
168+
it('keeps everything when the context satisfies every clause', () => {
169+
const result = filterCommandsByWhen(makeDockMode(), makeContext())
170+
expect(result).toHaveLength(1)
171+
expect(result[0]!.children).toHaveLength(2)
172+
})
173+
174+
it('does not mutate the input commands', () => {
175+
const commands = makeDockMode()
176+
filterCommandsByWhen(commands, makeContext({ popupOpen: true }))
177+
expect(commands[0]!.children).toHaveLength(2)
178+
})
179+
})
180+
108181
describe('areKeybindingsEqual', () => {
109182
it('treats undefined and empty arrays as equal', () => {
110183
expect(areKeybindingsEqual(undefined, [])).toBe(true)

packages/core/src/client/webcomponents/state/commands.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import type { ShallowRef } from 'vue'
66
import { evaluateWhen } from 'devframe/utils/when'
77
import { computed, markRaw, reactive, ref, watch } from 'vue'
88
import { sharedStateToRef } from './docks'
9-
import { collectAllKeybindings, normalizeKeyEvent } from './keybindings'
10-
import { useDockPopupWindow } from './popup'
9+
import { collectAllKeybindings, filterCommandsByWhen, normalizeKeyEvent } from './keybindings'
10+
import { useDockPopupWindow, useIsDockPopupOpen } from './popup'
1111

1212
export { formatKeybinding, isMac, normalizeKeyEvent } from './keybindings'
1313

@@ -35,6 +35,7 @@ export async function createCommandsContext(
3535
const shortcutOverrides = computed(() => settings.value.commandShortcuts ?? {})
3636

3737
const paletteOpen = ref(false)
38+
const isDockPopupOpen = useIsDockPopupOpen()
3839

3940
const getWhenContext = (): WhenContext => {
4041
if (whenContextProvider)
@@ -44,6 +45,7 @@ export async function createCommandsContext(
4445
dockOpen: false,
4546
paletteOpen: paletteOpen.value,
4647
dockSelectedId: '',
48+
popupOpen: isDockPopupOpen.value,
4749
}
4850
}
4951

@@ -55,13 +57,8 @@ export async function createCommandsContext(
5557

5658
const paletteCommands = computed<DevToolsCommandEntry[]>(() => {
5759
const ctx = getWhenContext()
58-
return commands.value.filter((cmd) => {
59-
if (cmd.showInPalette === false)
60-
return false
61-
if (cmd.when && !evaluateWhen(cmd.when, ctx))
62-
return false
63-
return true
64-
})
60+
const available = filterCommandsByWhen(commands.value, ctx)
61+
return available.filter(cmd => cmd.showInPalette !== false)
6562
})
6663

6764
function register(cmd: DevToolsClientCommand | DevToolsClientCommand[]): () => void {

packages/core/src/client/webcomponents/state/context.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { createCommandsContext } from './commands'
1313
import { docksGroupByCategories, getCategoryLabel, getGroupMembers, getGroupMembersGrouped, getRegisteredGroupIds, resolveCommandIcon, resolveGroupDefaultChild } from './dock-settings'
1414
import { createDockEntryState, DEFAULT_DOCK_PANEL_STORE, sharedStateToRef, useDocksEntries } from './docks'
1515
import { createClientMessagesClient } from './messages-client'
16-
import { registerMainFrameDockActionHandler, triggerMainFrameDockAction } from './popup'
16+
import { registerMainFrameDockActionHandler, triggerMainFrameDockAction, useIsDockPopupOpen } from './popup'
1717
import { createDockRenderers } from './renderers'
1818
import { executeSetupScript } from './setup-script'
1919

@@ -131,11 +131,13 @@ export async function createDocksContext(
131131

132132
// Shared when-context provider — used by both commands and docks
133133
let commandsContext: CommandsContext
134+
const isDockPopupOpen = useIsDockPopupOpen()
134135
const getWhenContext = (): WhenContext => ({
135136
clientType,
136137
dockOpen: panelStore.value.open,
137138
paletteOpen: commandsContext?.paletteOpen ?? false,
138139
dockSelectedId: selectedId.value ?? '',
140+
popupOpen: isDockPopupOpen.value,
139141
})
140142

141143
// Tracks the shared frame's current member tab, keyed by `frameId`. A
@@ -387,13 +389,19 @@ export async function createDocksContext(
387389
source: 'client',
388390
title: 'Dock Mode',
389391
icon: 'ph:layout-duotone',
390-
when: clientType === 'embedded' ? 'clientType == embedded' : undefined,
392+
// While the popup is open the embedded shell is unmounted and the popup
393+
// renders the standalone layout, so neither mode is observable — mirrors
394+
// the Appearance settings hiding its own dock-mode control.
395+
when: clientType === 'embedded' ? 'clientType == embedded && !popupOpen' : undefined,
391396
children: [
392397
{
393398
id: 'devtools:dock-mode:float',
394399
source: 'client',
395400
title: 'Float Mode',
396401
icon: 'ph:cards-three-duotone',
402+
// Repeated per child: shortcut dispatch reads the matched command's
403+
// own `when` and does not inherit the parent's.
404+
when: '!popupOpen',
397405
action: () => {
398406
panelStore.value.mode = 'float'
399407
},
@@ -403,6 +411,7 @@ export async function createDocksContext(
403411
source: 'client',
404412
title: 'Edge Mode',
405413
icon: 'ph:square-half-bottom-duotone',
414+
when: '!popupOpen',
406415
action: () => {
407416
panelStore.value.mode = 'edge'
408417
},

packages/core/src/client/webcomponents/state/keybindings.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import type { DevToolsCommandEntry, DevToolsCommandKeybinding } from '@vitejs/devtools-kit'
2+
import type { WhenContext } from 'devframe/utils/when'
3+
import { evaluateWhen } from 'devframe/utils/when'
24

35
export type { WhenContext } from 'devframe/utils/when'
46
export { evaluateWhen, resolveContextValue } from 'devframe/utils/when'
@@ -57,6 +59,38 @@ export function isKeybindingOverrideDifferentFromDefault(
5759
return override !== undefined && !areKeybindingsEqual(override, defaults)
5860
}
5961

62+
/**
63+
* Drop the commands whose `when` clause does not hold in the current context,
64+
* children included — `when` is documented to control palette visibility, but
65+
* nothing evaluated it for nested entries.
66+
*
67+
* A parent that survives is shallow-cloned so its `children` can be narrowed
68+
* without mutating the registry. Callers therefore get fresh parent objects on
69+
* every call: match entries by `id`, never by reference.
70+
*/
71+
export function filterCommandsByWhen(
72+
commands: DevToolsCommandEntry[],
73+
ctx: WhenContext,
74+
): DevToolsCommandEntry[] {
75+
const isAvailable = (cmd: { when?: string }) => !cmd.when || evaluateWhen(cmd.when, ctx)
76+
77+
const result: DevToolsCommandEntry[] = []
78+
for (const cmd of commands) {
79+
if (!isAvailable(cmd))
80+
continue
81+
if (!cmd.children) {
82+
result.push(cmd)
83+
continue
84+
}
85+
// `children` is typed `Server[] | Client[]` rather than `(Server | Client)[]`,
86+
// so filtering it in place widens the element type — same cast the other
87+
// child-walking call sites use.
88+
const children = (cmd.children as DevToolsCommandEntry[]).filter(isAvailable)
89+
result.push({ ...cmd, children } as DevToolsCommandEntry)
90+
}
91+
return result
92+
}
93+
6094
export function collectAllKeybindings(
6195
commands: { value: DevToolsCommandEntry[] },
6296
getKeybindings: (id: string) => DevToolsCommandKeybinding[],

0 commit comments

Comments
 (0)