diff --git a/front/package-lock.json b/front/package-lock.json index 10f7191b72..93337684d3 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -13271,9 +13271,9 @@ } }, "preact": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.2.1.tgz", - "integrity": "sha512-BUNvmQcVtNElku7mYHexIiM5JlGNSW2BY9O2t9xk1NqA43O8wbE0cah6PAlvT7PBHvyDRJ5TAj5Fewdi9DqLoQ==" + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.3.2.tgz", + "integrity": "sha512-yIx4i7gp45enhzX4SLkvvR20UZ+YOUbMdj2KEscU/dC70MHv/L6dpTcsP+4sXrU9SRbA3GjJQQCPfFa5sE17dQ==" }, "preact-cli": { "version": "2.2.1", diff --git a/front/package.json b/front/package.json index 3a9bd485ea..bb0dcc650a 100644 --- a/front/package.json +++ b/front/package.json @@ -38,7 +38,7 @@ "leaflet": "^1.4.0", "linkstate": "^1.1.1", "moment": "^2.24.0", - "preact": "^10.2.1", + "preact": "^10.3.2", "preact-cli-plugin-fast-async": "^1.0.1", "preact-i18n": "^2.0.0-preactx.2", "preact-router": "^3.2.1", diff --git a/front/src/actions/createScene.js b/front/src/actions/createScene.js index b5e22cb3d4..c78f01ec7a 100644 --- a/front/src/actions/createScene.js +++ b/front/src/actions/createScene.js @@ -10,6 +10,9 @@ function createActions(store) { if (!state.newScene.name) { newSceneErrors.name = true; } + if (state.newScene.name === 'new') { + newSceneErrors.name = true; + } if (!state.newScene.icon) { newSceneErrors.icon = true; } diff --git a/front/src/actions/scene.js b/front/src/actions/scene.js index d18b35031d..687d233a23 100644 --- a/front/src/actions/scene.js +++ b/front/src/actions/scene.js @@ -1,7 +1,6 @@ import { RequestStatus } from '../utils/consts'; import update, { extend } from 'immutability-helper'; import debounce from 'debounce'; -import { route } from 'preact-router'; extend('$auto', function(value, object) { return object ? update(object, value) : update({}, value); @@ -43,204 +42,6 @@ function createActions(store) { getScenesOrderDir: e.target.value }); await actions.getScenes(store.getState()); - }, - async getSceneBySelector(state, sceneSelector) { - store.setState({ - SceneGetStatus: RequestStatus.Getting - }); - try { - const scene = await state.httpClient.get(`/api/v1/scene/${sceneSelector}`); - if (scene.actions[scene.actions.length - 1].length > 0) { - scene.actions.push([]); - } - if (!scene.triggers) { - scene.triggers = []; - } - store.setState({ - scene, - SceneGetStatus: RequestStatus.Success - }); - } catch (e) { - store.setState({ - SceneGetStatus: RequestStatus.Error - }); - } - }, - async startScene(state, selector) { - store.setState({ - SceneStartStatus: RequestStatus.Getting - }); - try { - await state.httpClient.post(`/api/v1/scene/${selector}/start`); - store.setState({ - SceneStartStatus: RequestStatus.Success - }); - } catch (e) { - store.setState({ - SceneStartStatus: RequestStatus.Error - }); - } - }, - async saveScene(state) { - await state.httpClient.patch(`/api/v1/scene/${state.scene.selector}`, state.scene); - }, - addAction(state, columnIndex) { - let newState = update(state, { - scene: { - actions: { - [columnIndex]: { - $push: [ - { - type: null - } - ] - } - } - } - }); - if (state.scene.actions[columnIndex].length === 0) { - newState = update(newState, { - scene: { - actions: { - $push: [[]] - } - } - }); - } - store.setState(newState); - }, - deleteAction(state, columnIndex, rowIndex) { - let newState = update(state, { - scene: { - actions: { - [columnIndex]: { - $splice: [[rowIndex, 1]] - } - } - } - }); - // if necessary, we remove the last action group - if (newState.scene.actions.length >= 2) { - if ( - newState.scene.actions[newState.scene.actions.length - 1].length === 0 && - newState.scene.actions[newState.scene.actions.length - 2].length === 0 - ) { - newState = update(newState, { - scene: { - actions: { - $splice: [[newState.scene.actions.length - 1, 1]] - } - } - }); - } - } - store.setState(newState); - }, - updateActionProperty(state, columnIndex, rowIndex, property, value) { - const newState = update(state, { - scene: { - actions: { - [columnIndex]: { - [rowIndex]: { - [property]: { - $set: value - } - } - } - } - } - }); - store.setState(newState); - }, - highlighCurrentlyExecutedAction(state, { columnIndex, rowIndex }) { - store.setState({ - highLightedActions: { - [`${columnIndex}:${rowIndex}`]: true - } - }); - }, - removeHighlighAction(state, { columnIndex, rowIndex }) { - setTimeout(() => { - store.setState({ - highLightedActions: { - [`${columnIndex}:${rowIndex}`]: false - } - }); - }, 500); - }, - async getUsers(state) { - store.setState({ - GetUsersStatus: RequestStatus.Getting - }); - try { - const users = await state.httpClient.get(`/api/v1/user`); - const sceneParamsData = { - users - }; - store.setState({ - sceneParamsData, - GetUsersStatus: RequestStatus.Success - }); - } catch (e) { - store.setState({ - GetUsersStatus: RequestStatus.Error - }); - } - }, - async deleteScene(state, selector) { - store.setState({ - deleteSceneStatus: RequestStatus.Getting - }); - try { - await state.httpClient.delete(`/api/v1/scene/${selector}`); - store.setState({ - deleteSceneStatus: RequestStatus.Success - }); - route('/dashboard/scene'); - } catch (e) { - store.setState({ - GetUsersStatus: RequestStatus.Error - }); - } - }, - addTrigger(state) { - const newState = update(state, { - scene: { - triggers: { - $push: [ - { - type: null - } - ] - } - } - }); - store.setState(newState); - }, - deleteTrigger(state, index) { - const newState = update(state, { - scene: { - triggers: { - $splice: [[index, 1]] - } - } - }); - store.setState(newState); - }, - updateTriggerProperty(state, index, property, value) { - console.log({ index, property, value }); - const newState = update(state, { - scene: { - triggers: { - [index]: { - [property]: { - $set: value - } - } - } - } - }); - store.setState(newState); } }; actions.debouncedSearch = debounce(actions.search, 200); diff --git a/front/src/components/device/SelectDeviceFeature.jsx b/front/src/components/device/SelectDeviceFeature.jsx new file mode 100644 index 0000000000..00ebb92772 --- /dev/null +++ b/front/src/components/device/SelectDeviceFeature.jsx @@ -0,0 +1,112 @@ +import { Component } from 'preact'; +import { connect } from 'unistore/preact'; +import Select from 'react-select'; + +import { getDeviceFeatureName } from '../../utils/device'; + +@connect('httpClient', {}) +class SelectDeviceFeature extends Component { + getOptions = async () => { + try { + // we get the rooms with the devices + const rooms = await this.props.httpClient.get('/api/v1/room?expand=devices'); + const deviceOptions = []; + + const deviceDictionnary = {}; + const deviceFeaturesDictionnary = {}; + + // and compose the multi-level options + rooms.forEach(room => { + const roomDeviceFeatures = []; + room.devices.forEach(device => { + device.features.forEach(feature => { + // keep device / deviceFeature in dictionnary + deviceFeaturesDictionnary[feature.selector] = feature; + deviceDictionnary[feature.selector] = device; + + roomDeviceFeatures.push({ + value: feature.selector, + label: getDeviceFeatureName(this.context.intl.dictionary, device, feature) + }); + }); + }); + if (roomDeviceFeatures.length > 0) { + roomDeviceFeatures.sort((a, b) => { + if (a.label < b.label) { + return -1; + } else if (a.label > b.label) { + return 1; + } + return 0; + }); + deviceOptions.push({ + label: room.name, + options: roomDeviceFeatures + }); + } + }); + await this.setState({ deviceOptions, deviceFeaturesDictionnary, deviceDictionnary }); + await this.refreshSelectedOptions(this.props); + if (this.state.selectedOption && this.state.selectedOption.value) { + this.props.onDeviceFeatureChange( + deviceFeaturesDictionnary[this.state.selectedOption.value], + deviceDictionnary[this.state.selectedOption.value] + ); + } + return deviceOptions; + } catch (e) { + console.log(e); + } + }; + handleChange = selectedOption => { + const { deviceFeaturesDictionnary, deviceDictionnary } = this.state; + if (selectedOption && selectedOption.value) { + this.props.onDeviceFeatureChange( + deviceFeaturesDictionnary[selectedOption.value], + deviceDictionnary[selectedOption.value] + ); + } else { + this.props.onDeviceFeatureChange(null); + } + }; + refreshSelectedOptions = async nextProps => { + let selectedOption = ''; + if (nextProps.value && this.state.deviceOptions) { + let deviceOption; + let i = 0; + while (i < this.state.deviceOptions.length && deviceOption === undefined) { + deviceOption = this.state.deviceOptions[i].options.find(option => option.value === nextProps.value); + i++; + } + + if (deviceOption) { + selectedOption = deviceOption; + } + } + await this.setState({ selectedOption }); + }; + constructor(props) { + super(props); + this.state = { + deviceOptions: null, + selectedOption: '' + }; + } + + async componentDidMount() { + this.getOptions(); + } + + componentWillReceiveProps(nextProps) { + this.refreshSelectedOptions(nextProps); + } + + render(props, { selectedOption, deviceOptions }) { + if (!deviceOptions) { + return null; + } + return ; + } +} + +export default SelectDeviceFeature; diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json index 4787650d68..cc47dde2aa 100644 --- a/front/src/config/i18n/en.json +++ b/front/src/config/i18n/en.json @@ -434,12 +434,12 @@ "selectTriggerLabel": "Select a trigger type", "newAction": "New action", "selectActionType": "Select an action type", - "addActionButton": "Add new action", + "addActionButton": "Add action", "noTriggersYet": "No trigger added yet. It's not mandatory to have a trigger in a scene.", "noActionsYet": "No actions added yet. Click on the + button to add an action to this scene.", "triggersDescription": "Every trigger is independent. When any of those triggers is triggered, the scene will run.", "actionsDescription": "All actions in this block will run in parallel. To make a sequence of actions, add actions to the next block.", - "addNewTriggerButton": "Add new trigger", + "addNewTriggerButton": "Add trigger", "saveSceneError": "There was an error saving your scene. Please check that all actions/triggers are filled and correct.", "actionsCard": { "delay": { @@ -454,15 +454,37 @@ "userLabel": "User", "textLabel": "Message", "textPlaceholder": "Message text" + }, + "turnOnLights": { + "label": "Select the lights you want to turn on" + }, + "turnOffLights": { + "label": "Select the lights you want to turn off" + }, + "deviceGetValue": { + "description": "This will get the last value of the selected device, so you can use it later in the scene.", + "deviceLabel": "Select a device" + }, + "onlyContinueIf": { + "variableLabel": "Variable", + "operatorLabel": "Operator", + "valueLabel": "Value", + "removeLabel": "Remove", + "orText": "OR", + "orButton": "OR" } }, "actions": { + "device": { + "get-value": "Get device value" + }, "message": { "send": "Send Message" }, "delay": "Wait", "light": { - "turn-on": "Turn On the Lights" + "turn-on": "Turn On the Lights", + "turn-off": "Turn Off the Lights" }, "scene": { "start": "Start scene" @@ -470,6 +492,16 @@ "service": { "start": "Start service", "stop": "Stop service" + }, + "condition": { + "only-continue-if": "Only continue if" + } + }, + "variables": { + "device": { + "get-value": { + "last_value": "Device last value" + } } }, "triggers": { @@ -624,7 +656,7 @@ "namePlaceholder": "Enter the name of your scene", "iconLabel": "Select an icon for you scene", "createSceneButton": "Create scene", - "invalidName": "Name is required", + "invalidName": "Name is required and cannot be 'new'", "invalidIcon": "Icon is required", "sceneAlreadyExist": "A scene with that name already exist." }, diff --git a/front/src/routes/scene/edit-scene/ActionCard.jsx b/front/src/routes/scene/edit-scene/ActionCard.jsx index 0dcebaced1..a0dea9ea66 100644 --- a/front/src/routes/scene/edit-scene/ActionCard.jsx +++ b/front/src/routes/scene/edit-scene/ActionCard.jsx @@ -1,85 +1,124 @@ import { h } from 'preact'; import { Text } from 'preact-i18n'; +import { ACTIONS } from '../../../../../server/utils/constants'; + +// Actions cards +import ChooseActionTypeParams from './actions/ChooseActionTypeCard'; import DelayActionParams from './actions/DelayActionParams'; -import TurnOnLightParams from './actions/TurnOnLightParams'; +import DeviceGetValueParams from './actions/DeviceGetValueParams'; import SendMessageParams from './actions/SendMessageParams'; -import ChooseActionTypeParams from './actions/ChooseActionTypeCard'; +import OnlyContinueIfParams from './actions/only-continue-if/OnlyContinueIfParams'; +import TurnOnOffLightParams from './actions/TurnOnOffLightParams'; const deleteActionFromColumn = (columnIndex, rowIndex, deleteAction) => () => { deleteAction(columnIndex, rowIndex); }; const ACTION_ICON = { - 'light.turn-on': 'fe fe-sun', - delay: 'fe fe-clock', - 'message.send': 'fe fe-message-square' + [ACTIONS.LIGHT.TURN_ON]: 'fe fe-toggle-right', + [ACTIONS.LIGHT.TURN_OFF]: 'fe fe-toggle-left', + [ACTIONS.TIME.DELAY]: 'fe fe-clock', + [ACTIONS.MESSAGE.SEND]: 'fe fe-message-square', + [ACTIONS.CONDITION.ONLY_CONTINUE_IF]: 'fe fe-shuffle', + [ACTIONS.DEVICE.GET_VALUE]: 'fe fe-refresh-cw' }; const ActionCard = ({ children, ...props }) => ( -
+