Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions packages/hub-ui/src/client/state/context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,31 @@ describe('createDocksContext', () => {
expect(context.docks.selected?.id).toBe('nuxt:modules')
})

it('does not remember a grouped action as the group\'s last-opened member', async () => {
expect.assertions(1)

const { rpc, sharedStates, trust } = createStubRpc()
const session = ref<DockSessionStorage>({
open: false,
selectedDockId: null,
selectedDockRoute: null,
groupLastChildIds: {},
})
const context = await createDocksContext('embedded', rpc, undefined, session)

trust()
sharedStates.get('devframe:docks')!.push([
{ id: 'tools', type: 'group', title: 'Tools', icon: 'ph:wrench-duotone' },
{ id: 'tools:run', type: 'action', action: { importFrom: '/action.js' }, title: 'Run', icon: 'ph:play-duotone', groupId: 'tools' },
] satisfies DevframeDockEntry[])
sharedStates.get('devframe:dock-renderers')!.push({})
await flushRestore()

await context.docks.switchEntry('tools:run')

expect(session.value.groupLastChildIds).toEqual({})
})

it('keeps a dock closed when the user closes it before initialization finishes', async () => {
expect.assertions(2)

Expand Down
11 changes: 5 additions & 6 deletions packages/hub-ui/src/client/state/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,15 +243,14 @@ export async function createDocksContext(
await executeSetupScript(entry, scriptContext)
}

// Remember this selection for later redirects: a member tab (carries its
// anchor's `frameId`) as the frame's live tab, a grouped member as its
// group's last-opened child. Only iframes own an address-bar route, so clear
// a stale route for anything else. Guarded: a store predating these fields
// has no map yet.
// Remember selection redirects: a member tab as its frame's live tab, and a
// grouped non-action member as its group's last-opened child. One-shot actions
// leave the preferred panel unchanged. Only iframes own an address-bar route,
// so clear a stale route for anything else.
const rememberEntrySelection = (entry: DevframeDockEntry) => {
if (entry.type === 'iframe' && entry.frameId && !entry.subTabs)
frameNavCurrentMember.set(entry.frameId, entry.id)
if (entry.groupId)
if (entry.type !== 'action' && entry.groupId)
(sessionStore.value.groupLastChildIds ??= {})[entry.groupId] = entry.id
if (entry.type !== 'iframe')
sessionStore.value.selectedDockRoute = null
Expand Down
15 changes: 15 additions & 0 deletions packages/hub-ui/src/client/state/dock-settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ function group(id: string, extra: Partial<DevframeViewGroup> = {}): DevframeDock
return { id, type: 'group', title: id.toUpperCase(), icon: 'ph:folder-duotone', ...extra } as DevframeDockEntry
}

function action(id: string, extra: Partial<DevframeDockEntry> = {}): DevframeDockEntry {
return { id, type: 'action', action: { importFrom: '/action.js' }, title: id.toUpperCase(), icon: 'ph:play-duotone', ...extra } as DevframeDockEntry
}

function ids(groups: DevframeDockEntriesGrouped): string[] {
return groups.flatMap(([, items]) => items.map(item => item.id))
}
Expand Down Expand Up @@ -129,10 +133,21 @@ describe('resolveGroupPreferredChild', () => {
expect(resolveGroupPreferredChild([g, defaultMember, gated], g, 'g:gated', whenContext)).toBe(defaultMember)
})

it('falls back to defaultChildId when the remembered member is an action', () => {
const rememberedAction = action('g:run', { groupId: 'g' })
expect(resolveGroupPreferredChild([...entries, rememberedAction], g, 'g:run')).toBe(defaultMember)
})

it('resolves nothing for a popover-only group without memory', () => {
const bare = group('bare') as DevframeViewGroup
expect(resolveGroupPreferredChild([bare, iframe('bare:x', { groupId: 'bare' })], bare, undefined)).toBeUndefined()
})

it('ignores a remembered action for a popover-only group', () => {
const bare = group('bare') as DevframeViewGroup
const rememberedAction = action('bare:run', { groupId: 'bare' })
expect(resolveGroupPreferredChild([bare, rememberedAction], bare, 'bare:run')).toBeUndefined()
})
})

describe('resolveRecentDockEntry', () => {
Expand Down
8 changes: 5 additions & 3 deletions packages/hub-ui/src/client/state/dock-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,9 +222,10 @@ export function resolveGroupDefaultChild(
/**
* Resolve the member a group activation opens, layering the per-tab "last
* opened member" memory (`DockSessionStorage.groupLastChildIds`) over the
* author's `defaultChildId`. The remembered member wins while it still
* author's `defaultChildId`. A remembered non-action member wins while it still
* resolves (it exists in the group and its `when` clause holds), so reopening
* a group lands back on the member the developer last used; otherwise the
* a group lands back on the panel the developer last used. One-shot actions are
* skipped, including values persisted by an older client. Otherwise the
* `defaultChildId` target is tried under the same rules (both via
* {@link resolveGroupDefaultChild}, so the render-only `visibility` clause is
* ignored for either candidate). Returns `undefined` when neither resolves:
Expand All @@ -237,7 +238,8 @@ export function resolveGroupPreferredChild(
lastChildId: string | undefined,
whenContext?: WhenContext,
): DevframeDockEntry | undefined {
return resolveGroupDefaultChild(entries, group.id, lastChildId, whenContext)
const remembered = resolveGroupDefaultChild(entries, group.id, lastChildId, whenContext)
return (remembered?.type === 'action' ? undefined : remembered)
?? resolveGroupDefaultChild(entries, group.id, group.defaultChildId, whenContext)
}

Expand Down
Loading