From 016e3b091d7677c3ac21d50cf4d7b1848f0b1e4c Mon Sep 17 00:00:00 2001 From: Yannick Schaus Date: Sat, 12 Dec 2020 21:46:10 +0100 Subject: [PATCH] Add wizards to help adding rule modules (#622) Replace the default flat grouped list of module types with a way user-friendlier UI that is aware of the core modules and offers an improved experience. Examples include directly jumping into the script editor, in Blockly mode, or pick the item from the model before deciding which event to consider. Also now the behavior of the icons on the cron triggers and script actions/conditions is now reversed: clicking on the bar will launch the special action, and clicking on the little icon will bring up the generic module editor popup. Fix time of day parameter, replaced day of week with an inline list. Move the Blockly button to the bottom in the script editor. Support system start level in triggers > system triggers. Signed-off-by: Yannick Schaus --- .../config/controls/item-picker.vue | 4 + .../config/controls/parameter-dayofweek.vue | 21 +- .../config/controls/parameter-time.vue | 78 ++--- .../config/controls/thing-picker.vue | 10 +- .../config/controls/triggerchannel-picker.vue | 4 +- .../components/rule/action-module-wizard.vue | 265 +++++++++++++++ .../rule/condition-module-wizard.vue | 228 +++++++++++++ .../components/rule/module-wizard-mixin.js | 74 +++++ .../components/rule/trigger-module-wizard.vue | 312 ++++++++++++++++++ .../rules/module-description-suggestions.js | 17 +- .../src/pages/settings/rules/rule-edit.vue | 38 ++- .../settings/rules/rule-module-popup.vue | 62 ++-- .../settings/rules/script/script-edit.vue | 2 +- .../web/src/res/img/blockly.svg | 76 +++++ 14 files changed, 1107 insertions(+), 84 deletions(-) create mode 100644 bundles/org.openhab.ui/web/src/components/rule/action-module-wizard.vue create mode 100644 bundles/org.openhab.ui/web/src/components/rule/condition-module-wizard.vue create mode 100644 bundles/org.openhab.ui/web/src/components/rule/module-wizard-mixin.js create mode 100644 bundles/org.openhab.ui/web/src/components/rule/trigger-module-wizard.vue create mode 100644 bundles/org.openhab.ui/web/src/res/img/blockly.svg diff --git a/bundles/org.openhab.ui/web/src/components/config/controls/item-picker.vue b/bundles/org.openhab.ui/web/src/components/config/controls/item-picker.vue index e1c112cde..b3351e230 100644 --- a/bundles/org.openhab.ui/web/src/components/config/controls/item-picker.vue +++ b/bundles/org.openhab.ui/web/src/components/config/controls/item-picker.vue @@ -18,6 +18,8 @@ + + diff --git a/bundles/org.openhab.ui/web/src/components/rule/condition-module-wizard.vue b/bundles/org.openhab.ui/web/src/components/rule/condition-module-wizard.vue new file mode 100644 index 000000000..c684d78e7 --- /dev/null +++ b/bundles/org.openhab.ui/web/src/components/rule/condition-module-wizard.vue @@ -0,0 +1,228 @@ + + + + + diff --git a/bundles/org.openhab.ui/web/src/components/rule/module-wizard-mixin.js b/bundles/org.openhab.ui/web/src/components/rule/module-wizard-mixin.js new file mode 100644 index 000000000..dd6ed1607 --- /dev/null +++ b/bundles/org.openhab.ui/web/src/components/rule/module-wizard-mixin.js @@ -0,0 +1,74 @@ +import ModelPickerPopup from '@/components/model/model-picker-popup.vue' + +export default { + data () { + return { + category: '', + currentItem: null + } + }, + computed: { + commandSuggestions () { + if (!this.currentItem || this.category !== 'item') return [] + let type = (this.currentItem.type === 'Group' && this.currentItem.groupType) ? this.currentItem.groupType : this.currentItem.type + + if (this.currentItem.commandDescription && this.currentItem.commandDescription.commandOptions) { + return this.currentItem.commandDescription.commandOptions + } + if (type === 'Switch') { + return ['ON', 'OFF'].map((c) => { return { command: c, label: c } }) + } + if (type === 'Rollershutter') { + return ['UP', 'DOWN', 'STOP'].map((c) => { return { command: c, label: c } }) + } + if (type === 'Color') { + return ['ON', 'OFF'].map((c) => { return { command: c, label: c } }) + } + + return ['ON', 'OFF'].map((c) => { return { command: c, label: c } }) + }, + stateSuggestions () { + if (!this.currentItem || this.category !== 'item') return [] + let type = (this.currentItem.type === 'Group' && this.currentItem.groupType) ? this.currentItem.groupType : this.currentItem.type + + if (this.currentItem.stateDescription && this.currentItem.stateDescription.options) { + return this.currentItem.stateDescription.options + } + if (type === 'Switch') { + return ['ON', 'OFF'].map((c) => { return { value: c, label: c } }) + } + if (type === 'Rollershutter') { + return ['UP', 'DOWN', 'STOP'].map((c) => { return { value: c, label: c } }) + } + if (type === 'Contact') { + return ['OPEN', 'CLOSED'].map((c) => { return { value: c, label: c } }) + } + + return ['ON', 'OFF'].map((c) => { return { value: c, label: c } }) + } + }, + methods: { + openModelPicker () { + const popup = { + component: ModelPickerPopup + } + + this.$f7router.navigate({ + url: 'pick-from-model', + route: { + path: 'pick-from-model', + popup + } + }, { + props: { + multiple: false + } + }) + + this.$f7.once('itemsPicked', this.itemPicked) + this.$f7.once('modelPickerClosed', () => { + this.$f7.off('itemsPicked', this.itemPicked) + }) + } + } +} diff --git a/bundles/org.openhab.ui/web/src/components/rule/trigger-module-wizard.vue b/bundles/org.openhab.ui/web/src/components/rule/trigger-module-wizard.vue new file mode 100644 index 000000000..02bbdb672 --- /dev/null +++ b/bundles/org.openhab.ui/web/src/components/rule/trigger-module-wizard.vue @@ -0,0 +1,312 @@ + + + + + diff --git a/bundles/org.openhab.ui/web/src/pages/settings/rules/module-description-suggestions.js b/bundles/org.openhab.ui/web/src/pages/settings/rules/module-description-suggestions.js index c17ce6980..ae08b2b44 100644 --- a/bundles/org.openhab.ui/web/src/pages/settings/rules/module-description-suggestions.js +++ b/bundles/org.openhab.ui/web/src/pages/settings/rules/module-description-suggestions.js @@ -41,16 +41,28 @@ export default { case 'core.GroupCommandTrigger': if (!config.groupName && !config.command) return moduleType.label if (!config.command) return 'When a member of ' + config.groupName + ' received a command' - return 'When a member of ' + config.itemName + ' received command ' + config.command + return 'When a member of ' + config.groupName + ' received command ' + config.command case 'core.GroupStateUpdateTrigger': if (!config.groupName) return moduleType.label - return 'When ' + config.groupName + ' was updated' + + return 'When a member of ' + config.groupName + ' was updated' + ((config.state) ? ' to ' + config.state : '') case 'core.GroupStateChangeTrigger': if (!config.groupName) return moduleType.label return 'When a member of ' + config.groupName + ' changed' + ((config.previousState) ? ' from ' + config.previousState : '') + ((config.state) ? ' to ' + config.state : '') + case 'core.ThingStatusUpdateTrigger': + if (!config.thingUID) return moduleType.label + return 'When ' + config.thingUID + ' status was updated' + + ((config.status) ? ' to ' + config.status : '') + case 'core.ThingStatusChangeTrigger': + if (!config.thingUID) return moduleType.label + return 'When ' + config.thingUID + ' changed' + + ((config.previousStatus) ? ' from ' + config.previousStatus : '') + + ((config.status) ? ' to ' + config.status : '') + case 'core.SystemStartlevelTrigger': + if (config.startLevel === undefined) return moduleType.label + return 'When the system has reached start level ' + config.startLevel // actions case 'core.ItemCommandAction': if (!config.itemName || !config.command) return moduleType.label @@ -71,7 +83,6 @@ export default { case 'core.ItemStateCondition': if (!config.itemName || !config.operator || !config.state) return moduleType.label return 'If ' + config.itemName + ' ' + config.operator + ' ' + config.state - default: return moduleType.label } diff --git a/bundles/org.openhab.ui/web/src/pages/settings/rules/rule-edit.vue b/bundles/org.openhab.ui/web/src/pages/settings/rules/rule-edit.vue index 8ae7522c2..a60934b69 100644 --- a/bundles/org.openhab.ui/web/src/pages/settings/rules/rule-edit.vue +++ b/bundles/org.openhab.ui/web/src/pages/settings/rules/rule-edit.vue @@ -91,10 +91,11 @@ :title="mod.label || suggestedModuleTitle(mod, null, section)" :footer="mod.description || suggestedModuleDescription(mod, null, section)" v-for="mod in rule[section]" :key="mod.id" - :link="isEditable && !showModuleControls" @click.native="(ev) => editModule(ev, section, mod)" swipeout> + :link="isEditable && !showModuleControls" + @click.native="(ev) => editModule(ev, section, mod)" swipeout> - - + + Delete @@ -265,19 +266,19 @@ export default { }) }, save (stay) { - if (!this.isEditable) return + if (!this.isEditable) return Promise.reject() if (this.currentTab === 'code') { if (!this.fromYaml()) { - return + return Promise.reject() } } if (!this.rule.uid) { this.$f7.dialog.alert('Please give an ID to the rule') - return + return Promise.reject() } if (!this.rule.name) { this.$f7.dialog.alert('Please give a name to the rule') - return + return Promise.reject() } const promise = (this.createMode) ? this.$oh.api.postPlain('/rest/rules', JSON.stringify(this.rule), 'text/plain', 'application/json') @@ -401,7 +402,7 @@ export default { this.$f7.swipeout.open(swipeoutElement) } }, - editModule (ev, section, mod) { + editModule (ev, section, mod, force) { if (this.showModuleControls) return if (!this.isEditable) return let swipeoutElement = ev.target @@ -414,6 +415,15 @@ export default { this.currentModule = Object.assign({}, mod) this.currentModuleType = this.moduleTypes[section].find((m) => m.uid === mod.type) + if (mod.type && mod.type.indexOf('script') === 0 && !force) { + this.editScriptDirect(ev, mod) + return + } + if (mod.type && mod.type === 'timer.GenericCronTrigger' && !force) { + this.buildCronExpression(ev, mod) + return + } + const popup = { component: RuleModulePopup } @@ -487,8 +497,10 @@ export default { }) this.$f7.once('ruleModuleConfigUpdate', this.saveModule) + this.$f7.once('editNewScript', this.saveAndEditNewScript) this.$f7.once('ruleModuleConfigClosed', () => { this.$f7.off('ruleModuleConfigUpdate', this.saveModule) + this.$f7.off('editNewScript', this.saveAndEditNewScript) this.moduleConfigClosed() }) }, @@ -509,6 +521,12 @@ export default { this.$set(this.rule[this.currentSection], idx, updatedModule) } }, + saveAndEditNewScript (updatedModule) { + this.saveModule(updatedModule) + this.save().then(() => { + this.$f7router.navigate('/settings/rules/' + this.rule.uid + '/script/' + updatedModule.id, { transition: this.$theme.aurora ? 'f7-cover-v' : '' }) + }) + }, moduleConfigClosed () { this.currentModule = null this.currentModuleType = null @@ -519,9 +537,9 @@ export default { this.currentModuleType = mod.type this.scriptCode = mod.configuration.script - const updatePromise = (this.rule.editable) ? this.save() : Promise.resolve() + const updatePromise = (this.rule.editable || this.createMode) ? this.save() : Promise.resolve() updatePromise.then(() => { - this.$f7router.navigate('script/' + mod.id, { transition: this.$theme.aurora ? 'f7-cover-v' : '' }) + this.$f7router.navigate('/settings/rules/' + this.rule.uid + '/script/' + mod.id, { transition: this.$theme.aurora ? 'f7-cover-v' : '' }) }) }, buildCronExpression (ev, mod) { diff --git a/bundles/org.openhab.ui/web/src/pages/settings/rules/rule-module-popup.vue b/bundles/org.openhab.ui/web/src/pages/settings/rules/rule-module-popup.vue index 2f94ea531..bcb49ea17 100644 --- a/bundles/org.openhab.ui/web/src/pages/settings/rules/rule-module-popup.vue +++ b/bundles/org.openhab.ui/web/src/pages/settings/rules/rule-module-popup.vue @@ -22,23 +22,28 @@ - Tip: leave fields blank to set automatically to the suggested name and description. Clear + - Type of {{currentSection.replace(/.$/, '')}} - -
    - - -
-
- +
+ {{sectionLabels[currentSection][0]}} + +
    + + +
+
+ + + +
+ - Configuration - + Configuration + import ConfigSheet from '@/components/config/config-sheet.vue' - +import TriggerModuleWizard from '@/components/rule/trigger-module-wizard.vue' +import ConditionModuleWizard from '@/components/rule/condition-module-wizard.vue' +import ActionModuleWizard from '@/components/rule/action-module-wizard.vue' import ModuleDescriptionSuggestions from './module-description-suggestions' export default { mixins: [ModuleDescriptionSuggestions], components: { + TriggerModuleWizard, + ConditionModuleWizard, + ActionModuleWizard, ConfigSheet }, props: ['rule', 'ruleModule', 'ruleModuleType', 'moduleTypes', 'currentSection'], data () { return { currentRuleModuleType: this.ruleModuleType, + advancedTypePicker: false, sectionLabels: { triggers: ['When', 'Add Trigger'], actions: ['Then', 'Add Action'], @@ -85,23 +96,34 @@ export default { } }, methods: { - setModuleType (moduleType) { + setModuleType (val, clearConfig) { + const moduleType = (typeof val === 'string') ? this.moduleTypes[this.currentSection].find((t) => t.uid === val) : val this.ruleModule.type = moduleType.uid this.$set(this, 'currentRuleModuleType', moduleType) - this.$set(this.ruleModule, 'configuration', {}) + if (clearConfig) this.$set(this.ruleModule, 'configuration', {}) this.ruleModule.label = this.ruleModule.description = '' }, moduleConfigClosed () { this.$f7.emit('ruleModuleConfigClosed') }, updateModuleConfig () { - if (!this.$refs.parameters.isValid()) { + if (this.$refs.parameters && !this.$refs.parameters.isValid()) { this.$f7.dialog.alert('Please review the configuration and correct validation errors') return } this.$f7.emit('ruleModuleConfigUpdate', this.ruleModule) this.$refs.modulePopup.close() }, + startScripting (language) { + const contentType = (language === 'blockly') ? 'application/javascript' : language + this.$set(this.ruleModule.configuration, 'type', contentType) + if (language === 'blockly') { + // initialize an empty blockly source + this.$set(this.ruleModule.configuration, 'blockSource', '') + } + this.$f7.emit('editNewScript', this.ruleModule) + this.$refs.modulePopup.close() + }, groupedModuleTypes (section) { const moduleTypes = this.moduleTypes[section].filter((t) => t.visibility === 'VISIBLE') let moduleTypesByScope = moduleTypes.reduce((prev, type, i, types) => { diff --git a/bundles/org.openhab.ui/web/src/pages/settings/rules/script/script-edit.vue b/bundles/org.openhab.ui/web/src/pages/settings/rules/script/script-edit.vue index 92fe0aba8..2535ad453 100644 --- a/bundles/org.openhab.ui/web/src/pages/settings/rules/script/script-edit.vue +++ b/bundles/org.openhab.ui/web/src/pages/settings/rules/script/script-edit.vue @@ -55,7 +55,7 @@ - + diff --git a/bundles/org.openhab.ui/web/src/res/img/blockly.svg b/bundles/org.openhab.ui/web/src/res/img/blockly.svg new file mode 100644 index 000000000..4616ee99f --- /dev/null +++ b/bundles/org.openhab.ui/web/src/res/img/blockly.svg @@ -0,0 +1,76 @@ + + + + + + image/svg+xml + + logo-only + + + + + + + + logo-only + + + + +