From 6a6d65d5c52569fd6728415d6c36d3b59adfe728 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Thu, 2 Jul 2026 10:26:30 -0700 Subject: [PATCH] fix: Highlight folder icon for correct folder / media (#2560) - Closes: #2128 - Closes: #2129 --- src/components-lib/menu-button-controller.ts | 21 +++- src/view/unified-query.ts | 4 + .../menu-button-controller.test.ts | 112 ++++++++++++++++++ tests/view/unified-query.test.ts | 21 ++++ 4 files changed, 156 insertions(+), 2 deletions(-) diff --git a/src/components-lib/menu-button-controller.ts b/src/components-lib/menu-button-controller.ts index 568a709e..2fe8bb44 100644 --- a/src/components-lib/menu-button-controller.ts +++ b/src/components-lib/menu-button-controller.ts @@ -871,7 +871,7 @@ export class MenuButtonController { if (folders.length === 1) { const folderID = folders[0][0]; - const isSelected = !!view?.query?.getFolderQueries(folderID).length; + const isSelected = !!view?.query?.hasFolderQueries(folderID); const folder = folders[0][1]; return { @@ -886,7 +886,7 @@ export class MenuButtonController { } const submenuItems = folders.map(([id, folder]) => { - const isSelected = !!view?.query?.getFolderQueries(id).length; + const isSelected = !!view?.query?.hasFolderQueries(id); return { enabled: true, @@ -951,6 +951,23 @@ export class MenuButtonController { continue; } + // Unlike other views, a folder action targets a specific folder, so + // emphasize it only when that folder is the one being viewed. Matching + // on the view name alone would emphasize every folder button at once. + // An action without a folder ID keeps the plain view-name match. + if ( + action.advanced_camera_card_action === 'folder' || + action.advanced_camera_card_action === 'folders' + ) { + const emphasized = action.folder + ? !!options?.view?.query?.hasFolderQueries(action.folder) + : !!options?.view?.is(action.advanced_camera_card_action); + if (emphasized) { + return this._getEmphasizedStyle(); + } + continue; + } + if ( VIEWS_USER_SPECIFIED.some( (viewName) => diff --git a/src/view/unified-query.ts b/src/view/unified-query.ts index f673cb79..90b9a56c 100644 --- a/src/view/unified-query.ts +++ b/src/view/unified-query.ts @@ -58,6 +58,10 @@ export class UnifiedQuery { ); } + public hasFolderQueries(folderID?: string): boolean { + return this.getFolderQueries(folderID).length > 0; + } + public getNonMediaQueries(): QueryNode[] { return this._nodes.filter((node) => !this._isMediaQuery(node)); } diff --git a/tests/components-lib/menu-button-controller.test.ts b/tests/components-lib/menu-button-controller.test.ts index b03b493c..2bd6e9f5 100644 --- a/tests/components-lib/menu-button-controller.test.ts +++ b/tests/components-lib/menu-button-controller.test.ts @@ -2699,6 +2699,118 @@ describe('MenuButtonController', () => { }); }); + // `folder` (single folder) and `folders` (gallery) actions both carry a + // folder ID and are emphasized identically, so exercise both. + it.each([['folder' as const], ['folders' as const]])( + 'with a %s action, emphasizes only the button for the folder being viewed', + (action: 'folder' | 'folders') => { + const folder = createFolder({ id: 'folder-a' }); + const folderNode: FolderQuery = { + source: QuerySource.Folder, + folder: folder, + path: [{ ha: { id: 'one' } }], + }; + const view = createView({ + view: action, + query: new UnifiedQuery().addNode(folderNode), + }); + + const viewedButton: MenuItem = { + ...dynamicButton, + icon: 'mdi:folder-a', + tap_action: { + action: 'fire-dom-event', + advanced_camera_card_action: action, + folder: 'folder-a', + }, + }; + const otherButton: MenuItem = { + ...dynamicButton, + icon: 'mdi:folder-b', + tap_action: { + action: 'fire-dom-event', + advanced_camera_card_action: action, + folder: 'folder-b', + }, + }; + controller.addDynamicMenuButton(viewedButton); + controller.addDynamicMenuButton(otherButton); + + const buttons = calculateButtons(controller, { view: view }); + + expect(buttons).toContainEqual({ + ...viewedButton, + style: { color: 'var(--advanced-camera-card-menu-button-active-color)' }, + }); + expect(buttons).toContainEqual({ + ...otherButton, + style: {}, + }); + }, + ); + + it('with a folder action and no folder ID, emphasizes on the folder view', () => { + const button: MenuItem = { + ...dynamicButton, + tap_action: { action: 'fire-dom-event', advanced_camera_card_action: 'folder' }, + }; + const view = createView({ view: 'folder' }); + controller.addDynamicMenuButton(button); + + expect(calculateButtons(controller, { view: view })).toContainEqual({ + ...button, + style: { color: 'var(--advanced-camera-card-menu-button-active-color)' }, + }); + }); + + it('with a folder action, stays emphasized in the media viewer while its folder query remains', () => { + const folder = createFolder({ id: 'folder-a' }); + const folderNode: FolderQuery = { + source: QuerySource.Folder, + folder: folder, + path: [{ ha: { id: 'one' } }], + }; + // Opening a media item from a folder keeps the folder query but changes + // the view to the media viewer, so emphasis must follow the folder query + // rather than the view name. + const view = createView({ + view: 'media', + query: new UnifiedQuery().addNode(folderNode), + }); + + const viewedButton: MenuItem = { + ...dynamicButton, + icon: 'mdi:folder-a', + tap_action: { + action: 'fire-dom-event', + advanced_camera_card_action: 'folder', + folder: 'folder-a', + }, + }; + const otherButton: MenuItem = { + ...dynamicButton, + icon: 'mdi:folder-b', + tap_action: { + action: 'fire-dom-event', + advanced_camera_card_action: 'folder', + folder: 'folder-b', + }, + }; + controller.addDynamicMenuButton(viewedButton); + controller.addDynamicMenuButton(otherButton); + + const buttons = calculateButtons(controller, { view: view }); + + expect(buttons).toContainEqual({ + ...viewedButton, + style: { color: 'var(--advanced-camera-card-menu-button-active-color)' }, + }); + expect(buttons).toContainEqual({ + ...otherButton, + style: {}, + }); + }); + it('with array of actions', () => { const button: MenuItem = { ...dynamicButton, diff --git a/tests/view/unified-query.test.ts b/tests/view/unified-query.test.ts index e9a1e499..b66093cd 100644 --- a/tests/view/unified-query.test.ts +++ b/tests/view/unified-query.test.ts @@ -51,6 +51,27 @@ describe('UnifiedQuery', () => { expect(clipsQueries).toHaveLength(1); }); + it('should return true when a folder query matches the folder ID', () => { + const query = new UnifiedQuery(); + query.addNode(createFolderQuery('clips')); + + expect(query.hasFolderQueries('clips')).toBe(true); + }); + + it('should return false when no folder query matches the folder ID', () => { + const query = new UnifiedQuery(); + query.addNode(createFolderQuery('clips')); + + expect(query.hasFolderQueries('recordings')).toBe(false); + }); + + it('should return true when any folder query exists and no folder ID is given', () => { + const query = new UnifiedQuery(); + query.addNode(createFolderQuery('clips')); + + expect(query.hasFolderQueries()).toBe(true); + }); + it('should get all media types', () => { const query = new UnifiedQuery(); query.addNode(createEventQuery('front', { hasClip: true }));