From cfd390ede33dce720b21ead02d69dddee80de85b Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sat, 30 Apr 2022 08:52:10 -0700 Subject: [PATCH] Support confirmation for actions. --- src/common.ts | 20 +++++++++--- src/types.ts | 84 +++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 77 insertions(+), 27 deletions(-) diff --git a/src/common.ts b/src/common.ts index f3308871..0ca79446 100644 --- a/src/common.ts +++ b/src/common.ts @@ -5,6 +5,7 @@ import { handleActionConfig, hasAction, stateIcon, + ActionConfig, } from 'custom-card-helpers'; import { StyleInfo } from 'lit/directives/style-map.js'; import { ZodSchema, z } from 'zod'; @@ -596,12 +597,19 @@ export const frigateCardHandleActionConfig = ( action: string, actionConfig: ActionType | ActionType[] | undefined, ): boolean => { + // Only allow a tap action to use a default non-config (the more-info config). 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)) { - actionConfig.forEach((action) => handleActionConfig(node, hass, config, action)); + actionConfig.forEach((action) => + handleActionConfig(node, hass, config, action as ActionConfig | undefined), + ); } else { - handleActionConfig(node, hass, config, actionConfig); + handleActionConfig(node, hass, config, actionConfig as ActionConfig | undefined); } return true; } @@ -617,10 +625,12 @@ export const frigateCardHandleActionConfig = ( export const frigateCardHasAction = ( config?: ActionType | ActionType[] | undefined, ): boolean => { + // See note above on 'ActionConfig vs ActionType' for why this cast is + // necessary and harmless. 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); }; /** diff --git a/src/types.ts b/src/types.ts index a6638d2a..513c2da3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,7 @@ import { StyleInfo } from 'lit/directives/style-map.js'; import { CallServiceActionConfig, + ConfirmationRestrictionConfig, CustomActionConfig, HomeAssistant, LovelaceCard, @@ -68,7 +69,6 @@ export class FrigateCardError extends Error {} /** * Action Types (for "Picture Elements" / Menu) */ - // Declare schemas to existing types: // - https://github.com/colinhacks/zod/issues/372#issuecomment-826380330 const schemaForType = @@ -77,47 +77,87 @@ const schemaForType = >(arg: S) => { return arg; }; -const toggleActionSchema = schemaForType()( - 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'), }), ); -const callServiceActionSchema = schemaForType()( - z.object({ +const callServiceActionSchema = schemaForType< + CallServiceActionConfig & ExtendedConfirmationRestrictionConfig +>()( + actionBaseSchema.extend({ action: z.literal('call-service'), service: z.string(), service_data: z.object({}).passthrough().optional(), }), ); -const navigateActionSchema = schemaForType()( - z.object({ +const navigateActionSchema = schemaForType< + NavigateActionConfig & ExtendedConfirmationRestrictionConfig +>()( + actionBaseSchema.extend({ action: z.literal('navigate'), navigation_path: z.string(), }), ); -const urlActionSchema = schemaForType()( - z.object({ +const urlActionSchema = schemaForType< + UrlActionConfig & ExtendedConfirmationRestrictionConfig +>()( + actionBaseSchema.extend({ action: z.literal('url'), url_path: z.string(), }), ); -const moreInfoActionSchema = schemaForType()( - z.object({ +const moreInfoActionSchema = schemaForType< + MoreInfoActionConfig & ExtendedConfirmationRestrictionConfig +>()( + actionBaseSchema.extend({ action: z.literal('more-info'), }), ); -const customActionSchema = schemaForType()( - z.object({ +const customActionSchema = schemaForType< + CustomActionConfig & ExtendedConfirmationRestrictionConfig +>()( + actionBaseSchema.extend({ action: z.literal('fire-dom-event'), }), ); -const noActionSchema = schemaForType()( - z.object({ +const noActionSchema = schemaForType< + NoActionConfig & ExtendedConfirmationRestrictionConfig +>()( + actionBaseSchema.extend({ 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. action: z .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; 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), }); -const frigateCardCameraSelectActionSchema = frigateCardCustomActionBaseSchema.extend({ +const frigateCardCameraSelectActionSchema = frigateCardCustomactionsBaseSchema.extend({ frigate_card_action: z.literal('camera_select'), camera: z.string(), }); @@ -168,7 +208,7 @@ const actionSchema = z.union([ ]); export type ActionType = z.infer; -const actionBaseSchema = z +const actionsBaseSchema = z .object({ tap_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 // custom_card_helpers may depending on how the action is configured. .passthrough(); -export type Actions = z.infer; +export type Actions = z.infer; export type ActionsConfig = Actions & { camera_image?: string; entity?: string; }; const actionsSchema = z.object({ - actions: actionBaseSchema.optional(), + actions: actionsBaseSchema.optional(), }); -const elementsBaseSchema = actionBaseSchema.extend({ +const elementsBaseSchema = actionsBaseSchema.extend({ style: z.object({}).passthrough().optional(), title: z.string().nullable().optional(), });