Skip to content

Commit 300703e

Browse files
fix: expand collapsed macro groups when jumping to a macro
When a macro is selected via Jump to macro or navigation, open its ancestor sidebar groups so the target macro stays visible. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 6951895 commit 300703e

3 files changed

Lines changed: 88 additions & 5 deletions

File tree

packages/uhk-common/src/util/group-macros-by-name.test.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ import {
55
MACRO_GROUPING_MAX_DEPTH,
66
normalizeMacroGroupingSettings,
77
} from '../models/macro-grouping-settings.js';
8-
import { groupMacrosByName, GroupableMacroItem, splitMacroName } from './group-macros-by-name.js';
8+
import {
9+
findMacroGroupAncestorPaths,
10+
groupMacrosByName,
11+
GroupableMacroItem,
12+
splitMacroName
13+
} from './group-macros-by-name.js';
914

1015
function createMacro(id: number, name: string): GroupableMacroItem {
1116
return {
@@ -216,6 +221,38 @@ describe('groupMacrosByName', () => {
216221
});
217222
});
218223

224+
describe('findMacroGroupAncestorPaths', () => {
225+
it('returns ancestor group paths for a nested macro', ({ assert }) => {
226+
const macros = [
227+
createMacro(1, 'Open: daily sites'),
228+
createMacro(2, 'Open: weekly sites'),
229+
];
230+
const tree = groupMacrosByName(macros, {
231+
...ENABLED_MACRO_GROUPING_SETTINGS,
232+
maxDepth: 2,
233+
});
234+
235+
assert.deepEqual(findMacroGroupAncestorPaths(tree, 1), ['Open']);
236+
assert.deepEqual(findMacroGroupAncestorPaths(tree, 2), ['Open']);
237+
});
238+
239+
it('returns an empty array for macros that are not grouped', ({ assert }) => {
240+
const macros = [
241+
createMacro(1, 'Doom'),
242+
createMacro(2, 'Chainsaw'),
243+
];
244+
const tree = groupMacrosByName(macros, ENABLED_MACRO_GROUPING_SETTINGS);
245+
246+
assert.deepEqual(findMacroGroupAncestorPaths(tree, 1), []);
247+
});
248+
249+
it('returns null when the macro is not in the tree', ({ assert }) => {
250+
const tree = groupMacrosByName([createMacro(1, 'Doom: Chainsaw')], ENABLED_MACRO_GROUPING_SETTINGS);
251+
252+
assert.equal(findMacroGroupAncestorPaths(tree, 99), null);
253+
});
254+
});
255+
219256
describe('normalizeMacroGroupingSettings', () => {
220257
it('clamps maxDepth to the supported sidebar layout limit', ({ assert }) => {
221258
const result = normalizeMacroGroupingSettings({ maxDepth: 99 });

packages/uhk-common/src/util/group-macros-by-name.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,32 @@ function getMacroMenuTreeNodeLabel<TMacro extends GroupableMacroItem>(node: Macr
135135
return node.macro?.name || '';
136136
}
137137

138+
export function findMacroGroupAncestorPaths<TMacro extends GroupableMacroItem>(
139+
nodes: MacroMenuTreeNode<TMacro>[],
140+
macroId: number,
141+
ancestorPaths: string[] = []
142+
): string[] | null {
143+
for (const node of nodes) {
144+
if (node.type === 'macro' && node.macro?.id === macroId) {
145+
return ancestorPaths;
146+
}
147+
148+
if (node.type === 'group' && node.path && node.children) {
149+
const found = findMacroGroupAncestorPaths(
150+
node.children,
151+
macroId,
152+
[...ancestorPaths, node.path]
153+
);
154+
155+
if (found !== null) {
156+
return found;
157+
}
158+
}
159+
}
160+
161+
return null;
162+
}
163+
138164
export function splitMacroName(name: string, camelCaseSeparation: boolean): string[] {
139165
let parts = name.split(MACRO_NAME_SEPARATOR).filter(part => part.length > 0);
140166

packages/uhk-web/src/app/components/side-menu/side-menu.component.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,11 @@ import {
3030

3131
import { Store } from '@ngrx/store';
3232

33-
import { Subscription } from 'rxjs';
34-
import { MAX_ALLOWED_MACROS_TOOLTIP, UHK_80_DEVICE } from 'uhk-common';
33+
import { combineLatest, Subscription } from 'rxjs';
34+
import { distinctUntilChanged, map } from 'rxjs/operators';
35+
import { findMacroGroupAncestorPaths, MAX_ALLOWED_MACROS_TOOLTIP, UHK_80_DEVICE } from 'uhk-common';
3536

36-
import { AppState, getSideMenuPageState } from '../../store';
37+
import { AppState, getSelectedMacro, getSideMenuPageState } from '../../store';
3738
import { AddMacroAction } from '../../store/actions/macro';
3839
import { RenameUserConfigurationAction } from '../../store/actions/user-config';
3940
import { DeviceUiStates, MacroMenuTreeNode, SideMenuPageState } from '../../models';
@@ -120,11 +121,20 @@ export class SideMenuComponent implements OnChanges, OnInit, OnDestroy {
120121
}
121122

122123
ngOnInit(): void {
123-
this.stateSubscription = this.store.select(getSideMenuPageState).subscribe(data => {
124+
this.stateSubscription = combineLatest([
125+
this.store.select(getSideMenuPageState),
126+
this.store.select(getSelectedMacro).pipe(
127+
map(macro => macro?.id),
128+
distinctUntilChanged()
129+
)
130+
]).subscribe(([data, selectedMacroId]) => {
124131
this.state = data;
125132
this.isBatterySettingsMenuAllowed = this.state.connectedDevice?.id === UHK_80_DEVICE.id;
126133
this.isConnectionsMenuAllowed = this.state.connectedDevice?.id === UHK_80_DEVICE.id;
127134
this.calculateDeviceAnimationState();
135+
if (selectedMacroId !== undefined) {
136+
this.expandMacroGroupsForMacro(selectedMacroId, data.macroTree);
137+
}
128138
this.syncMacroGroupState();
129139
this.cdRef.markForCheck();
130140
});
@@ -205,6 +215,16 @@ export class SideMenuComponent implements OnChanges, OnInit, OnDestroy {
205215
: 'inactive';
206216
}
207217

218+
private expandMacroGroupsForMacro(macroId: number, macroTree: MacroMenuTreeNode[]): void {
219+
for (const path of findMacroGroupAncestorPaths(macroTree, macroId) ?? []) {
220+
this.macroGroupState[path] = { icon: faChevronUp, animation: 'active' };
221+
}
222+
223+
if (this.sideMenuState.macro.animation !== 'active') {
224+
this.sideMenuState.macro = { icon: faChevronUp, animation: 'active' };
225+
}
226+
}
227+
208228
private syncMacroGroupState(): void {
209229
const nextState: Record<string, SideMenuItemState> = {};
210230

0 commit comments

Comments
 (0)