fix: Ignore URL actions for other card IDs (#2272)

- Closes #2190
This commit is contained in:
Dermot Duffy
2025-12-09 20:22:22 -08:00
committed by GitHub
parent dacde91f3a
commit c9c7e93e19
3 changed files with 108 additions and 10 deletions
+9 -9
View File
@@ -96,6 +96,7 @@ export class QueryStringManager {
protected _getActions(): AdvancedCameraCardCustomActionConfig[] {
const params = new URLSearchParams(window.location.search);
const actions: AdvancedCameraCardCustomActionConfig[] = [];
const configuredCardID = this._api.getConfigManager().getConfig()?.card_id;
const actionRE = new RegExp(
/^(advanced-camera-card|frigate-card)-action([.:](?<cardID>\w+))?[.:](?<action>\w+)/,
);
@@ -107,14 +108,17 @@ export class QueryStringManager {
const cardID: string | undefined = match.groups['cardID'];
const actionName = match.groups['action'];
// Skip actions targeted at other cards.
if (cardID && cardID !== configuredCardID) {
continue;
}
let action: AdvancedCameraCardCustomActionConfig | null = null;
switch (actionName) {
case 'camera_select':
case 'live_substream_select':
if (value) {
action = createCameraAction(actionName, value, {
cardID: cardID,
});
action = createCameraAction(actionName, value, { cardID });
}
break;
case 'camera_ui':
@@ -122,9 +126,7 @@ export class QueryStringManager {
case 'download':
case 'expand':
case 'menu_toggle':
action = createGeneralAction(actionName, {
cardID: cardID,
});
action = createGeneralAction(actionName, { cardID });
break;
case 'clip':
case 'clips':
@@ -136,9 +138,7 @@ export class QueryStringManager {
case 'snapshot':
case 'snapshots':
case 'timeline':
action = createViewAction(actionName, {
cardID: cardID,
});
action = createViewAction(actionName, { cardID });
break;
default:
console.warn(
+1
View File
@@ -254,6 +254,7 @@ export interface CardMicrophoneAPI {
export interface CardQueryStringAPI {
getActionsManager(): ActionsManager;
getCardElementManager(): CardElementManager;
getConfigManager(): ConfigManager;
getViewManager(): ViewManager;
}
@@ -1,8 +1,9 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { CardController } from '../../src/card-controller/controller';
import { QueryStringManager } from '../../src/card-controller/query-string-manager';
import { SubstreamSelectViewModifier } from '../../src/card-controller/view/modifiers/substream-select';
import { createCardAPI } from '../test-utils';
import { createCardAPI, createConfig } from '../test-utils';
const setQueryString = (qs: string): void => {
const location: Location = mock<Location>();
@@ -11,6 +12,14 @@ const setQueryString = (qs: string): void => {
vi.spyOn(window, 'location', 'get').mockReturnValue(location);
};
const setCardID = (api: CardController, cardID: string): void => {
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
card_id: cardID,
}),
);
};
// @vitest-environment jsdom
describe('QueryStringManager', () => {
afterEach(() => {
@@ -45,6 +54,7 @@ describe('QueryStringManager', () => {
])('%s', async (viewName: string) => {
setQueryString(`?advanced-camera-card-action.id.${viewName}=`);
const api = createCardAPI();
setCardID(api, 'id');
// View actions do not need the card to have been updated.
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(false);
@@ -71,6 +81,7 @@ describe('QueryStringManager', () => {
])('%s', async (action: string) => {
setQueryString(`?advanced-camera-card-action.id.${action}=`);
const api = createCardAPI();
setCardID(api, 'id');
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
const manager = new QueryStringManager(api);
@@ -92,6 +103,7 @@ describe('QueryStringManager', () => {
it('should execute view default action', async () => {
setQueryString('?advanced-camera-card-action.id.default=');
const api = createCardAPI();
setCardID(api, 'id');
// View actions do not need the card to have been updated.
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(false);
@@ -109,6 +121,7 @@ describe('QueryStringManager', () => {
it('should execute camera_select action', async () => {
setQueryString('?advanced-camera-card-action.id.camera_select=camera.office');
const api = createCardAPI();
setCardID(api, 'id');
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
const manager = new QueryStringManager(api);
@@ -130,6 +143,7 @@ describe('QueryStringManager', () => {
'?advanced-camera-card-action.id.live_substream_select=camera.office_hd',
);
const api = createCardAPI();
setCardID(api, 'id');
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
const manager = new QueryStringManager(api);
@@ -152,6 +166,7 @@ describe('QueryStringManager', () => {
async (action: string) => {
setQueryString(`?advanced-camera-card-action.id.${action}=`);
const api = createCardAPI();
setCardID(api, 'id');
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
const manager = new QueryStringManager(api);
@@ -170,6 +185,7 @@ describe('QueryStringManager', () => {
setQueryString('?advanced-camera-card-action.id.not_an_action=value');
const api = createCardAPI();
setCardID(api, 'id');
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
const manager = new QueryStringManager(api);
@@ -197,6 +213,7 @@ describe('QueryStringManager', () => {
])('%s', async (viewName: string) => {
setQueryString(`?advanced-camera-card-action.id.${viewName}=`);
const api = createCardAPI();
setCardID(api, 'id');
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
const manager = new QueryStringManager(api);
@@ -221,6 +238,7 @@ describe('QueryStringManager', () => {
'&advanced-camera-card-action.id.camera_select=camera.kitchen',
);
const api = createCardAPI();
setCardID(api, 'id');
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
const manager = new QueryStringManager(api);
@@ -241,6 +259,7 @@ describe('QueryStringManager', () => {
'&advanced-camera-card-action.id.camera_select=camera.office',
);
const api = createCardAPI();
setCardID(api, 'id');
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
const manager = new QueryStringManager(api);
@@ -259,6 +278,7 @@ describe('QueryStringManager', () => {
'?advanced-camera-card-action.id.live_substream_select=camera.office_hd',
);
const api = createCardAPI();
setCardID(api, 'id');
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
const manager = new QueryStringManager(api);
@@ -282,6 +302,7 @@ describe('QueryStringManager', () => {
it('should execute actions with old frigate-card-action key', async () => {
setQueryString(`?frigate-card-action.id.clips=`);
const api = createCardAPI();
setCardID(api, 'id');
// View actions do not need the card to have been updated.
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(false);
@@ -297,4 +318,80 @@ describe('QueryStringManager', () => {
},
});
});
describe('should filter by card_id', () => {
it('should execute view action when card_id matches', async () => {
setQueryString('?advanced-camera-card-action.my_card.clips=');
const api = createCardAPI();
setCardID(api, 'my_card');
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(false);
const manager = new QueryStringManager(api);
expect(manager.hasViewRelatedActionsToRun()).toBeTruthy();
await manager.executeIfNecessary();
expect(api.getViewManager().setViewByParametersWithNewQuery).toBeCalledWith({
params: {
view: 'clips',
},
});
});
it('should NOT execute view action when card_id does not match', async () => {
setQueryString('?advanced-camera-card-action.other_card.clips=');
const api = createCardAPI();
setCardID(api, 'my_card');
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(false);
const manager = new QueryStringManager(api);
expect(manager.hasViewRelatedActionsToRun()).toBeFalsy();
await manager.executeIfNecessary();
expect(api.getViewManager().setViewByParametersWithNewQuery).not.toBeCalled();
expect(api.getActionsManager().executeActions).not.toBeCalled();
});
it('should execute action without card_id on any card', async () => {
setQueryString('?advanced-camera-card-action.clips=');
const api = createCardAPI();
setCardID(api, 'my_card');
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(false);
const manager = new QueryStringManager(api);
expect(manager.hasViewRelatedActionsToRun()).toBeTruthy();
await manager.executeIfNecessary();
expect(api.getViewManager().setViewByParametersWithNewQuery).toBeCalledWith({
params: {
view: 'clips',
},
});
});
it('should NOT execute non-view action when card_id does not match', async () => {
setQueryString('?advanced-camera-card-action.other_card.menu_toggle=');
const api = createCardAPI();
setCardID(api, 'my_card');
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
const manager = new QueryStringManager(api);
expect(manager.hasViewRelatedActionsToRun()).toBeFalsy();
await manager.executeIfNecessary();
expect(api.getActionsManager().executeActions).not.toBeCalled();
});
it('should execute action when card has no card_id and URL has card_id', async () => {
setQueryString('?advanced-camera-card-action.some_card.clips=');
const api = createCardAPI();
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(false);
const manager = new QueryStringManager(api);
// Should NOT execute since URL targets 'some_card' but this card has no card_id
expect(manager.hasViewRelatedActionsToRun()).toBeFalsy();
await manager.executeIfNecessary();
expect(api.getViewManager().setViewByParametersWithNewQuery).not.toBeCalled();
});
});
});