Support confirmation for actions.

This commit is contained in:
Dermot Duffy
2022-04-30 08:52:10 -07:00
parent e6f6f9b1a4
commit cfd390ede3
2 changed files with 77 additions and 27 deletions
+15 -5
View File
@@ -5,6 +5,7 @@ import {
handleActionConfig, handleActionConfig,
hasAction, hasAction,
stateIcon, stateIcon,
ActionConfig,
} from 'custom-card-helpers'; } from 'custom-card-helpers';
import { StyleInfo } from 'lit/directives/style-map.js'; import { StyleInfo } from 'lit/directives/style-map.js';
import { ZodSchema, z } from 'zod'; import { ZodSchema, z } from 'zod';
@@ -596,12 +597,19 @@ export const frigateCardHandleActionConfig = (
action: string, action: string,
actionConfig: ActionType | ActionType[] | undefined, actionConfig: ActionType | ActionType[] | undefined,
): boolean => { ): boolean => {
// Only allow a tap action to use a default non-config (the more-info config).
if (actionConfig || action == 'tap') { if (actionConfig || action == 'tap') {
// Only allow a tap action to use a default non-config (the more-info config). // ActionConfig vs ActionType:
// There is a slight typing (but not functional) difference between
// ActionType in this card and ActionConfig in `custom-card-helpers`. See
// `ExtendedConfirmationRestrictionConfig` in `types.ts` for the source and
// reason behind this difference.
if (Array.isArray(actionConfig)) { if (Array.isArray(actionConfig)) {
actionConfig.forEach((action) => handleActionConfig(node, hass, config, action)); actionConfig.forEach((action) =>
handleActionConfig(node, hass, config, action as ActionConfig | undefined),
);
} else { } else {
handleActionConfig(node, hass, config, actionConfig); handleActionConfig(node, hass, config, actionConfig as ActionConfig | undefined);
} }
return true; return true;
} }
@@ -617,10 +625,12 @@ export const frigateCardHandleActionConfig = (
export const frigateCardHasAction = ( export const frigateCardHasAction = (
config?: ActionType | ActionType[] | undefined, config?: ActionType | ActionType[] | undefined,
): boolean => { ): boolean => {
// See note above on 'ActionConfig vs ActionType' for why this cast is
// necessary and harmless.
if (Array.isArray(config)) { if (Array.isArray(config)) {
return !!config.find((item) => hasAction(item)); return !!config.find((item) => hasAction(item as ActionConfig | undefined));
} }
return hasAction(config); return hasAction(config as ActionConfig | undefined);
}; };
/** /**
+62 -22
View File
@@ -1,6 +1,7 @@
import { StyleInfo } from 'lit/directives/style-map.js'; import { StyleInfo } from 'lit/directives/style-map.js';
import { import {
CallServiceActionConfig, CallServiceActionConfig,
ConfirmationRestrictionConfig,
CustomActionConfig, CustomActionConfig,
HomeAssistant, HomeAssistant,
LovelaceCard, LovelaceCard,
@@ -68,7 +69,6 @@ export class FrigateCardError extends Error {}
/** /**
* Action Types (for "Picture Elements" / Menu) * Action Types (for "Picture Elements" / Menu)
*/ */
// Declare schemas to existing types: // Declare schemas to existing types:
// - https://github.com/colinhacks/zod/issues/372#issuecomment-826380330 // - https://github.com/colinhacks/zod/issues/372#issuecomment-826380330
const schemaForType = const schemaForType =
@@ -77,47 +77,87 @@ const schemaForType =
<S extends z.ZodType<T, any, any>>(arg: S) => { <S extends z.ZodType<T, any, any>>(arg: S) => {
return arg; return arg;
}; };
const toggleActionSchema = schemaForType<ToggleActionConfig>()(
z.object({ // https://www.home-assistant.io/dashboards/actions/#options-for-confirmation
const actionBaseSchema = z.object({
confirmation: z
.boolean()
.or(
z.object({
text: z.string().optional(),
exemptions: z
.object({
user: z.string(),
})
.array()
.optional(),
}),
)
.optional(),
});
// HA accepts either a boolean or a ConfirmationRestrictionConfig object.
// `custom-card-helpers` currently only supports the latter. For maximum
// compatibility, this card supports what HA supports.
export interface ExtendedConfirmationRestrictionConfig {
confirmation?: boolean | ConfirmationRestrictionConfig;
}
const toggleActionSchema = schemaForType<
ToggleActionConfig & ExtendedConfirmationRestrictionConfig
>()(
actionBaseSchema.extend({
action: z.literal('toggle'), action: z.literal('toggle'),
}), }),
); );
const callServiceActionSchema = schemaForType<CallServiceActionConfig>()( const callServiceActionSchema = schemaForType<
z.object({ CallServiceActionConfig & ExtendedConfirmationRestrictionConfig
>()(
actionBaseSchema.extend({
action: z.literal('call-service'), action: z.literal('call-service'),
service: z.string(), service: z.string(),
service_data: z.object({}).passthrough().optional(), service_data: z.object({}).passthrough().optional(),
}), }),
); );
const navigateActionSchema = schemaForType<NavigateActionConfig>()( const navigateActionSchema = schemaForType<
z.object({ NavigateActionConfig & ExtendedConfirmationRestrictionConfig
>()(
actionBaseSchema.extend({
action: z.literal('navigate'), action: z.literal('navigate'),
navigation_path: z.string(), navigation_path: z.string(),
}), }),
); );
const urlActionSchema = schemaForType<UrlActionConfig>()( const urlActionSchema = schemaForType<
z.object({ UrlActionConfig & ExtendedConfirmationRestrictionConfig
>()(
actionBaseSchema.extend({
action: z.literal('url'), action: z.literal('url'),
url_path: z.string(), url_path: z.string(),
}), }),
); );
const moreInfoActionSchema = schemaForType<MoreInfoActionConfig>()( const moreInfoActionSchema = schemaForType<
z.object({ MoreInfoActionConfig & ExtendedConfirmationRestrictionConfig
>()(
actionBaseSchema.extend({
action: z.literal('more-info'), action: z.literal('more-info'),
}), }),
); );
const customActionSchema = schemaForType<CustomActionConfig>()( const customActionSchema = schemaForType<
z.object({ CustomActionConfig & ExtendedConfirmationRestrictionConfig
>()(
actionBaseSchema.extend({
action: z.literal('fire-dom-event'), action: z.literal('fire-dom-event'),
}), }),
); );
const noActionSchema = schemaForType<NoActionConfig>()( const noActionSchema = schemaForType<
z.object({ NoActionConfig & ExtendedConfirmationRestrictionConfig
>()(
actionBaseSchema.extend({
action: z.literal('none'), action: z.literal('none'),
}), }),
); );
const frigateCardCustomActionBaseSchema = customActionSchema.extend({ const frigateCardCustomactionsBaseSchema = customActionSchema.extend({
// Syntactic sugar to avoid 'fire-dom-event' as part of an external API. // Syntactic sugar to avoid 'fire-dom-event' as part of an external API.
action: z action: z
.literal('custom:frigate-card-action') .literal('custom:frigate-card-action')
@@ -142,10 +182,10 @@ const FRIGATE_CARD_GENERAL_ACTIONS = [
const FRIGATE_CARD_ACTIONS = [...FRIGATE_CARD_GENERAL_ACTIONS, 'camera_select'] as const; const FRIGATE_CARD_ACTIONS = [...FRIGATE_CARD_GENERAL_ACTIONS, 'camera_select'] as const;
export type FrigateCardAction = typeof FRIGATE_CARD_ACTIONS[number]; export type FrigateCardAction = typeof FRIGATE_CARD_ACTIONS[number];
const frigateCardGeneralActionSchema = frigateCardCustomActionBaseSchema.extend({ const frigateCardGeneralActionSchema = frigateCardCustomactionsBaseSchema.extend({
frigate_card_action: z.enum(FRIGATE_CARD_GENERAL_ACTIONS), frigate_card_action: z.enum(FRIGATE_CARD_GENERAL_ACTIONS),
}); });
const frigateCardCameraSelectActionSchema = frigateCardCustomActionBaseSchema.extend({ const frigateCardCameraSelectActionSchema = frigateCardCustomactionsBaseSchema.extend({
frigate_card_action: z.literal('camera_select'), frigate_card_action: z.literal('camera_select'),
camera: z.string(), camera: z.string(),
}); });
@@ -168,7 +208,7 @@ const actionSchema = z.union([
]); ]);
export type ActionType = z.infer<typeof actionSchema>; export type ActionType = z.infer<typeof actionSchema>;
const actionBaseSchema = z const actionsBaseSchema = z
.object({ .object({
tap_action: actionSchema.or(actionSchema.array()).optional(), tap_action: actionSchema.or(actionSchema.array()).optional(),
hold_action: actionSchema.or(actionSchema.array()).optional(), hold_action: actionSchema.or(actionSchema.array()).optional(),
@@ -180,17 +220,17 @@ const actionBaseSchema = z
// card doesn't need these attributes, but handleAction() in // card doesn't need these attributes, but handleAction() in
// custom_card_helpers may depending on how the action is configured. // custom_card_helpers may depending on how the action is configured.
.passthrough(); .passthrough();
export type Actions = z.infer<typeof actionBaseSchema>; export type Actions = z.infer<typeof actionsBaseSchema>;
export type ActionsConfig = Actions & { export type ActionsConfig = Actions & {
camera_image?: string; camera_image?: string;
entity?: string; entity?: string;
}; };
const actionsSchema = z.object({ const actionsSchema = z.object({
actions: actionBaseSchema.optional(), actions: actionsBaseSchema.optional(),
}); });
const elementsBaseSchema = actionBaseSchema.extend({ const elementsBaseSchema = actionsBaseSchema.extend({
style: z.object({}).passthrough().optional(), style: z.object({}).passthrough().optional(),
title: z.string().nullable().optional(), title: z.string().nullable().optional(),
}); });