Fix view-related querystring action race condition
This commit is contained in:
+57
-24
@@ -48,12 +48,12 @@ import {
|
||||
CameraConfig,
|
||||
CardWideConfig,
|
||||
ExtendedHomeAssistant,
|
||||
FRIGATE_CARD_VIEW_DEFAULT,
|
||||
FrigateCardConfig,
|
||||
frigateCardConfigSchema,
|
||||
FrigateCardCustomAction,
|
||||
FrigateCardError,
|
||||
FrigateCardView,
|
||||
FRIGATE_CARD_VIEW_DEFAULT,
|
||||
MediaLoadedInfo,
|
||||
MenuButton,
|
||||
Message,
|
||||
@@ -65,6 +65,7 @@ import {
|
||||
frigateCardHandleActionConfig,
|
||||
frigateCardHasAction,
|
||||
getActionConfigGivenAction,
|
||||
isViewAction,
|
||||
} from './utils/action.js';
|
||||
import { errorToConsole } from './utils/basic.js';
|
||||
import { log } from './utils/debug.js';
|
||||
@@ -440,8 +441,17 @@ class FrigateCard extends LitElement {
|
||||
return this._overriddenConfig || this._config;
|
||||
}
|
||||
|
||||
protected _changeView(args?: { view?: View; resetMessage?: boolean }): void {
|
||||
log(this._cardWideConfig, `Frigate Card view change: `, args?.view ?? '[default]');
|
||||
protected _changeView(args?: {
|
||||
view?: View;
|
||||
viewName?: FrigateCardView;
|
||||
cameraID?: string;
|
||||
resetMessage?: boolean;
|
||||
}): void {
|
||||
log(
|
||||
this._cardWideConfig,
|
||||
`Frigate Card view change: `,
|
||||
args?.view ?? args?.viewName ?? '[default]',
|
||||
);
|
||||
const changeView = (view: View): void => {
|
||||
if (View.isMajorMediaChange(this._view, view)) {
|
||||
this._mediaLoadedInfoController.clear();
|
||||
@@ -464,12 +474,13 @@ class FrigateCard extends LitElement {
|
||||
}
|
||||
|
||||
if (!args?.view) {
|
||||
// Load the default view.
|
||||
let cameraID: string | null = null;
|
||||
if (this._cameraManager) {
|
||||
const cameras = this._cameraManager.getStore().getVisibleCameras();
|
||||
if (cameras) {
|
||||
if (this._view?.camera && this._getConfig().view.update_cycle_camera) {
|
||||
if (args?.cameraID && cameras.has(args.cameraID)) {
|
||||
cameraID = args.cameraID;
|
||||
} else if (this._view?.camera && this._getConfig().view.update_cycle_camera) {
|
||||
const keys = Array.from(cameras.keys());
|
||||
const currentIndex = keys.indexOf(this._view.camera);
|
||||
const targetIndex = currentIndex + 1 >= keys.length ? 0 : currentIndex + 1;
|
||||
@@ -484,7 +495,7 @@ class FrigateCard extends LitElement {
|
||||
if (cameraID) {
|
||||
changeView(
|
||||
new View({
|
||||
view: this._getConfig().view.default,
|
||||
view: args?.viewName ?? this._getConfig().view.default,
|
||||
camera: cameraID,
|
||||
}),
|
||||
);
|
||||
@@ -719,11 +730,26 @@ class FrigateCard extends LitElement {
|
||||
this._handleThrownError(e);
|
||||
}
|
||||
|
||||
// If there's no view set yet, set one. This will be the case on initial camera load.
|
||||
// Set a view on initial load. However,if the query string contains an
|
||||
// action that needs to render content (e.g. a view action or diagnostics),
|
||||
// we don't set any view here and allow that content to be triggered by the
|
||||
// firstUpdated() call. To do otherwise may cause a race condition between
|
||||
// the default view and the querystring view, see:
|
||||
// https://github.com/dermotduffy/frigate-hass-card/issues/1200
|
||||
if (!this._view) {
|
||||
// Don't reset the message which may be set to an error above. This sets the
|
||||
// first view using the newly loaded cameras.
|
||||
this._changeView({ resetMessage: false });
|
||||
const querystringActions = getActionsFromQueryString();
|
||||
if (
|
||||
!querystringActions.find(
|
||||
(action) =>
|
||||
isViewAction(action) || action.frigate_card_action === 'diagnostics',
|
||||
)
|
||||
) {
|
||||
this._changeView({
|
||||
// Don't reset the message which may be set to an error above. This sets the
|
||||
// first view using the newly loaded cameras.
|
||||
resetMessage: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1024,7 +1050,10 @@ class FrigateCard extends LitElement {
|
||||
}
|
||||
|
||||
protected _cardActionHandler(frigateCardAction: FrigateCardCustomAction): void {
|
||||
if (!this._view || !this._cameraManager) {
|
||||
// Note: This function needs to process (view-related) commands even when
|
||||
// _view has not yet been initialized (since it may be used to set a view
|
||||
// via the querystring).
|
||||
if (!this._cameraManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1052,10 +1081,8 @@ class FrigateCard extends LitElement {
|
||||
case 'snapshots':
|
||||
case 'timeline':
|
||||
this._changeView({
|
||||
view: new View({
|
||||
view: action,
|
||||
camera: this._view.camera,
|
||||
}),
|
||||
viewName: action,
|
||||
cameraID: this._view?.camera,
|
||||
});
|
||||
break;
|
||||
case 'download':
|
||||
@@ -1097,21 +1124,27 @@ class FrigateCard extends LitElement {
|
||||
}
|
||||
break;
|
||||
case 'live_substream_select': {
|
||||
const view = createViewWithSelectedSubstream(
|
||||
this._view,
|
||||
frigateCardAction.camera,
|
||||
);
|
||||
view && this._changeView({ view: view });
|
||||
if (this._view) {
|
||||
const view = createViewWithSelectedSubstream(
|
||||
this._view,
|
||||
frigateCardAction.camera,
|
||||
);
|
||||
view && this._changeView({ view: view });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'live_substream_off': {
|
||||
const view = createViewWithoutSubstream(this._view);
|
||||
view && this._changeView({ view: view });
|
||||
if (this._view) {
|
||||
const view = createViewWithoutSubstream(this._view);
|
||||
view && this._changeView({ view: view });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'live_substream_on': {
|
||||
const view = createViewWithNextStream(this._cameraManager, this._view);
|
||||
view && this._changeView({ view: view });
|
||||
if (this._view) {
|
||||
const view = createViewWithNextStream(this._cameraManager, this._view);
|
||||
view && this._changeView({ view: view });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'media_player':
|
||||
|
||||
+7
-9
@@ -209,15 +209,11 @@ const frigateCardCustomActionsBaseSchema = customActionSchema.extend({
|
||||
|
||||
const FRIGATE_CARD_GENERAL_ACTIONS = [
|
||||
'camera_ui',
|
||||
'clip',
|
||||
'clips',
|
||||
'default',
|
||||
'diagnostics',
|
||||
'expand',
|
||||
'download',
|
||||
'fullscreen',
|
||||
'image',
|
||||
'live',
|
||||
'menu_toggle',
|
||||
'mute',
|
||||
'live_substream_on',
|
||||
@@ -226,15 +222,11 @@ const FRIGATE_CARD_GENERAL_ACTIONS = [
|
||||
'microphone_unmute',
|
||||
'play',
|
||||
'pause',
|
||||
'recording',
|
||||
'recordings',
|
||||
'screenshot',
|
||||
'snapshot',
|
||||
'snapshots',
|
||||
'timeline',
|
||||
'unmute',
|
||||
] as const;
|
||||
const FRIGATE_CARD_ACTIONS = [
|
||||
...FRIGATE_CARD_VIEWS_USER_SPECIFIED,
|
||||
...FRIGATE_CARD_GENERAL_ACTIONS,
|
||||
'camera_select',
|
||||
'live_substream_select',
|
||||
@@ -242,6 +234,11 @@ const FRIGATE_CARD_ACTIONS = [
|
||||
] as const;
|
||||
export type FrigateCardAction = (typeof FRIGATE_CARD_ACTIONS)[number];
|
||||
|
||||
const frigateCardViewActionSchema = frigateCardCustomActionsBaseSchema.extend({
|
||||
frigate_card_action: z.enum(FRIGATE_CARD_VIEWS_USER_SPECIFIED),
|
||||
});
|
||||
export type FrigateCardViewAction = z.infer<typeof frigateCardViewActionSchema>;
|
||||
|
||||
const frigateCardGeneralActionSchema = frigateCardCustomActionsBaseSchema.extend({
|
||||
frigate_card_action: z.enum(FRIGATE_CARD_GENERAL_ACTIONS),
|
||||
});
|
||||
@@ -261,6 +258,7 @@ const frigateCardMediaPlayerActionSchema = frigateCardCustomActionsBaseSchema.ex
|
||||
});
|
||||
|
||||
export const frigateCardCustomActionSchema = z.union([
|
||||
frigateCardViewActionSchema,
|
||||
frigateCardGeneralActionSchema,
|
||||
frigateCardCameraSelectActionSchema,
|
||||
frigateCardLiveDependencySelectActionSchema,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
FrigateCardAction,
|
||||
FrigateCardCustomAction,
|
||||
frigateCardCustomActionSchema,
|
||||
FrigateCardViewAction,
|
||||
} from '../types.js';
|
||||
|
||||
/**
|
||||
@@ -173,3 +174,21 @@ export const frigateCardHasAction = (config?: ActionType | ActionType[]): boolea
|
||||
export const stopEventFromActivatingCardWideActions = (ev: Event): void => {
|
||||
ev.stopPropagation();
|
||||
};
|
||||
|
||||
export const isViewAction = (
|
||||
action: FrigateCardCustomAction,
|
||||
): action is FrigateCardViewAction => {
|
||||
switch (action.frigate_card_action) {
|
||||
case 'clip':
|
||||
case 'clips':
|
||||
case 'image':
|
||||
case 'live':
|
||||
case 'recording':
|
||||
case 'recordings':
|
||||
case 'snapshot':
|
||||
case 'snapshots':
|
||||
case 'timeline':
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import { handleActionConfig, hasAction } from 'custom-card-helpers';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { actionSchema } from '../../src/types';
|
||||
import {
|
||||
convertActionToFrigateCardCustomAction,
|
||||
createFrigateCardCustomAction,
|
||||
frigateCardHandleActionConfig,
|
||||
frigateCardHasAction,
|
||||
getActionConfigGivenAction,
|
||||
stopEventFromActivatingCardWideActions,
|
||||
actionSchema,
|
||||
FrigateCardAction,
|
||||
FrigateCardCustomAction,
|
||||
frigateCardCustomActionSchema,
|
||||
} from '../../src/types';
|
||||
import {
|
||||
convertActionToFrigateCardCustomAction,
|
||||
createFrigateCardCustomAction,
|
||||
frigateCardHandleAction,
|
||||
frigateCardHandleActionConfig,
|
||||
frigateCardHasAction,
|
||||
getActionConfigGivenAction,
|
||||
isViewAction,
|
||||
stopEventFromActivatingCardWideActions,
|
||||
} from '../../src/utils/action';
|
||||
import { createHASS } from '../test-utils';
|
||||
|
||||
@@ -173,6 +180,23 @@ describe('frigateCardHandleActionConfig', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('frigateCardHandleAction', () => {
|
||||
const element = document.createElement('div');
|
||||
const action = actionSchema.parse({
|
||||
action: 'none',
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should call action handler', () => {
|
||||
frigateCardHandleAction(element, createHASS(), {}, action);
|
||||
expect(handleActionConfig).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('frigateCardHasAction', () => {
|
||||
const action = actionSchema.parse({
|
||||
action: 'toggle',
|
||||
@@ -200,3 +224,45 @@ describe('stopEventFromActivatingCardWideActions', () => {
|
||||
expect(event.stopPropagation).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isViewAction', () => {
|
||||
const createAction = (action: FrigateCardAction): FrigateCardCustomAction => {
|
||||
return frigateCardCustomActionSchema.parse({
|
||||
action: 'fire-dom-event' as const,
|
||||
frigate_card_action: action,
|
||||
});
|
||||
};
|
||||
it('should return true for clip view ', () => {
|
||||
expect(isViewAction(createAction('clip'))).toBeTruthy();
|
||||
});
|
||||
it('should return true for clips view ', () => {
|
||||
expect(isViewAction(createAction('clips'))).toBeTruthy();
|
||||
});
|
||||
it('should return true for image view ', () => {
|
||||
expect(isViewAction(createAction('image'))).toBeTruthy();
|
||||
});
|
||||
it('should return true for live view ', () => {
|
||||
expect(isViewAction(createAction('live'))).toBeTruthy();
|
||||
});
|
||||
it('should return true for recording view ', () => {
|
||||
expect(isViewAction(createAction('recording'))).toBeTruthy();
|
||||
});
|
||||
it('should return true for live view ', () => {
|
||||
expect(isViewAction(createAction('live'))).toBeTruthy();
|
||||
});
|
||||
it('should return true for recordings view ', () => {
|
||||
expect(isViewAction(createAction('recordings'))).toBeTruthy();
|
||||
});
|
||||
it('should return true for snapshot view ', () => {
|
||||
expect(isViewAction(createAction('snapshot'))).toBeTruthy();
|
||||
});
|
||||
it('should return true for snapshots view ', () => {
|
||||
expect(isViewAction(createAction('snapshots'))).toBeTruthy();
|
||||
});
|
||||
it('should return true for timeline view ', () => {
|
||||
expect(isViewAction(createAction('timeline'))).toBeTruthy();
|
||||
});
|
||||
it('should return false for anything else', () => {
|
||||
expect(isViewAction(createAction('diagnostics'))).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user