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 <github@schaus.net>
pull/631/head
Yannick Schaus 2020-12-12 21:46:10 +01:00 committed by GitHub
parent 9ea63a0085
commit 016e3b091d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1107 additions and 84 deletions

View File

@ -18,6 +18,8 @@
<style lang="stylus">
.item-picker-container
.item-content
padding-left calc(var(--f7-list-item-padding-horizontal)/2 + var(--f7-safe-area-left))
.item-media
padding 0
.item-inner:after
@ -67,12 +69,14 @@ export default {
this.$f7.input.validateInputs(this.$refs.smartSelect.$el)
const value = this.$refs.smartSelect.f7SmartSelect.getValue()
this.$emit('input', value)
if (!this.multiple) this.$emit('itemSelected', this.items.find((i) => i.name === value))
},
updateFromModelPicker (value) {
if (this.multiple) {
this.$emit('input', value.map((i) => i.name))
} else {
this.$emit('input', value.name)
this.$emit('itemSelected', value)
}
this.ready = false
this.$nextTick(() => { this.ready = true })

View File

@ -1,11 +1,7 @@
<template>
<ul>
<f7-list-item
:title="configDescription.label" smart-select :smart-select-params="{ view: $f7.views.main, openIn: 'popover', multiple: configDescription.multiple, closeOnSelect: !configDescription.multiple }" ref="item">
<select :name="configDescription.name" @change="updateValue" :multiple="configDescription.multiple">
<option v-for="(day, $idx) in values" :value="day" :key="day" :selected="isSelected(day)">{{labels[$idx]}}</option>
</select>
</f7-list-item>
<f7-list-item v-for="(day, $idx) in values" :value="day" :key="day"
:title="labels[$idx]" checkbox :checked="isSelected(day)" @change="(evt) => select(day, evt.target.checked)" />
</ul>
</template>
@ -19,10 +15,6 @@ export default {
}
},
methods: {
updateValue (event) {
let value = this.$refs.item.f7SmartSelect.getValue()
this.$emit('input', value)
},
isSelected (option) {
if (this.value === null || this.value === undefined) return
if (!this.configDescription.multiple) {
@ -30,6 +22,15 @@ export default {
} else {
return this.value && this.value.indexOf(option) >= 0
}
},
select (day, value) {
const newValuesSet = (this.value) ? new Set([...this.value]) : new Set()
if (value) newValuesSet.add(day)
if (!value) newValuesSet.delete(day)
let newValues = new Array(...newValuesSet).sort((a, b) => this.values.indexOf(a) < this.values.indexOf(b))
newValues.sort((a, b) => this.values.indexOf(a) - this.values.indexOf(b))
console.log(newValues)
this.$emit('input', newValues)
}
}
}

View File

@ -9,7 +9,9 @@
:required="configDescription.required" validate
:clear-button="!configDescription.required"
@input="updateValue" />
<div slot="content-end" ref="picker" />
<div slot="content-end" class="display-flex justify-content-center">
<div ref="picker"></div>
</div>
</ul>
</template>
@ -27,44 +29,46 @@ export default {
const containerControl = this.$refs.picker
if (!inputControl || !inputControl.$el || !containerControl) return
const inputElement = this.$$(inputControl.$el).find('input')
this.picker = this.$f7.picker.create({
containerEl: containerControl,
inputEl: inputElement,
toolbar: false,
inputReadOnly: false,
rotateEffect: true,
value: (self.value && self.value.indexOf(':') >= 0) ? self.value.split(':') : ['00', '00'],
formatValue: function (values, displayValues) {
return values[0] + ':' + values[1]
},
cols: [
// Hours
{
values: (function () {
var arr = []
for (var i = 0; i <= 23; i++) { arr.push(i < 10 ? `0${i}` : i) }
return arr
})()
this.$nextTick(() => {
this.picker = this.$f7.picker.create({
containerEl: containerControl,
inputEl: inputElement,
toolbar: false,
inputReadOnly: false,
rotateEffect: true,
value: (self.value && self.value.indexOf(':') >= 0) ? self.value.split(':') : ['00', '00'],
formatValue: function (values, displayValues) {
return values[0] + ':' + values[1]
},
// Divider
{
divider: true,
content: ':'
},
// Minutes
{
values: (function () {
var arr = []
for (var i = 0; i <= 59; i++) { arr.push(i < 10 ? `0${i}` : i) }
return arr
})()
cols: [
// Hours
{
values: (function () {
var arr = []
for (var i = 0; i <= 23; i++) { arr.push(i < 10 ? `0${i}` : i) }
return arr
})()
},
// Divider
{
divider: true,
content: ':'
},
// Minutes
{
values: (function () {
var arr = []
for (var i = 0; i <= 59; i++) { arr.push(i < 10 ? `0${i}` : i) }
return arr
})()
}
],
on: {
change: function (picker, values, displayValues) {
self.$emit('input', displayValues[0] + ':' + displayValues[1])
}
}
],
on: {
change: function (picker, values, displayValues) {
self.$emit('input', displayValues[0] + ':' + displayValues[1])
}
}
})
})
},
beforeDestroy () {

View File

@ -15,7 +15,7 @@
<script>
export default {
props: ['title', 'name', 'value', 'multiple', 'required', 'filterType', 'filterUid'],
props: ['title', 'name', 'value', 'multiple', 'required', 'filterType', 'filterUid', 'openOnReady'],
data () {
return {
ready: false,
@ -51,9 +51,17 @@ export default {
this.things = this.things.filter((t) => this.filterUid.indexOf(t.UID) >= 0)
}
this.ready = true
if (this.openOnReady) {
this.$nextTick(() => {
this.$refs.smartSelect.f7SmartSelect.open()
})
}
})
},
methods: {
open () {
this.$refs.smartSelect.f7SmartSelect.open()
},
select (e) {
this.$f7.input.validateInputs(this.$refs.smartSelect.$el)
this.$emit('input', e.target.value)

View File

@ -3,7 +3,7 @@
<f7-list-item :title="title || 'Thing'" smart-select :smart-select-params="smartSelectParams" v-if="ready" ref="smartSelect">
<select :name="name" :multiple="multiple" @change="select" :required="required">
<option v-if="!multiple" value=""></option>
<optgroup v-for="thing in things" :label="thing.label" :key="thing.UID">
<optgroup v-for="thing in things.filter((t) => (filterThing) ? t.UID === filterThing : true)" :label="thing.label" :key="thing.UID">
<option v-for="channel in thing.triggerChannels" :value="channel.uid" :key="channel.uid" :selected="(multiple) ? value.indexOf(channel.uid) >= 0 : value === channel.uid">
{{channel.id}} ({{channel.label}})
</option>
@ -17,7 +17,7 @@
<script>
export default {
props: ['title', 'name', 'value', 'multiple', 'required', 'filterType'],
props: ['title', 'name', 'value', 'multiple', 'required', 'filterThing'],
data () {
return {
ready: false,

View File

@ -0,0 +1,265 @@
<template>
<f7-block v-if="!category">
<f7-row class="margin-bottom">
<f7-col class="elevation-2 elevation-hover-6 elevation-pressed-1 triggertype-big-button" width="50">
<f7-link class="display-flex flex-direction-column no-ripple" no-ripple @click="chooseItemCategory">
<f7-icon size="35" f7="square_on_circle" class="margin" />
Item<br />Action
</f7-link>
</f7-col>
<f7-col class="elevation-2 elevation-hover-6 elevation-pressed-1 triggertype-big-button" width="50">
<f7-link class="display-flex flex-direction-column no-ripple" no-ripple @click="chooseScriptCategory">
<f7-icon size="35" f7="doc_plaintext" class="margin" />
Run<br />Script
</f7-link>
</f7-col>
</f7-row>
<f7-row class="margin-bottom">
<f7-col class="elevation-2 elevation-hover-6 elevation-pressed-1 triggertype-big-button" width="50">
<f7-link class="display-flex flex-direction-column no-ripple" no-ripple @click="chooseRulesCategory">
<f7-icon size="35" f7="wand_stars" class="margin" />
Other<br />Rules
</f7-link>
</f7-col>
<f7-col class="elevation-2 elevation-hover-6 elevation-pressed-1 triggertype-big-button" width="50">
<f7-link class="display-flex flex-direction-column no-ripple" no-ripple @click="chooseMediaCategory">
<f7-icon size="35" f7="music_note_list" class="margin" />
Audio &amp;<br />Voice
</f7-link>
</f7-col>
</f7-row>
<f7-list>
<f7-list-button title="Show All" color="blue" @click="$emit('showAdvanced')"></f7-list-button>
</f7-list>
</f7-block>
<f7-block class="no-margin no-padding" v-else-if="category === 'item'">
<f7-list>
<item-picker :value="currentModule.configuration.itemName" title="Item" @input="(val) => $set(currentModule.configuration, 'itemName', val)" @itemSelected="(value) => { $set(this, 'currentItem', value); updateItemEventType('command') }" />
</f7-list>
<f7-list>
<f7-list-input
label="Command to send"
name="command"
type="text"
:value="currentModule.configuration.command"
@blur="(evt) => $set(currentModule.configuration, 'command', evt.target.value)"
/>
</f7-list>
<f7-list v-if="commandSuggestions.length">
<f7-list-item radio :checked="currentModule.configuration.command === suggestion.command" v-for="suggestion in commandSuggestions" :key="suggestion.command"
:title="suggestion.label" @click="$set(currentModule.configuration, 'command', suggestion.command)" />
</f7-list>
<f7-block v-if="currentItem && (currentItem.type === 'Dimmer' || currentItem.type === 'Rollershutter' || (currentItem.type === 'Number' && currentItem.stateDescription && currentItem.stateDescription.minimum !== undefined))">
<f7-range :value="currentModule.configuration.command" @range:changed="(val) => $set(currentModule.configuration, 'command', val)"
:min="(currentItem.stateDescription && currentItem.stateDescription.minimum) ? currentItem.stateDescription.minimum : 0"
:max="(currentItem.stateDescription && currentItem.stateDescription.maximum) ? currentItem.stateDescription.maximum : 100"
:step="(currentItem.stateDescription && currentItem.stateDescription.step) ? currentItem.stateDescription.step : 1"
:scale="true" :label="true" :scaleSubSteps="5" />
</f7-block>
<f7-list v-if="currentItem && currentItem.type === 'Color'" media-list>
<f7-list-input media-item type="colorpicker" label="Pick a color" :color-picker-params="{
targetEl: '#color-picker-value',
targetElSetBackgroundColor: true,
openIn: 'auto',
modules: ['hsb-sliders', 'wheel', 'palette'],
sliderValue: true,
sliderValueEditable: true,
sliderLabel: true,
formatValue: colorToCommand
}"
:value="commandToColor()"
@change="updateColorCommand"
>
<i slot="media" style="width: 32px; height: 32px" class="icon demo-list-icon" id="color-picker-value"></i>
</f7-list-input>
</f7-list>
</f7-block>
<f7-block class="no-margin no-padding" v-else-if="category === 'script'">
<f7-block-title class="padding-horizontal">Run a script</f7-block-title>
<f7-list media-list>
<f7-list-item media-item
title="Design with Blockly"
footer="A beginner-friendly way to build scripts visually by assembling blocks"
link="" @click="scriptLanguagePicked('blockly')">
<img src="res/img/blockly.svg" height="32" width="32" slot="media" />
</f7-list-item>
</f7-list>
<f7-block-footer class="padding-horizontal margin-vertical">or choose the scripting language:</f7-block-footer>
<f7-list media-list>
<f7-list-item media-item v-for="language in languages" :key="language.contentType"
:title="language.name" :after="language.version" :footer="language.contentType" link="" @click="scriptLanguagePicked(language.contentType)">
<span slot="media" class="item-initial">{{language.name[0]}}</span>
</f7-list-item>
</f7-list>
<f7-block-footer class="padding-horizontal margin-bottom"><small><strong>Note:</strong> Creating a new scripted module will <em>save the rule</em> before launching the script editor.</small></f7-block-footer>
</f7-block>
<f7-block class="no-margin no-padding" v-else-if="category === 'rules'">
<f7-list>
<f7-list-item radio :checked="rulesEventType === 'run'" name="rulesEventType" title="run these rule(s)" @click="updateRulesEventType('run')" />
<f7-list-item radio :checked="rulesEventType === 'enable'" name="rulesEventType" title="enable or disable these rule(s)" @click="updateRulesEventType('enable')" />
</f7-list>
<config-sheet v-if="currentModuleType" :key="currentModule.id"
:parameterGroups="[]"
:parameters="currentModuleType.configDescriptions"
:configuration="currentModule.configuration"
@updated="dirty = true"
/>
</f7-block>
<f7-block class="no-margin no-padding" v-else-if="category === 'media'">
<f7-list>
<f7-list-item radio :checked="mediaEventType === 'say'" name="MediaEventType" title="say something" @click="updateMediaEventType('say')" />
<f7-list-item radio :checked="mediaEventType === 'play'" name="MediaEventType" title="play an audio file" @click="updateMediaEventType('play')" />
</f7-list>
<config-sheet v-if="currentModuleType" :key="currentModule.id"
:parameterGroups="[]"
:parameters="currentModuleType.configDescriptions"
:configuration="currentModule.configuration"
@updated="dirty = true"
/>
</f7-block>
</template>
<style lang="stylus">
.triggertype-big-button
background var(--f7-card-bg-color)
text-align center
height 7.5rem
.link
color var(--f7-text-color)
</style>
<script>
import ModuleWizard from './module-wizard-mixin'
import ItemPicker from '@/components/config/controls/item-picker.vue'
import ConfigSheet from '@/components/config/config-sheet.vue'
export default {
mixins: [ModuleWizard],
props: ['currentModule', 'currentModuleType'],
components: {
ItemPicker,
ConfigSheet
},
data () {
return {
category: '',
itemEventType: 'command',
rulesEventType: 'cron',
mediaEventType: 'say',
languages: [],
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 === 'Contact') {
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 []
}
},
methods: {
chooseItemCategory () {
this.openModelPicker()
},
chooseScriptCategory () {
this.category = 'script'
this.$emit('typeSelect', 'script.ScriptAction')
this.$nextTick(() => {
this.$set(this, 'languages', this.currentModuleType.configDescriptions
.find((c) => c.name === 'type').options
.map((l) => {
return {
contentType: l.value,
name: l.label.split(' (')[0],
version: l.label.split(' (')[1].replace(')', '')
}
}))
})
},
chooseRulesCategory () {
this.category = 'rules'
this.updateRulesEventType('run')
},
chooseMediaCategory () {
this.category = 'media'
this.updateMediaEventType('say')
},
updateItemEventType (type) {
this.itemEventType = type
switch (type) {
case 'command':
this.$emit('typeSelect', 'core.ItemCommandAction')
break
}
},
updateRulesEventType (type) {
this.rulesEventType = type
switch (type) {
case 'run':
this.$emit('typeSelect', 'core.RunRuleAction')
break
case 'enable':
this.$emit('typeSelect', 'core.RuleEnablementAction')
break
}
},
updateMediaEventType (type) {
this.mediaEventType = type
switch (type) {
case 'say':
this.$emit('typeSelect', 'media.SayAction')
break
case 'play':
this.$emit('typeSelect', 'media.PlayAction')
break
}
},
updateColorCommand (evt) {
this.$set(this.currentModule.configuration, 'command', evt.target.value)
},
commandToColor (evt) {
if (!this.currentModule.configuration.command || this.currentModule.configuration.command.split(',').length !== 3) return null
let color = this.currentModule.configuration.command.split(',')
color[0] = parseInt(color[0])
color[1] = color[1] / 100
color[2] = color[2] / 100
return { hsb: color }
},
colorToCommand (val) {
let hsb = [...val.hsb]
hsb[0] = Math.round(hsb[0]) % 360
hsb[1] = Math.round(hsb[1] * 100)
hsb[2] = Math.round(hsb[2] * 100)
return hsb
// this.$set(this.currentModule.configuration, 'command', hsb.join(','))
},
scriptLanguagePicked (value) {
this.$emit('startScript', value)
},
itemPicked (value) {
this.category = 'item'
this.currentItem = value
this.$set(this.currentModule.configuration, 'itemName', value.name)
this.$emit('typeSelect', 'core.ItemCommandAction')
}
}
}
</script>

View File

@ -0,0 +1,228 @@
<template>
<f7-block v-if="!category">
<f7-row class="margin-bottom">
<f7-col class="elevation-2 elevation-hover-6 elevation-pressed-1 triggertype-big-button" width="50">
<f7-link class="display-flex flex-direction-column no-ripple" no-ripple @click="chooseItemCategory">
<f7-icon size="35" f7="square_on_circle" class="margin" />
Item<br />Condition
</f7-link>
</f7-col>
<f7-col class="elevation-2 elevation-hover-6 elevation-pressed-1 triggertype-big-button" width="50">
<f7-link class="display-flex flex-direction-column no-ripple" no-ripple @click="chooseScriptCategory">
<f7-icon size="35" f7="doc_plaintext" class="margin" />
Script<br />Condition
</f7-link>
</f7-col>
</f7-row>
<f7-row class="margin-bottom">
<f7-col class="elevation-2 elevation-hover-6 elevation-pressed-1 triggertype-big-button" width="50">
<f7-link class="display-flex flex-direction-column no-ripple" no-ripple @click="chooseTimeCategory">
<f7-icon size="35" f7="clock" class="margin" />
Time<br />Condition
</f7-link>
</f7-col>
<f7-col class="elevation-2 elevation-hover-6 elevation-pressed-1 triggertype-big-button" width="50">
<f7-link class="display-flex flex-direction-column no-ripple" no-ripple @click="chooseEphemerisCategory">
<f7-icon size="35" f7="calendar_today" class="margin" />
Ephemeris<br />Schedule
</f7-link>
</f7-col>
</f7-row>
<f7-list>
<f7-list-button title="Show All" color="blue" @click="$emit('showAdvanced')"></f7-list-button>
</f7-list>
</f7-block>
<f7-block class="no-margin no-padding" v-else-if="category === 'item'">
<f7-list>
<item-picker :value="currentModule.configuration.itemName" title="Item" @input="(val) => $set(currentModule.configuration, 'itemName', val)" />
</f7-list>
<f7-list>
<f7-list-item radio
v-for="operator in operators" :key="operator.value"
:title="operator.label"
name="itemStateOperator"
:checked="currentModule.configuration.operator === operator.value"
@click="$set(currentModule.configuration, 'operator', operator.value)" />
<f7-list-input
label="State"
name="itemState"
type="text"
:value="currentModule.configuration.state"
@blur="(evt) => $set(currentModule.configuration, 'state', evt.target.value)"
/>
</f7-list>
<f7-list v-if="stateSuggestions.length">
<f7-list-item radio :checked="currentModule.configuration.state === suggestion.value" v-for="suggestion in stateSuggestions" :key="suggestion.value"
:title="suggestion.label" @click="$set(currentModule.configuration, 'state', suggestion.value)" />
</f7-list>
</f7-block>
<f7-block class="no-margin no-padding" v-else-if="category === 'script'">
<f7-block-title class="padding-horizontal">A script evaluates to true</f7-block-title>
<f7-list media-list>
<f7-list-item media-item
title="Design with Blockly"
footer="A beginner-friendly way to build scripts visually by assembling blocks"
link="" @click="scriptLanguagePicked('blockly')">
<img src="res/img/blockly.svg" height="32" width="32" slot="media" />
</f7-list-item>
</f7-list>
<f7-block-footer class="padding-horizontal margin-vertical">or choose the scripting language:</f7-block-footer>
<f7-list media-list>
<f7-list-item media-item v-for="language in languages" :key="language.contentType"
:title="language.name" :after="language.version" :footer="language.contentType" link="" @click="scriptLanguagePicked(language.contentType)">
<span slot="media" class="item-initial">{{language.name[0]}}</span>
</f7-list-item>
</f7-list>
<f7-block-footer class="padding-horizontal margin-bottom"><small><strong>Note:</strong> Creating a new scripted module will <em>save the rule</em> before launching the script editor.</small></f7-block-footer>
</f7-block>
<f7-block class="no-margin no-padding" v-else-if="category === 'time'">
<f7-list>
<f7-list-item radio :checked="timeEventType === 'dayOfWeek'" name="timeEventType" title="the current day of the week is" @click="updateTimeEventType('dayOfWeek')" />
<f7-list-item radio :checked="timeEventType === 'timeOfDay'" name="timeEventType" title="inside a time range" @click="updateTimeEventType('timeOfDay')" />
</f7-list>
<config-sheet v-if="currentModuleType" :key="currentModule.id"
:parameterGroups="[]"
:parameters="currentModuleType.configDescriptions"
:configuration="currentModule.configuration"
@updated="dirty = true"
/>
</f7-block>
<f7-block class="no-margin no-padding" v-else-if="category === 'ephemeris'">
<f7-list>
<f7-list-item radio :checked="ephemerisEventType === 'weekdays'" name="EphemerisEventType" title="it's a weekday" @click="updateEphemerisEventType('weekdays')" />
<f7-list-item radio :checked="ephemerisEventType === 'weekends'" name="EphemerisEventType" title="it's the weekend" @click="updateEphemerisEventType('weekends')" />
<f7-list-item radio :checked="ephemerisEventType === 'holidays'" name="EphemerisEventType" title="it's a holiday" @click="updateEphemerisEventType('holidays')" />
<f7-list-item radio :checked="ephemerisEventType === 'dayset'" name="EphemerisEventType" title="today is in a specific dayset" @click="updateEphemerisEventType('dayset')" />
</f7-list>
<f7-block-footer class="padding-horizontal">Remember to configure Ephemeris in Settings before using these conditions.</f7-block-footer>
<config-sheet v-if="currentModuleType" :key="currentModule.id"
:parameterGroups="[]"
:parameters="currentModuleType.configDescriptions"
:configuration="currentModule.configuration"
@updated="dirty = true"
/>
</f7-block>
</template>
<style lang="stylus">
.triggertype-big-button
background var(--f7-card-bg-color)
text-align center
height 7.5rem
.link
color var(--f7-text-color)
</style>
<script>
import ModuleWizard from './module-wizard-mixin'
import ItemPicker from '@/components/config/controls/item-picker.vue'
import ConfigSheet from '@/components/config/config-sheet.vue'
export default {
mixins: [ModuleWizard],
props: ['currentModule', 'currentModuleType'],
components: {
ItemPicker,
ConfigSheet
},
data () {
return {
category: '',
itemEventType: 'state',
thingEventType: 'triggerChannelFired',
timeEventType: 'cron',
ephemerisEventType: 'weekdays',
languages: [],
operators: [
{ value: '=', label: 'is equal to' },
{ value: '!=', label: 'is different than' },
{ value: '>', label: 'is greater than' },
{ value: '>=', label: 'is greater or equal to' },
{ value: '<', label: 'is less than' },
{ value: '<=', label: 'is less or equal to' }
]
}
},
methods: {
chooseItemCategory () {
this.openModelPicker()
},
chooseScriptCategory () {
this.category = 'script'
this.$emit('typeSelect', 'script.ScriptCondition')
this.$nextTick(() => {
this.$set(this, 'languages', this.currentModuleType.configDescriptions
.find((c) => c.name === 'type').options
.map((l) => {
return {
contentType: l.value,
name: l.label.split(' (')[0],
version: l.label.split(' (')[1].replace(')', '')
}
}))
})
},
chooseTimeCategory () {
this.category = 'time'
this.updateTimeEventType('dayOfWeek')
},
chooseEphemerisCategory () {
this.category = 'ephemeris'
this.updateEphemerisEventType('weekdays')
},
updateItemEventType (type) {
this.itemEventType = type
switch (type) {
case 'command':
this.$emit('typeSelect', 'core.ItemCommandTrigger')
break
case 'updated':
this.$emit('typeSelect', 'core.ItemStateUpdateTrigger')
break
case 'changed':
this.$emit('typeSelect', 'core.ItemStateChangeTrigger')
break
}
},
updateTimeEventType (type) {
this.timeEventType = type
switch (type) {
case 'dayOfWeek':
this.$emit('typeSelect', 'timer.DayOfWeekCondition')
break
case 'timeOfDay':
this.$emit('typeSelect', 'core.TimeOfDayCondition')
break
}
},
updateEphemerisEventType (type) {
this.ephemerisEventType = type
switch (type) {
case 'weekdays':
this.$emit('typeSelect', 'ephemeris.WeekdayCondition')
break
case 'weekends':
this.$emit('typeSelect', 'ephemeris.WeekendCondition')
break
case 'holidays':
this.$emit('typeSelect', 'ephemeris.HolidayCondition')
break
case 'dayset':
this.$emit('typeSelect', 'ephemeris.DaysetCondition')
break
}
},
scriptLanguagePicked (value) {
this.$emit('startScript', value)
},
itemPicked (value) {
this.category = 'item'
this.currentItem = value
this.$set(this.currentModule.configuration, 'itemName', value.name)
this.$set(this.currentModule.configuration, 'operator', '=')
this.$emit('typeSelect', 'core.ItemStateCondition')
}
}
}
</script>

View File

@ -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)
})
}
}
}

View File

@ -0,0 +1,312 @@
<template>
<f7-block v-if="!category">
<f7-row class="margin-bottom">
<f7-col class="elevation-2 elevation-hover-6 elevation-pressed-1 triggertype-big-button" width="50">
<f7-link class="display-flex flex-direction-column no-ripple" no-ripple @click="chooseItemCategory">
<f7-icon size="35" f7="square_on_circle" class="margin" />
Item<br />Event
</f7-link>
</f7-col>
<f7-col class="elevation-2 elevation-hover-6 elevation-pressed-1 triggertype-big-button" width="50">
<f7-link class="display-flex flex-direction-column no-ripple" no-ripple @click="chooseThingCategory">
<f7-icon size="35" f7="lightbulb" class="margin" />
Thing<br />Event
</f7-link>
</f7-col>
</f7-row>
<f7-row class="margin-bottom">
<f7-col class="elevation-2 elevation-hover-6 elevation-pressed-1 triggertype-big-button" width="50">
<f7-link class="display-flex flex-direction-column no-ripple" no-ripple @click="chooseTimeCategory">
<f7-icon size="35" f7="clock" class="margin" />
Time<br />Event
</f7-link>
</f7-col>
<f7-col class="elevation-2 elevation-hover-6 elevation-pressed-1 triggertype-big-button" width="50">
<f7-link class="display-flex flex-direction-column no-ripple" no-ripple @click="chooseSystemCategory">
<f7-icon size="35" f7="gear" class="margin" />
System<br />Event
</f7-link>
</f7-col>
</f7-row>
<f7-list>
<f7-list-button title="Show All" color="blue" @click="$emit('showAdvanced')"></f7-list-button>
</f7-list>
</f7-block>
<f7-block class="no-margin no-padding" v-else-if="category === 'item'">
<f7-list>
<item-picker :required="true" :value="currentItem.name" title="Item" @input="(val) => $set(currentModule.configuration, 'itemName', val)" @itemSelected="(value) => { currentItem = value; updateItemEventType('command') }" />
</f7-list>
<f7-list>
<f7-list-item radio :checked="itemEventType === 'command'" name="itemEventType" title="received a command" @click="updateItemEventType('command')" />
<f7-list-item radio :checked="itemEventType === 'updated'" name="itemEventType" title="was updated" @click="updateItemEventType('updated')" />
<f7-list-item radio :checked="itemEventType === 'changed'" name="itemEventType" title="changed" @click="updateItemEventType('changed')" />
<f7-list-item radio v-if="currentItem && currentItem.type === 'Group'" :checked="itemEventType === 'memberCommand'" name="itemEventType" title="had a member receive a command" @click="updateItemEventType('memberCommand')" />
<f7-list-item radio v-if="currentItem && currentItem.type === 'Group'" :checked="itemEventType === 'memberUpdated'" name="itemEventType" title="had a member update" @click="updateItemEventType('memberUpdated')" />
<f7-list-item radio v-if="currentItem && currentItem.type === 'Group'" :checked="itemEventType === 'memberChanged'" name="itemEventType" title="had a member change" @click="updateItemEventType('memberChanged')" />
</f7-list>
<f7-list>
<f7-list-input
v-if="itemEventType === 'command' || itemEventType === 'memberCommand'"
label="Command"
name="command"
type="text"
placeholder="Any"
:value="currentModule.configuration.command"
@blur="(evt) => $set(currentModule.configuration, 'command', evt.target.value)"
/>
<f7-list-input
v-if="itemEventType === 'updated' || itemEventType === 'memberUpdated'"
label="to state"
name="updatedState"
type="text"
placeholder="Any"
:value="currentModule.configuration.state"
@blur="(evt) => $set(currentModule.configuration, 'state', evt.target.value)"
/>
<f7-list-input
v-if="itemEventType === 'changed' || itemEventType === 'memberChanged'"
label="from state"
name="changedFromState"
type="text"
placeholder="Any"
:value="currentModule.configuration.previousState"
@blur="(evt) => $set(currentModule.configuration, 'previousState', evt.target.value)"
/>
<f7-list-input
v-if="itemEventType === 'changed' || itemEventType === 'memberChanged'"
label="to state"
name="changedToState"
type="text"
placeholder="Any"
:value="currentModule.configuration.state"
@blur="(evt) => $set(currentModule.configuration, 'state', evt.target.value)"
/>
</f7-list>
<f7-list v-if="(itemEventType === 'command' || itemEventType === 'memberCommand') && commandSuggestions.length">
<f7-list-item radio :checked="currentModule.configuration.command === suggestion.command" v-for="suggestion in commandSuggestions" :key="suggestion.command"
:title="suggestion.label" @click="$set(currentModule.configuration, 'command', suggestion.command)" />
</f7-list>
<f7-list v-else-if="stateSuggestions.length">
<f7-list-item radio :checked="currentModule.configuration.state === suggestion.value" v-for="suggestion in stateSuggestions" :key="suggestion.value"
:title="suggestion.label" @click="$set(currentModule.configuration, 'state', suggestion.value)" />
</f7-list>
</f7-block>
<f7-block class="no-margin no-padding" v-else-if="category === 'thing'">
<f7-list>
<thing-picker ref="thingPicker" :value="currentModule.configuration.thingUID" title="Thing" @input="(val) => $set(currentModule.configuration, 'thingUID', val)" :open-on-ready="true" />
</f7-list>
<f7-list>
</f7-list>
<f7-list>
<f7-list-item radio :checked="thingEventType === 'triggerChannelFired'" name="thingEventType" title="a trigger channel fired" @click="updateThingEventType('triggerChannelFired')" />
<f7-list-item radio v-if="currentModule.configuration.thingUID" :checked="thingEventType === 'statusUpdated'" name="thingEventType" title="status was updated" @click="updateThingEventType('statusUpdated')" />
<f7-list-item radio v-if="currentModule.configuration.thingUID" :checked="thingEventType === 'statusChanged'" name="thingEventType" title="status changed" @click="updateThingEventType('statusChanged')" />
</f7-list>
<f7-list>
<f7-list-item
v-if="thingEventType === 'statusUpdated'"
title="to"
smart-select :smart-select-params="{ view: $f7.view.main, openIn: 'popover' }">
<select name="thingStatus" required @change="(evt) => $set(currentModule.configuration, 'status', evt.target.value)">
<option v-for="status in [{ value: '', label: '' }, ...currentModuleType.configDescriptions.find((p) => p.name === 'status').options]"
:value="status.value" :key="status.value"
:selected="currentModule.configuration.status === status.value">
{{status.label}}
</option>
</select>
</f7-list-item>
<f7-list-item
v-if="thingEventType === 'statusChanged'"
title="from"
smart-select :smart-select-params="{ view: $f7.view.main, openIn: 'popover' }">
<select name="thingStatus" required @change="(evt) => $set(currentModule.configuration, 'previousStatus', evt.target.value)">
<option v-for="status in [{ value: '', label: '' }, ...currentModuleType.configDescriptions.find((p) => p.name === 'previousStatus').options]"
:value="status.value" :key="status.value"
:selected="currentModule.configuration.previousStatus === status.value">
{{status.label}}
</option>
</select>
</f7-list-item>
<f7-list-item
v-if="thingEventType === 'statusChanged'"
title="to"
smart-select :smart-select-params="{ view: $f7.view.main, openIn: 'popover' }">
<select name="thingStatus" required @change="(evt) => $set(currentModule.configuration, 'status', evt.target.value)">
<option v-for="status in [{ value: '', label: '' }, ...currentModuleType.configDescriptions.find((p) => p.name === 'status').options]"
:value="status.value" :key="status.value"
:selected="currentModule.configuration.status === status.value">
{{status.label}}
</option>
</select>
</f7-list-item>
</f7-list>
<f7-list>
<trigger-channel-picker v-if="thingEventType === 'triggerChannelFired'" :value="currentModule.configuration.channelUID" title="Channel" @input="(val) => $set(currentModule.configuration, 'channelUID', val)" :filter-thing="currentModule.configuration.thingUID" />
</f7-list>
<f7-list>
<f7-list-input
v-if="thingEventType === 'triggerChannelFired'"
label="Event"
name="triggerChannelEvent"
type="text"
placeholder="Any"
:value="currentModule.configuration.event"
@blur="(evt) => $set(currentModule.configuration, 'event', evt.target.value)"
/>
</f7-list>
</f7-block>
<f7-block class="no-margin no-padding" v-else-if="category === 'time'">
<f7-list>
<f7-list-item radio :checked="timeEventType === 'cron'" name="timeEventType" title="on a schedule (cron)" @click="updateTimeEventType('cron')" />
<f7-list-item radio :checked="timeEventType === 'timeOfDay'" name="timeEventType" title="at a fixed time of the day" @click="updateTimeEventType('timeOfDay')" />
</f7-list>
<config-sheet v-if="currentModuleType" :key="currentSection + currentModule.id"
:parameterGroups="[]"
:parameters="currentModuleType.configDescriptions"
:configuration="currentModule.configuration"
@updated="dirty = true"
/>
</f7-block>
<f7-block class="no-margin no-padding" v-else-if="category === 'system'">
<f7-list>
<f7-list-item radio :checked="systemEventType === 'start'" name="systemEventType" title="the system is being initialized" @click="updateSystemEventType('start')" />
</f7-list>
<f7-block-footer class="padding-horizontal margin-vertical">and this start level has been reached:</f7-block-footer>
<f7-list v-if="systemEventType === 'start' && currentModule">
<f7-list-item radio :checked="currentModule.configuration.startLevel === 0" name="startLevel" title="00 - OSGi framework started" @click="$set(currentModule.configuration, 'startLevel', 0)" />
<f7-list-item radio :checked="currentModule.configuration.startLevel === 10" name="startLevel" title="10 - OSGi bundles activated" @click="$set(currentModule.configuration, 'startLevel', 10)" />
<f7-list-item radio :checked="currentModule.configuration.startLevel === 20" name="startLevel" title="20 - Entities (items, things...) loaded" @click="$set(currentModule.configuration, 'startLevel', 20)" />
<f7-list-item radio :checked="currentModule.configuration.startLevel === 30" name="startLevel" title="30 - Items states restored from persistence" @click="$set(currentModule.configuration, 'startLevel', 30)" />
<f7-list-item radio :checked="currentModule.configuration.startLevel === 40" name="startLevel" title="40 - Rules loaded" @click="$set(currentModule.configuration, 'startLevel', 40)" />
<f7-list-item radio :checked="currentModule.configuration.startLevel === 50" name="startLevel" title="50 - Rule engine ready" @click="$set(currentModule.configuration, 'startLevel', 50)" />
<f7-list-item radio :checked="currentModule.configuration.startLevel === 70" name="startLevel" title="70 - User interface running" @click="$set(currentModule.configuration, 'startLevel', 70)" />
<f7-list-item radio :checked="currentModule.configuration.startLevel === 80" name="startLevel" title="80 - Things initialized" @click="$set(currentModule.configuration, 'startLevel', 90)" />
<f7-list-item radio :checked="currentModule.configuration.startLevel === 100" name="startLevel" title="100 - Startup complete" @click="$set(currentModule.configuration, 'startLevel', 100)" />
<f7-block-footer class="padding-horizontal"><small>Start levels below 40 are provided for completeness but will not make a difference since the rules engine is not initialized yet at these levels.</small></f7-block-footer>
</f7-list>
</f7-block>
</template>
<style lang="stylus">
.triggertype-big-button
background var(--f7-card-bg-color)
text-align center
height 7.5rem
.link
color var(--f7-text-color)
</style>
<script>
import ModuleWizard from './module-wizard-mixin'
import ItemPicker from '@/components/config/controls/item-picker.vue'
import ThingPicker from '@/components/config/controls/thing-picker.vue'
import TriggerChannelPicker from '@/components/config/controls/triggerchannel-picker.vue'
import ConfigSheet from '@/components/config/config-sheet.vue'
export default {
mixins: [ModuleWizard],
props: ['currentModule', 'currentModuleType'],
components: {
ItemPicker,
ThingPicker,
TriggerChannelPicker,
ConfigSheet
},
data () {
return {
category: '',
itemEventType: 'command',
thingEventType: 'triggerChannelFired',
timeEventType: 'cron',
currentItem: null
}
},
methods: {
chooseItemCategory () {
this.openModelPicker()
},
chooseThingCategory () {
this.category = 'thing'
this.updateThingEventType('triggerChannelFired')
},
chooseTimeCategory () {
this.category = 'time'
this.updateTimeEventType('cron')
},
chooseSystemCategory () {
this.category = 'system'
this.updateSystemEventType('start')
},
updateItemEventType (type) {
this.itemEventType = type
switch (type) {
case 'command':
this.$emit('typeSelect', 'core.ItemCommandTrigger')
if (this.currentItem) this.$set(this.currentModule, 'configuration', Object.assign({}, { itemName: this.currentItem.name }))
break
case 'updated':
this.$emit('typeSelect', 'core.ItemStateUpdateTrigger')
if (this.currentItem) this.$set(this.currentModule, 'configuration', Object.assign({}, { itemName: this.currentItem.name }))
break
case 'changed':
this.$emit('typeSelect', 'core.ItemStateChangeTrigger')
if (this.currentItem) this.$set(this.currentModule, 'configuration', Object.assign({}, { itemName: this.currentItem.name }))
break
case 'memberCommand':
this.$emit('typeSelect', 'core.GroupCommandTrigger')
if (this.currentItem) this.$set(this.currentModule, 'configuration', Object.assign({}, { groupName: this.currentItem.name }))
break
case 'memberUpdated':
this.$emit('typeSelect', 'core.GroupStateUpdateTrigger')
if (this.currentItem) this.$set(this.currentModule, 'configuration', Object.assign({}, { groupName: this.currentItem.name }))
break
case 'memberChanged':
this.$emit('typeSelect', 'core.GroupStateChangeTrigger')
if (this.currentItem) this.$set(this.currentModule, 'configuration', Object.assign({}, { groupName: this.currentItem.name }))
break
}
},
updateThingEventType (type) {
this.thingEventType = type
switch (type) {
case 'triggerChannelFired':
this.$emit('typeSelect', 'core.ChannelEventTrigger', true)
break
case 'statusUpdated':
this.$emit('typeSelect', 'core.ThingStatusUpdateTrigger', true)
break
case 'statusChanged':
this.$emit('typeSelect', 'core.ThingStatusChangeTrigger', true)
break
}
},
updateTimeEventType (type) {
this.timeEventType = type
switch (type) {
case 'cron':
this.$emit('typeSelect', 'timer.GenericCronTrigger', true)
break
case 'timeOfDay':
this.$emit('typeSelect', 'timer.TimeOfDayTrigger', true)
break
}
},
updateSystemEventType (type) {
this.systemEventType = type
switch (type) {
case 'start':
this.$emit('typeSelect', 'core.SystemStartlevelTrigger', true)
this.$set(this.currentModule.configuration, 'startlevel', 20)
break
}
},
itemPicked (value) {
this.category = 'item'
this.currentItem = value
this.$set(this.currentModule.configuration, 'itemName', value.name)
this.updateItemEventType('command')
}
}
}
</script>

View File

@ -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
}

View File

@ -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>
<f7-link slot="media" v-if="isEditable" icon-color="red" icon-aurora="f7:minus_circle_filled" icon-ios="f7:minus_circle_filled" icon-md="material:remove_circle_outline" @click="showSwipeout"></f7-link>
<f7-link slot="after" v-if="!createMode && mod.type && mod.type.indexOf('script') === 0" icon-f7="pencil_ellipsis_rectangle" color="gray" @click.native="(ev) => editScriptDirect(ev, mod)" :tooltip="(isEditable) ? 'Edit script' : 'View script'"></f7-link>
<f7-link slot="after" v-if="!createMode && mod.type === 'timer.GenericCronTrigger' && isEditable" icon-f7="calendar" color="gray" @click.native="(ev) => buildCronExpression(ev, mod)" tooltip="Build cron expression"></f7-link>
<f7-link slot="after" v-if="mod.type && mod.type.indexOf('script') === 0" icon-f7="pencil_ellipsis_rectangle" color="gray" @click.native="(ev) => editModule(ev, section, mod, true)" :tooltip="'Edit module'"></f7-link>
<f7-link slot="after" v-if="mod.type === 'timer.GenericCronTrigger' && isEditable" icon-f7="pencil_ellipsis_rectangle" color="gray" @click.native="(ev) => editModule(ev, section, mod, true)" tooltip="Edit module"></f7-link>
<f7-swipeout-actions right v-if="isEditable">
<f7-swipeout-button @click="(ev) => deleteModule(ev, section, mod)" style="background-color: var(--f7-swipeout-delete-button-bg-color)">Delete</f7-swipeout-button>
</f7-swipeout-actions>
@ -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) {

View File

@ -22,23 +22,28 @@
</f7-list-input>
</f7-list>
</f7-col>
<f7-block-footer class="no-margin padding-left"><small>Tip: leave fields blank to set automatically to the suggested name and description. <f7-link @click="ruleModule.label = null; ruleModule.description = null">Clear</f7-link></small></f7-block-footer>
<!-- <f7-block-footer class="no-margin padding-left"><small>Tip: leave fields blank to set automatically to the suggested name and description. <f7-link @click="ruleModule.label = null; ruleModule.description = null">Clear</f7-link></small></f7-block-footer> -->
<f7-block-title>Type of {{currentSection.replace(/.$/, '')}}</f7-block-title>
<f7-list v-if="!currentRuleModuleType">
<ul v-for="(mt, scope) in groupedModuleTypes(currentSection)" :key="scope">
<f7-list-item divider :title="scope" />
<f7-list-item radio v-for="moduleType in mt"
:value="moduleType.uid"
@change="setModuleType(moduleType)"
:checked="ruleModule.type === moduleType.uid"
:key="moduleType.uid" :title="moduleType.label" name="module-type"></f7-list-item>
</ul>
</f7-list>
<f7-list v-else>
<div v-if="ruleModule.new">
<f7-block-title class="no-margin padding-horizontal margin-vertical" v-if="!advancedTypePicker" medium>{{sectionLabels[currentSection][0]}}</f7-block-title>
<f7-list v-if="advancedTypePicker && !ruleModule.type">
<ul v-for="(mt, scope) in groupedModuleTypes(currentSection)" :key="scope">
<f7-list-item divider :title="scope" />
<f7-list-item radio v-for="moduleType in mt"
:value="moduleType.uid"
@change="setModuleType(moduleType)"
:checked="ruleModule.type === moduleType.uid"
:key="moduleType.uid" :title="moduleType.label" name="module-type"></f7-list-item>
</ul>
</f7-list>
<trigger-module-wizard v-else-if="!advancedTypePicker && currentSection === 'triggers'" :current-module="ruleModule" :current-module-type="currentRuleModuleType" @typeSelect="setModuleType" @showAdvanced="advancedTypePicker = true" />
<condition-module-wizard v-else-if="!advancedTypePicker && currentSection === 'conditions'" :current-module="ruleModule" :current-module-type="currentRuleModuleType" @typeSelect="setModuleType" @showAdvanced="advancedTypePicker = true" @startScript="startScripting" />
<action-module-wizard v-else-if="!advancedTypePicker && currentSection === 'actions'" :current-module="ruleModule" :current-module-type="currentRuleModuleType" @typeSelect="setModuleType" @showAdvanced="advancedTypePicker = true" @startScript="startScripting" />
</div>
<f7-list v-if="ruleModule.type && (!ruleModule.new || advancedTypePicker)">
<f7-list-item :title="sectionLabels[currentSection][0]" ref="ruleModuleTypeSmartSelect" smart-select :smart-select-params="{ view: $f7.views.main, openIn: 'popup', closeOnSelect: true }">
<select name="ruleModuleType"
@change="setModuleType(moduleTypes[currentSection].find((t) => t.uid === $refs.ruleModuleTypeSmartSelect.f7SmartSelect.getValue()))">
@change="setModuleType(moduleTypes[currentSection].find((t) => t.uid === $refs.ruleModuleTypeSmartSelect.f7SmartSelect.getValue()), true)">
<optgroup v-for="(mt, scope) in groupedModuleTypes(currentSection)" :key="scope" :label="scope">
<option v-for="moduleType in mt"
:value="moduleType.uid" :key="moduleType.uid" :selected="currentRuleModuleType.uid === moduleType.uid">
@ -48,8 +53,8 @@
</select>
</f7-list-item>
</f7-list>
<f7-block-title v-if="ruleModule && currentRuleModuleType" style="margin-bottom: calc(var(--f7-block-title-margin-bottom) - var(--f7-list-margin-vertical))">Configuration</f7-block-title>
<f7-col v-if="ruleModule && currentRuleModuleType">
<f7-block-title v-if="ruleModule && currentRuleModuleType && (!ruleModule.new || advancedTypePicker)" style="margin-bottom: calc(var(--f7-block-title-margin-bottom) - var(--f7-list-margin-vertical))">Configuration</f7-block-title>
<f7-col v-if="ruleModule && currentRuleModuleType && (!ruleModule.new || advancedTypePicker)">
<config-sheet :key="currentSection + ruleModule.id"
ref="parameters"
:parameterGroups="[]"
@ -65,18 +70,24 @@
<script>
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', '<xml xmlns="https://developers.google.com/blockly/xml"></xml>')
}
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) => {

View File

@ -55,7 +55,7 @@
<f7-fab v-show="!newScript && isBlockly && blocklyCodePreview" position="right-bottom" slot="fixed" color="blue" @click="blocklyCodePreview = false">
<f7-icon f7="ticket"></f7-icon>
</f7-fab>
<f7-fab v-show="!newScript && !script && mode === 'application/javascript' && !isBlockly" position="center-center" slot="fixed" color="blue" @click="convertToBlockly" text="Design with Blockly">
<f7-fab v-show="!newScript && !script && mode === 'application/javascript' && !isBlockly" position="center-bottom" slot="fixed" color="blue" @click="convertToBlockly" text="Design with Blockly">
<f7-icon f7="ticket_fill"></f7-icon>
</f7-fab>

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
id="Layer_6"
data-name="Layer 6"
viewBox="0 0 192 192"
version="1.1"
sodipodi:docname="logo-only.svg"
inkscape:version="0.92.2pre0 (973e216, 2017-07-25)"
inkscape:export-filename="/usr/local/google/home/epastern/Documents/Blockly Logos/Square/logo-only.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<metadata
id="metadata913">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>logo-only</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2560"
inkscape:window-height="1379"
id="namedview911"
showgrid="false"
inkscape:zoom="2"
inkscape:cx="239.87642"
inkscape:cy="59.742687"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="g1013" />
<defs
id="defs902">
<style
id="style900">.cls-1{fill:#4285f4;}.cls-2{fill:#c8d1db;}</style>
</defs>
<title
id="title904">logo-only</title>
<g
id="g1013"
transform="translate(23.500002,-7.9121105)"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<path
id="path906"
d="M 20.140625,32 C 13.433598,31.994468 7.9944684,37.433598 8,44.140625 V 148.85938 C 7.99447,155.56641 13.433598,161.00553 20.140625,161 h 4.726563 c 2.330826,8.74182 10.245751,14.82585 19.292968,14.83008 C 53.201562,175.81878 61.108176,169.73621 63.4375,161 h 4.841797 15.726562 c 4.418278,0 8,-3.58172 8,-8 V 40 l -8,-8 z"
style="fill:#4285f4"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccssccc" />
<path
sodipodi:nodetypes="ccccccccccccccccc"
inkscape:connector-curvature="0"
id="path908"
d="M 80.007812,31.994141 C 79.997147,49.696887 80,67.396525 80,85.109375 L 63.369141,75.710938 C 60.971784,74.358189 58.004891,76.087168 58,78.839844 v 40.621096 c 0.0049,2.75267 2.971786,4.48165 5.369141,3.1289 L 80,113.18945 v 37.5918 2.21875 8 h 8 1.425781 36.054689 c 6.36195,-2.6e-4 11.51927,-5.15758 11.51953,-11.51953 V 43.480469 C 136.97822,37.133775 131.8272,32.000222 125.48047,32 Z"
style="fill:#c8d1db" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB