frontend/smart-hut/src/components/dashboard/AutomationCreationModal.js
2020-05-01 16:42:42 +02:00

473 lines
13 KiB
JavaScript

import React, { Component, useState } from "react";
import { connect } from "react-redux";
import { RemoteService } from "../../remote";
import update from "immutability-helper";
import "./Automations.css";
import {
Segment,
Grid,
Icon,
Header,
Input,
Button,
List,
Divider,
Menu,
Form,
Dropdown,
Checkbox,
} from "semantic-ui-react";
export const operands = [
{ key: "EQUAL", text: "=", value: "EQUAL" },
{
key: "GREATER_EQUAL",
text: "\u2265",
value: "GREATER_EQUAL",
},
{
key: "GREATER",
text: ">",
value: "GREATER",
},
{
key: "LESS_EQUAL",
text: "\u2264",
value: "LESS_EQUAL",
},
{
key: "LESS",
text: "<",
value: "LESS",
},
];
const deviceStateOptions = [
{ key: "off", text: "off", value: false },
{ key: "on", text: "on", value: true },
];
const CreateTrigger = (props) => {
const [activeOperand, setActiveOperand] = useState(true);
const admitedDevices = ["sensor", "regularLight", "dimmableLight"]; // TODO: Complete this list
const deviceList = props.devices
.map((device) => {
return {
key: device.id,
text: device.name,
value: device.id,
kind: device.kind,
};
})
.filter((e) => admitedDevices.includes(e.kind));
const onChange = (e, val) => {
props.inputChange(val);
if (
props.devices.filter((d) => d.id === val.value)[0].hasOwnProperty("on")
) {
setActiveOperand(false);
} else {
setActiveOperand(true);
}
};
return (
<List.Item>
<List.Content>
<Form>
<Form.Group>
<Form.Field inline width={7}>
<Dropdown
onChange={onChange}
name="device"
search
selection
options={deviceList}
placeholder="Device"
/>
</Form.Field>
{activeOperand ? (
<React.Fragment>
<Form.Field inline width={2}>
<Dropdown
onChange={(e, val) => props.inputChange(val)}
name="operand"
compact
selection
options={operands}
/>
</Form.Field>
<Form.Field inline width={7}>
<Input
onChange={(e, val) => props.inputChange(val)}
name="value"
type="number"
placeholder="Value"
/>
</Form.Field>
</React.Fragment>
) : (
<Form.Field inline width={7}>
<Dropdown
onChange={(e, val) => props.inputChange(val)}
placeholder="State"
name="on"
compact
selection
options={deviceStateOptions}
/>
</Form.Field>
)}
</Form.Group>
</Form>
</List.Content>
</List.Item>
);
};
const SceneItem = (props) => {
let position = props.order.indexOf(props.scene.id);
return (
<List.Item>
<List.Header>
<Grid textAlign="center">
<Grid.Row>
<Grid.Column width={4}>
<Checkbox
toggle
onChange={(e, val) =>
props.orderScenes(props.scene.id, val.checked)
}
checked={position + 1 > 0}
/>
</Grid.Column>
<Grid.Column width={8}>
<h3>{props.scene.name}</h3>
</Grid.Column>
<Grid.Column width={4}>
<h3>{position !== -1 ? "# " + (position + 1) : ""}</h3>
</Grid.Column>
</Grid.Row>
</Grid>
</List.Header>
</List.Item>
);
};
const Trigger = ({ deviceName, trigger, onRemove, index }) => {
const { operand, value, on } = trigger;
let symbol;
if (operand) {
symbol = operands.filter((opt) => opt.key === operand)[0].text;
}
return (
<List.Item className="trigger-item">
<Menu compact>
<Menu.Item as="span">{deviceName}</Menu.Item>
{operand ? <Menu.Item as="span">{symbol}</Menu.Item> : ""}
<Menu.Item as="span">{operand ? value : on ? "on" : "off"}</Menu.Item>
</Menu>
<Icon
as={"i"}
onClick={() => onRemove(index)}
className="remove-icon"
name="remove"
/>
</List.Item>
);
};
class AutomationSaveModal extends Component {
constructor(props) {
super(props);
this.props = props;
this.state = {
triggerList: [],
order: [],
automationName: "New Automation",
editName: false,
newTrigger: {},
scenesFilter: null,
};
this.setTrigger = this._setter("triggerList");
this.setOrder = this._setter("order");
this.setautomationName = this._setter("automationName");
this.setEditName = this._setter("editName");
this.setNewTrigger = this._setter("newTrigger");
this.addTrigger = this.addTrigger.bind(this);
this.removeTrigger = this.removeTrigger.bind(this);
this.onInputChange = this.onInputChange.bind(this);
this.searchScenes = this.searchScenes.bind(this);
this.orderScenes = this.orderScenes.bind(this);
}
get deviceList() {
return Object.values(this.props.devices);
}
_setter(property) {
return (value) =>
this.setState(update(this.state, { [property]: { $set: value } }));
}
triggerKind(trigger) {
if ("on" in trigger) {
return "BooleanTrigger";
} else if ("operand" in trigger && "value" in trigger) {
return "RangeTrigger";
} else {
throw new Error("Trigger kind not handled");
}
}
_checkNewTrigger(trigger) {
const auxDevice = this.props.devices[trigger.device];
// Check for missing fields for creation
const error = {
result: false,
message: "There are missing fields!",
};
switch (this.triggerKind(trigger)) {
case "BooleanTrigger":
if (!trigger.device || !trigger.on) return error;
break;
case "RangeTrigger":
if (!trigger.device || !trigger.operand || !trigger.value) return error;
break;
}
const isNotDuplicate = !this.state.triggerList.some(
(t) => t.device === trigger.device && t.operand === trigger.operand
);
return {
result: isNotDuplicate,
message: isNotDuplicate
? null
: "You have already created a trigger for this device with the same conditions",
};
}
addTrigger() {
const { result, message } = this._checkNewTrigger(this.state.newTrigger);
if (result) {
this.setState(
update(this.state, { triggerList: { $push: [this.state.newTrigger] } })
);
} else {
alert(message);
}
}
removeTrigger(index) {
this.setState(
update(this.state, { triggerList: { $splice: [[index, 1]] } })
);
}
// This gets triggered when the devices dropdown changes the value.
onInputChange(val) {
this.setNewTrigger({ ...this.state.newTrigger, [val.name]: val.value });
}
onChangeName(_, val) {
this.setautomationName(val.value);
}
orderScenes = (id, checked) => {
if (checked) {
this.setState(update(this.state, { order: { $push: [id] } }));
} else {
this.setState(
update(this.state, {
order: (prevList) => prevList.filter((e) => e !== id),
})
);
}
};
searchScenes(_, { value }) {
this.setState(update(this.state, { scenesFilter: { $set: value } }));
this.forceUpdate();
}
get sceneList() {
if (!this.scenesFilter) {
return this.props.scenes;
} else {
return this.props.scenes.filter((e) =>
e.name.includes(this.scenesFilter)
);
}
}
_generateKey = (trigger) => {
switch (this.triggerKind(trigger)) {
case "BooleanTrigger":
return trigger.device + trigger.value;
case "RangeTrigger":
return trigger.device + trigger.operand + trigger.value;
}
};
checkBeforeSave = () => {
if (!this.state.automationName) {
alert("Give a name to the automation");
return false;
}
if (this.state.triggerList.length <= 0) {
alert("You have to create a trigger");
return false;
}
if (this.state.order.length <= 0) {
alert("You need at least one active scene");
return false;
}
return true;
};
saveAutomation = () => {
if (this.checkBeforeSave()) {
const automation = {
name: this.state.automationName,
};
this.props.save({
automation,
triggerList: this.state.triggerList,
order: this.state.order,
});
}
};
render() {
return (
<React.Fragment>
<Header style={{ display: "inline", marginRight: "1rem" }}>
{this.state.editName ? (
<Input
focus
transparent
placeholder="New automation name..."
onChange={this.onChangeName}
/>
) : (
this.state.automationName
)}
</Header>
<Button
onClick={() => this.setEditName((prev) => !prev)}
style={{ display: "inline" }}
circular
size="small"
icon={this.state.editName ? "save" : "edit"}
/>
<Segment placeholder className="segment-automations">
<Grid columns={2} stackable textAlign="center">
<Divider vertical />
<Grid.Row verticalAlign="middle">
<Grid.Column>
<Header>Create Triggers</Header>
<List divided relaxed>
{this.state.triggerList.length > 0 &&
this.state.triggerList.map((trigger, i) => {
const deviceName = this.deviceList.filter(
(d) => d.id === trigger.device
)[0].name;
const key = this._generateKey(trigger);
return (
<Trigger
key={key}
index={i}
deviceName={deviceName}
trigger={trigger}
onRemove={this.removeTrigger}
/>
);
})}
<CreateTrigger
devices={this.deviceList}
inputChange={this.onInputChange}
/>
</List>
<Button
onClick={this.addTrigger}
circular
icon="add"
color="blue"
size="huge"
/>
</Grid.Column>
<Grid.Column>
{this.props.scenes.length > 0 ? (
<React.Fragment>
<Header>Activate Scenes</Header>
<Input
icon="search"
placeholder="Search..."
fluid
onChange={this.searchScenes}
/>
<Divider horizontal />
<List divided relaxed>
{this.sceneList.map((scene) => (
<SceneItem
key={scene.id}
scene={scene}
order={this.state.order}
orderScenes={this.orderScenes}
/>
))}
</List>
</React.Fragment>
) : (
<React.Fragment>
<Header icon>
<Icon name="world" />
</Header>
<Button primary>Create Scene</Button>
</React.Fragment>
)}
</Grid.Column>
</Grid.Row>
</Grid>
</Segment>
<Grid>
<Grid.Row>
<Grid.Column style={{ marginRight: "1rem" }}>
<Button onClick={() => this.saveAutomation()} color="green">
SAVE
</Button>
</Grid.Column>
</Grid.Row>
</Grid>
</React.Fragment>
);
}
}
const mapStateToProps = (state, _) => ({
activeRoom: state.active.activeRoom,
activeTab: state.active.activeTab,
get scenes() {
return Object.values(state.scenes);
},
devices: state.devices,
get automations() {
return Object.values(state.automations);
},
});
const AutomationSaveModalContainer = connect(
mapStateToProps,
RemoteService
)(AutomationSaveModal);
export default AutomationSaveModalContainer;