From 32acd4c7b6696d607885e1b59b487b2432b136b4 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 28 Jun 2020 18:41:28 +0100 Subject: [PATCH] Entity Click Action (#1218) * Add config for entity custom click aciton * Set toggleable if call-service * Add logic for custom action * Don't trigger light if default overriden * Remove prop setting of domain for ad-hoc logic --- frontend/src/Components/Cards/Base.tsx | 82 ++++++++----- .../src/Components/Configuration/Config.tsx | 18 +-- .../Configuration/EditCard/Base.tsx | 14 ++- .../Configuration/EditCard/EditCard.tsx | 39 +++++- .../Configuration/EditCard/Entity.tsx | 4 +- .../Configuration/EditCard/EntityAction.tsx | 116 ++++++++++++++++++ .../HomeAssistant/Cards/AlarmPanel.tsx | 5 +- .../Components/HomeAssistant/Cards/Entity.tsx | 6 +- .../Components/HomeAssistant/Cards/Light.tsx | 4 +- 9 files changed, 234 insertions(+), 54 deletions(-) create mode 100644 frontend/src/Components/Configuration/EditCard/EntityAction.tsx diff --git a/frontend/src/Components/Cards/Base.tsx b/frontend/src/Components/Cards/Base.tsx index 40f3c4a92..2878a3ea3 100644 --- a/frontend/src/Components/Cards/Base.tsx +++ b/frontend/src/Components/Cards/Base.tsx @@ -127,12 +127,19 @@ function Base(props: BaseProps): ReactElement { ); const handleSetToggleable = useCallback(() => { - setToggleable( - props.editing === 1 - ? false - : !props.card.disabled && props.card.toggleable - ); - }, [props.card.disabled, props.card.toggleable, props.editing]); + if (props.card.click_action?.type === 'call-service') setToggleable(true); + else + setToggleable( + props.editing === 1 + ? false + : !props.card.disabled && props.card.toggleable + ); + }, [ + props.card.click_action, + props.card.disabled, + props.card.toggleable, + props.editing, + ]); const handleSetExpandable = useCallback( (entitySizeKey: string) => { @@ -175,33 +182,48 @@ function Base(props: BaseProps): ReactElement { ]); function handleHassToggle(): void { - if (props.card.domain && props.handleHassChange) - if (props.card.domain === 'lock') { - console.log(props.card.state); + if (props.handleHassChange) { + const domain = props.card.entity?.split('.')[0]; + console.log('handleHassToggle', props.card); + if ( + props.card.click_action && + props.card.click_action.type === 'call-service' && + props.card.click_action.service && + props.card.click_action.service_data + ) { + const service = props.card.click_action.service.split('.'); props.handleHassChange( - props.card.domain, - props.card.state === 'locked' ? 'unlock' : 'lock', - { - entity_id: props.card.entity, - } - ); - } else { - console.log( - props.card.domain, - props.card.state === 'on' ? false : true, - { - entity_id: props.card.entity, - } - ); - props.handleHassChange( - props.card.domain, - props.card.state === 'on' ? false : true, - { - entity_id: props.card.entity, - }, - props.hassEntities + service[0], + service[1], + JSON.parse(props.card.click_action.service_data) ); + } else if (domain) { + if (domain === 'lock') { + process.env.NODE_ENV === 'development' && + console.log(props.card.state); + props.handleHassChange( + domain, + props.card.state === 'locked' ? 'unlock' : 'lock', + { + entity_id: props.card.entity, + } + ); + } else { + process.env.NODE_ENV === 'development' && + console.log(domain, props.card.state === 'on' ? false : true, { + entity_id: props.card.entity, + }); + props.handleHassChange( + domain, + props.card.state === 'on' ? false : true, + { + entity_id: props.card.entity, + }, + props.hassEntities + ); + } } + } } function handleExpand(): void { diff --git a/frontend/src/Components/Configuration/Config.tsx b/frontend/src/Components/Configuration/Config.tsx index d4f400a1a..76958d8c8 100644 --- a/frontend/src/Components/Configuration/Config.tsx +++ b/frontend/src/Components/Configuration/Config.tsx @@ -18,14 +18,7 @@ export interface ConfigProps { | Page | GroupProps | CardProps - | ( - | string - | number - | ConfigSectionItem - | Page - | GroupProps - | CardProps - )[] + | (string | number | ConfigSectionItem | Page | GroupProps | CardProps)[] ) => void; handleConfigChange: (config: ConfigurationProps) => void; handleSetTheme: (palette: ThemeProps) => void; @@ -131,6 +124,7 @@ export type CardProps = { chart_from?: number; chart_labels?: boolean; checklist_items?: ChecklistItem[]; + click_action?: EntityAction; }; export interface ChecklistItem { @@ -139,6 +133,14 @@ export interface ChecklistItem { text: string; } +export interface EntityAction { + type: EntityActionType; + service?: string; + service_data?: string; +} + +export type EntityActionType = 'default' | 'call-service'; + export type News = { news_api_key: string; }; diff --git a/frontend/src/Components/Configuration/EditCard/Base.tsx b/frontend/src/Components/Configuration/EditCard/Base.tsx index df4f03e5e..c2446f161 100644 --- a/frontend/src/Components/Configuration/EditCard/Base.tsx +++ b/frontend/src/Components/Configuration/EditCard/Base.tsx @@ -11,7 +11,13 @@ import Switch from '@material-ui/core/Switch'; import TextField from '@material-ui/core/TextField'; import { ColorResult } from 'react-color'; -import { CardProps, cardTypes, CardType, ConfigurationProps } from '../Config'; +import { + CardProps, + cardTypes, + CardType, + ConfigurationProps, + EntityAction, +} from '../Config'; import { CommandType } from '../../Utils/Command'; import { HomeAssistantChangeProps } from '../../HomeAssistant/HomeAssistant'; import ColorAdornment from '../../Utils/ColorAdornment'; @@ -45,7 +51,10 @@ export interface BaseProps { card: CardProps; command: CommandType; config: ConfigurationProps; - handleManualChange?: (name: string, value?: string | number) => void; + handleManualChange?: ( + name: string, + value?: string | number | EntityAction + ) => void; handleChange?: ( name: string ) => (event: React.ChangeEvent) => void; @@ -55,6 +64,7 @@ export interface BaseProps { handleSelectChange?: ( event: React.ChangeEvent<{ name?: string; value: unknown }> ) => void; + handleValidation?: (key: string, error?: string) => void; } interface BaseExtendedProps extends BaseProps, HomeAssistantChangeProps {} diff --git a/frontend/src/Components/Configuration/EditCard/EditCard.tsx b/frontend/src/Components/Configuration/EditCard/EditCard.tsx index 890e606df..6b8fe731a 100644 --- a/frontend/src/Components/Configuration/EditCard/EditCard.tsx +++ b/frontend/src/Components/Configuration/EditCard/EditCard.tsx @@ -6,11 +6,13 @@ import DialogActions from '@material-ui/core/DialogActions'; import DialogContent from '@material-ui/core/DialogContent'; import DialogTitle from '@material-ui/core/DialogTitle'; import Grid from '@material-ui/core/Grid'; +import Typography from '@material-ui/core/Typography'; import useMediaQuery from '@material-ui/core/useMediaQuery'; -import { CardProps, cardTypeDefaults } from '../Config'; +import { CardProps, cardTypeDefaults, EntityAction } from '../Config'; import Base, { BaseProps } from './Base'; import CardBase from '../../Cards/Base'; +import clone from '../../../utils/clone'; const useStyles = makeStyles((theme: Theme) => ({ dialogContent: { @@ -34,13 +36,19 @@ const useStyles = makeStyles((theme: Theme) => ({ }, })); +interface Validation { + key: string; + error?: string; +} + interface EditCardProps extends BaseProps { handleClose: () => void; handleUpdate: (data: CardProps) => void; } function EditCard(props: EditCardProps): ReactElement { - const [card, setCard] = useState(props.card); + const [validation, setValidation] = useState([]); + const [card, setCard] = useState(props.card); useEffect(() => setCard(props.card), [props.card]); @@ -49,7 +57,10 @@ function EditCard(props: EditCardProps): ReactElement { props.handleClose(); } - function handleManualChange(name: string, value?: string | number): void { + function handleManualChange( + name: string, + value?: string | number | EntityAction + ): void { setCard({ ...card, [name]: value, @@ -104,6 +115,16 @@ function EditCard(props: EditCardProps): ReactElement { } } + function handleValidation(key: string, error?: string) { + const newVal = clone(validation); + const valIndex = validation.findIndex((val: Validation) => val.key === key); + if (valIndex > -1) newVal[valIndex].error = error; + else newVal.push({ key, error }); + setValidation(newVal); + } + + const valErrors = validation.filter((val: Validation) => val.error); + const classes = useStyles(); const theme = useTheme(); const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); @@ -129,10 +150,11 @@ function EditCard(props: EditCardProps): ReactElement { + + {valErrors.map((val: Validation, key: number) => ( + {val.error} + ))} + - + ); diff --git a/frontend/src/Components/Configuration/EditCard/Entity.tsx b/frontend/src/Components/Configuration/EditCard/Entity.tsx index 6d1a8e5a7..ef6d2e000 100644 --- a/frontend/src/Components/Configuration/EditCard/Entity.tsx +++ b/frontend/src/Components/Configuration/EditCard/Entity.tsx @@ -16,6 +16,7 @@ import { BaseProps } from './Base'; import { chartTypes } from '../../Visualisations/Chart'; import { HomeAssistantEntityProps } from '../../HomeAssistant/HomeAssistant'; import EntitySelect from '../../HomeAssistant/Utils/EntitySelect'; +import EntityAction from './EntityAction'; const useStyles = makeStyles((theme: Theme) => ({ container: { @@ -34,7 +35,7 @@ const useStyles = makeStyles((theme: Theme) => ({ }, })); -interface EntityProps extends BaseProps, HomeAssistantEntityProps {} +export interface EntityProps extends BaseProps, HomeAssistantEntityProps {} function Entity(props: EntityProps): ReactElement { function handleGetEntityIcon(): void { @@ -289,6 +290,7 @@ function Entity(props: EntityProps): ReactElement { )} )} + ); diff --git a/frontend/src/Components/Configuration/EditCard/EntityAction.tsx b/frontend/src/Components/Configuration/EditCard/EntityAction.tsx new file mode 100644 index 000000000..2f37006bd --- /dev/null +++ b/frontend/src/Components/Configuration/EditCard/EntityAction.tsx @@ -0,0 +1,116 @@ +import React, { ReactElement, useCallback, Fragment } from 'react'; +import { makeStyles, Theme } from '@material-ui/core/styles'; +import FormControl from '@material-ui/core/FormControl'; +import Grid from '@material-ui/core/Grid'; +import InputLabel from '@material-ui/core/InputLabel'; +import MenuItem from '@material-ui/core/MenuItem'; +import Select from '@material-ui/core/Select'; +import TextField from '@material-ui/core/TextField'; + +import { EntityProps } from './Entity'; + +const useStyles = makeStyles((theme: Theme) => ({ + textField: { + width: `calc(100% - ${theme.spacing(1)}px)`, + flex: '1 1 auto', + margin: 4, + }, +})); + +function EntityAction(props: EntityProps): ReactElement | null { + const classes = useStyles(); + + const handleChange = useCallback( + (key: string) => (event: React.ChangeEvent): void => { + if (key === 'service_data') { + let json; + try { + json = JSON.parse(event.target.value); + } catch (e) {} + if (json) { + if (props.handleValidation) + props.handleValidation('click_action_service_data'); + } else { + if (props.handleValidation) + props.handleValidation( + 'click_action_service_data', + 'Invalid JSON in Service Data' + ); + } + } + if (props.handleManualChange && props.card.click_action) { + props.handleManualChange('click_action', { + ...props.card.click_action, + [key]: event.target.value, + }); + } + }, + [props] + ); + + const handleSelectChange = useCallback( + (key: string) => ( + event: React.ChangeEvent<{ name?: string; value: unknown }> + ): void => { + if (props.handleManualChange && props.card.click_action) { + props.handleManualChange('click_action', { + ...props.card.click_action, + [key]: event.target.value, + }); + } + }, + [props] + ); + + if (!props.card.click_action) { + if (props.handleManualChange) + props.handleManualChange('click_action', { type: 'default' }); + return null; + } + return ( + + + + Click Action + + + + {props.card.click_action.type === 'call-service' && ( + + + + + + + + + )} + + ); +} + +export default EntityAction; diff --git a/frontend/src/Components/HomeAssistant/Cards/AlarmPanel.tsx b/frontend/src/Components/HomeAssistant/Cards/AlarmPanel.tsx index d2214c8a9..206628eb2 100644 --- a/frontend/src/Components/HomeAssistant/Cards/AlarmPanel.tsx +++ b/frontend/src/Components/HomeAssistant/Cards/AlarmPanel.tsx @@ -40,8 +40,9 @@ function AlarmPanel(props: EntityProps): ReactElement | null { }; const handleUpdate = (service: string) => (): void => { - if (props.handleHassChange && props.card.domain) - props.handleHassChange(props.card.domain, service, { + const domain = props.card.entity?.split('.')[0]; + if (props.handleHassChange && domain) + props.handleHassChange(domain, service, { entity_id: props.card.entity, code, }); diff --git a/frontend/src/Components/HomeAssistant/Cards/Entity.tsx b/frontend/src/Components/HomeAssistant/Cards/Entity.tsx index bb4fab0e6..de3757aea 100644 --- a/frontend/src/Components/HomeAssistant/Cards/Entity.tsx +++ b/frontend/src/Components/HomeAssistant/Cards/Entity.tsx @@ -1,4 +1,4 @@ -import React, { ReactElement, useEffect } from 'react'; +import React, { ReactElement } from 'react'; import { HassEntity } from 'home-assistant-js-websocket'; import { makeStyles } from '@material-ui/core/styles'; import Grid from '@material-ui/core/Grid'; @@ -44,10 +44,6 @@ function Entity(props: EntityBaseProps): ReactElement | null { let entity: HassEntity | undefined; - useEffect(() => { - if (entity) props.card.domain = entity.entity_id.split('.')[0]; - }, [props.card.domain, entity]); - const classes = useStyles(); if (!props.hassAuth || !props.hassConfig || !props.hassEntities) return null; diff --git a/frontend/src/Components/HomeAssistant/Cards/Light.tsx b/frontend/src/Components/HomeAssistant/Cards/Light.tsx index 87b26ec6b..6f7b9b9bd 100644 --- a/frontend/src/Components/HomeAssistant/Cards/Light.tsx +++ b/frontend/src/Components/HomeAssistant/Cards/Light.tsx @@ -232,7 +232,9 @@ function Light(props: EntityProps): ReactElement | null { justify="center" spacing={1}> - +