Regenerate rule templates (#3197)
This PR depends on https://github.com/openhab/openhab-core/pull/4718. It does several things, but the major points are: * It enables regeneration of rules based on rule templates from the UI. Regeneration can be desirable if the rule template has been updated and you want the rule(s) to reflect the change, or if you wish to change one of the parameters, for example to make the rule work with a different Item. * It restores the display of read-only/unmanaged rules in the UI. It is currently broken because all read-only rules are forwarded to the script editor, effectively hiding the rule editor, unless you modify the URL manually. This is very relevant for https://github.com/openhab/openhab-core/pull/4633, whose rules will otherwise be quite meaningless in the UI (they will still work, but you won't be able to see what they do from the UI). * It provides a third tab "Source" to the rule editor that shows the "source script" used to create the rule, if the rule is supplied via a JSR223 based scripting add-on, *if* the "source script" is embedded as metadata with the rule. This is the current practice with most JSR223 based scripting add-on provided rules, and is the only situation where showing read-only rules in the script editor makes any sense. The "Source" tab is thus a replacement for removing the forwarding to the script editor. Rules that have embedded "source scripts" will by default open in the "Source" tab, so that the difference for these rules will be minimal, while it also allows other read-only rules to work. In addition to the above points, there are quite a few bug fixes that I have come over while debugging/testing this. There's a lot that could be said about the details here, but I know from experience that if I attempt to describe everything, people won't read it. So, I tried to make this description brief, and can instead elaborate on any subjects on demand. --------- Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com> Co-authored-by: Yannick Schaus <github@schaus.net>main
parent
f1bc650743
commit
548d1c6d36
|
@ -6,6 +6,7 @@
|
|||
"dialogs.save": "Save",
|
||||
"dialogs.close": "Close",
|
||||
"dialogs.copy": "Copy",
|
||||
"dialogs.create": "Create",
|
||||
"dialogs.delete": "Delete",
|
||||
"dialogs.reload": "Reload",
|
||||
"dialogs.retry": "Try Again",
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"HANDLER_INITIALIZING_ERROR": "ERROR: HANDLER",
|
||||
"HANDLER_MISSING_ERROR": "ERROR: HANDLER",
|
||||
"TEMPLATE_MISSING_ERROR": "ERROR: TEMPLATE",
|
||||
"TEMPLATE_PENDING": "TEMPLATE PENDING",
|
||||
"INVALID_RULE": "INVALID",
|
||||
"DISABLED": "DISABLED"
|
||||
}
|
||||
|
|
|
@ -178,6 +178,7 @@ export default {
|
|||
// See https://codemirror.net/5/mode/index.html for supported language names & MIME types
|
||||
if (!mode) return mode
|
||||
if (mode.indexOf('yaml') >= 0) return 'text/x-yaml'
|
||||
if (mode === 'application/json' || mode === 'json') return 'application/json'
|
||||
if (mode.startsWith('application/javascript') || mode === 'js') return 'text/javascript'
|
||||
if (mode === 'application/vnd.openhab.dsl.rule') return 'text/x-java'
|
||||
if (mode === 'application/x-groovy' || mode === 'groovy') return 'text/x-groovy'
|
||||
|
@ -234,7 +235,7 @@ export default {
|
|||
onCmReady (cm) {
|
||||
const self = this
|
||||
let extraKeys = {}
|
||||
if (this.mode.indexOf('application/javascript') === 0) {
|
||||
if (this.mode && this.mode.indexOf('application/javascript') === 0) {
|
||||
window.tern = tern
|
||||
if (this.ternAutocompletionHook) {
|
||||
tern.registerPlugin('openhab-tern-hook', (server, options) => {
|
||||
|
@ -281,7 +282,7 @@ export default {
|
|||
closeOnUnfocus: false,
|
||||
completeSingle: self.mode && self.mode.indexOf('yaml') > 0,
|
||||
hint (cm, option) {
|
||||
if (self.mode.indexOf('application/vnd.openhab.uicomponent') === 0) {
|
||||
if (self.mode && self.mode.indexOf('application/vnd.openhab.uicomponent') === 0) {
|
||||
return componentsHint(cm, option, self.mode)
|
||||
} else if (self.mode === 'application/vnd.openhab.item+yaml') {
|
||||
return itemsHint(cm, option, self.mode)
|
||||
|
|
|
@ -151,7 +151,7 @@ import ConfigSheet from '@/components/config/config-sheet.vue'
|
|||
|
||||
export default {
|
||||
mixins: [ModuleWizard],
|
||||
props: ['currentModule', 'currentModuleType'],
|
||||
props: ['currentModule', 'currentModuleType', 'moduleTypes'],
|
||||
components: {
|
||||
ItemPicker,
|
||||
ConfigSheet
|
||||
|
@ -196,18 +196,16 @@ export default {
|
|||
},
|
||||
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(')', '')
|
||||
}
|
||||
}))
|
||||
})
|
||||
let moduleType = this.moduleTypes.find((t) => t.uid === 'script.ScriptAction')
|
||||
if (moduleType) {
|
||||
this.$set(this, 'languages', moduleType.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'
|
||||
|
@ -272,7 +270,10 @@ export default {
|
|||
// this.$set(this.currentModule.configuration, 'command', hsb.join(','))
|
||||
},
|
||||
scriptLanguagePicked (value) {
|
||||
this.$emit('startScript', value)
|
||||
this.$emit('typeSelect', 'script.ScriptAction')
|
||||
this.$nextTick(() => {
|
||||
this.$emit('startScript', value)
|
||||
})
|
||||
},
|
||||
itemPicked (value) {
|
||||
this.category = 'item'
|
||||
|
|
|
@ -129,7 +129,7 @@ import ConfigSheet from '@/components/config/config-sheet.vue'
|
|||
|
||||
export default {
|
||||
mixins: [ModuleWizard],
|
||||
props: ['currentModule', 'currentModuleType'],
|
||||
props: ['currentModule', 'currentModuleType', 'moduleTypes'],
|
||||
components: {
|
||||
ItemPicker,
|
||||
ConfigSheet
|
||||
|
@ -158,18 +158,16 @@ export default {
|
|||
},
|
||||
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(')', '')
|
||||
}
|
||||
}))
|
||||
})
|
||||
let moduleType = this.moduleTypes.find((t) => t.uid === 'script.ScriptCondition')
|
||||
if (moduleType) {
|
||||
this.$set(this, 'languages', moduleType.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'
|
||||
|
@ -228,7 +226,10 @@ export default {
|
|||
}
|
||||
},
|
||||
scriptLanguagePicked (value) {
|
||||
this.$emit('startScript', value)
|
||||
this.$emit('typeSelect', 'script.ScriptCondition')
|
||||
this.$nextTick(() => {
|
||||
this.$emit('startScript', value)
|
||||
})
|
||||
},
|
||||
itemPicked (value) {
|
||||
this.category = 'item'
|
||||
|
|
|
@ -3,19 +3,20 @@
|
|||
<f7-block v-if="ready" class="block-narrow">
|
||||
<f7-col>
|
||||
<f7-list inline-labels no-hairlines-md>
|
||||
<f7-list-input ref="ruleId" :label="`${type} ID`" type="text" :placeholder="`A unique identifier for the ${type.toLowerCase()}`" :value="rule.uid" required validate
|
||||
<f7-list-input ref="ruleId" :label="`${type} ID`" type="text" :placeholder="`A unique identifier for the ${type.toLowerCase()}`" :value="rule.uid" required :validate="editable"
|
||||
:disabled="!createMode" :info="(createMode) ? 'Required. Note: cannot be changed after the creation' : ''" input-id="input"
|
||||
pattern="[A-Za-z0-9_\-]+" error-message="Required. A-Z,a-z,0-9,_,- only"
|
||||
@input="rule.uid = $event.target.value" :clear-button="createMode">
|
||||
<f7-link slot="inner" icon-f7="hammer_fill" style="margin-top: 4px; margin-left: 4px; margin-bottom: auto" tooltip="Fix ID" v-if="createMode && $refs.ruleId?.state?.inputInvalid && rule.uid.trim()" @click="$oh.utils.normalizeInput('#input')" />
|
||||
</f7-list-input>
|
||||
<f7-list-input v-if="!createMode && templateName" label="Template" type="text" :value="templateName" disabled />
|
||||
<f7-list-input label="Label" type="text" :placeholder="`${type} label for display purposes`" :info="(createMode) ? 'Required' : ''" :value="rule.name" required validate
|
||||
:disabled="!editable" @input="rule.name = $event.target.value" :clear-button="editable" />
|
||||
<f7-list-input label="Description" type="text" :value="rule.description"
|
||||
:disabled="!editable" @input="rule.description = $event.target.value" :clear-button="editable" />
|
||||
</f7-list>
|
||||
<f7-list inline-labels no-hairlines-md>
|
||||
<tag-input v-if="!createMode || !hasRuleTemplate" title="Tags" :item="rule" :disabled="!editable" :showSemanticTags="true" :inScriptEditor="inScriptEditor" :inSceneEditor="inSceneEditor" />
|
||||
<tag-input v-if="!stubMode" title="Tags" :item="rule" :disabled="!editable" :showSemanticTags="true" :inScriptEditor="inScriptEditor" :inSceneEditor="inSceneEditor" />
|
||||
</f7-list>
|
||||
</f7-col>
|
||||
</f7-block>
|
||||
|
@ -24,7 +25,7 @@
|
|||
<f7-block v-else class="block-narrow">
|
||||
<f7-col class="skeleton-text skeleton-effect-blink">
|
||||
<f7-list inline-labels no-hairlines-md>
|
||||
<f7-list-input label="Rule ID" type="text" placeholder="Required" value="_______" required validate
|
||||
<f7-list-input label="Rule ID" type="text" placeholder="Required" value="_______" required :validate="editable"
|
||||
:disabled="true" :info="(createMode) ? 'Note: cannot be changed after the creation' : ''"
|
||||
@input="rule.uid = $event.target.value" :clear-button="createMode" />
|
||||
<f7-list-input label="Name" type="text" placeholder="Required" required validate
|
||||
|
@ -33,7 +34,7 @@
|
|||
:disabled="true" @input="rule.description = $event.target.value" :clear-button="editable" />
|
||||
</f7-list>
|
||||
<f7-list inline-labels no-hairlines-md>
|
||||
<tag-input v-if="!createMode || !hasRuleTemplate" :item="rule" :disabled="!editable" :showSemanticTags="true" :inScriptEditor="inScriptEditor" :inSceneEditor="inSceneEditor" />
|
||||
<tag-input v-if="!stubMode" :item="rule" :disabled="!editable" :showSemanticTags="true" :inScriptEditor="inScriptEditor" :inSceneEditor="inSceneEditor" />
|
||||
</f7-list>
|
||||
</f7-col>
|
||||
</f7-block>
|
||||
|
@ -44,7 +45,7 @@
|
|||
import TagInput from '@/components/tags/tag-input.vue'
|
||||
|
||||
export default {
|
||||
props: ['rule', 'ready', 'createMode', 'hasRuleTemplate', 'inScriptEditor', 'inSceneEditor'],
|
||||
props: ['rule', 'ready', 'createMode', 'stubMode', 'templateName', 'inScriptEditor', 'inSceneEditor'],
|
||||
components: {
|
||||
TagInput
|
||||
},
|
||||
|
|
|
@ -3,14 +3,15 @@ import RuleStatusLabels from '@/assets/i18n/rule-status/en'
|
|||
export default {
|
||||
methods: {
|
||||
ruleStatusBadgeColor (statusInfo) {
|
||||
if (statusInfo.status === 'IDLE') return 'green'
|
||||
if (statusInfo.statusDetail === 'DISABLED') return 'gray'
|
||||
if (statusInfo.status === 'UNINITIALIZED') return 'red'
|
||||
if (statusInfo.status === 'INITIALIZING') return 'yellow'
|
||||
if (statusInfo.status === 'RUNNING') return 'orange'
|
||||
if (statusInfo?.status === 'IDLE') return 'green'
|
||||
if (statusInfo?.statusDetail === 'DISABLED') return 'gray'
|
||||
if (statusInfo?.status === 'UNINITIALIZED') return statusInfo.statusDetail === 'TEMPLATE_PENDING' ? 'orange' : 'red'
|
||||
if (statusInfo?.status === 'INITIALIZING') return 'yellow'
|
||||
if (statusInfo?.status === 'RUNNING') return 'orange'
|
||||
return 'green'
|
||||
},
|
||||
ruleStatusBadgeText (statusInfo) {
|
||||
if (!statusInfo?.status) return ''
|
||||
if (statusInfo.status === 'IDLE') return 'IDLE'
|
||||
if (statusInfo.statusDetail !== 'NONE') return RuleStatusLabels[statusInfo.statusDetail]
|
||||
return statusInfo.status
|
||||
|
|
|
@ -341,6 +341,12 @@ export default [
|
|||
beforeLeave: [checkDirtyBeforeLeave],
|
||||
async: loadAsync(RuleEditPage, { createMode: true })
|
||||
},
|
||||
{
|
||||
path: 'stub',
|
||||
beforeEnter: [enforceAdminForRoute],
|
||||
beforeLeave: [checkDirtyBeforeLeave],
|
||||
async: loadAsync(RuleEditPage, { createMode: false, stubMode: true })
|
||||
},
|
||||
{
|
||||
path: ':ruleId',
|
||||
beforeEnter: [enforceAdminForRoute],
|
||||
|
|
|
@ -40,7 +40,9 @@ export default {
|
|||
switchTab (tab, onSuccessCallback) {
|
||||
if (this.currentTab !== tab) {
|
||||
this.currentTab = tab
|
||||
onSuccessCallback()
|
||||
if (onSuccessCallback) {
|
||||
onSuccessCallback()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,30 @@ import cronstrue from 'cronstrue'
|
|||
|
||||
export default {
|
||||
methods: {
|
||||
findModuleType (mod, section) {
|
||||
if (!mod || !this.moduleTypes) {
|
||||
return undefined
|
||||
}
|
||||
let result
|
||||
if (section) {
|
||||
return this.moduleTypes[section]?.find((m) => m.uid === mod.type)
|
||||
} else {
|
||||
if (this.moduleTypes.actions) {
|
||||
result = this.moduleTypes.actions.find((m) => m.uid === mod.type)
|
||||
}
|
||||
if (!result && this.moduleTypes.triggers) {
|
||||
result = this.moduleTypes.triggers.find((m) => m.uid === mod.type)
|
||||
}
|
||||
if (!result && this.moduleTypes.conditions) {
|
||||
result = this.moduleTypes.conditions.find((m) => m.uid === mod.type)
|
||||
}
|
||||
return result
|
||||
}
|
||||
},
|
||||
suggestedModuleTitle (mod, moduleType, section) {
|
||||
if (!moduleType) {
|
||||
moduleType = this.moduleTypes[section].find((m) => m.uid === mod.type)
|
||||
if (!this.moduleTypes) return 'Name'
|
||||
moduleType = this.findModuleType(mod, section)
|
||||
if (!moduleType) return 'Name'
|
||||
}
|
||||
const config = mod.configuration
|
||||
|
@ -93,7 +114,8 @@ export default {
|
|||
},
|
||||
suggestedModuleDescription (mod, moduleType, section) {
|
||||
if (!moduleType) {
|
||||
moduleType = this.moduleTypes[section].find((m) => m.uid === mod.type)
|
||||
if (!this.moduleTypes) return 'Description'
|
||||
moduleType = this.findModuleType(mod, section)
|
||||
if (!moduleType) return 'Description'
|
||||
}
|
||||
const config = mod.configuration
|
||||
|
|
|
@ -100,6 +100,12 @@ export default {
|
|||
case 'state':
|
||||
this.$set(this.rule, 'status', JSON.parse(event.payload)) // e.g. {"status":"RUNNING","statusDetail":"NONE"}
|
||||
break
|
||||
case 'added':
|
||||
case 'updated':
|
||||
if (!this.dirty) {
|
||||
this.load()
|
||||
}
|
||||
break
|
||||
}
|
||||
})
|
||||
},
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
<template>
|
||||
<f7-page @page:afterin="onPageAfterIn" @page:afterout="onPageAfterOut">
|
||||
<f7-navbar :title="(createMode ? 'Create rule' : rule.name) + dirtyIndicator" back-link="Back" no-hairline>
|
||||
<f7-navbar :title="(createMode ? (ruleCopy ? 'Duplicate rule' : 'Create rule') : stubMode ? 'Regenerate rule from template' : rule.name) + dirtyIndicator" back-link="Back" no-hairline>
|
||||
<f7-nav-right>
|
||||
<developer-dock-icon />
|
||||
<template v-if="isEditable">
|
||||
<f7-link @click="save()" v-if="$theme.md" icon-md="material:save" icon-only />
|
||||
<f7-link @click="save()" v-if="!$theme.md">
|
||||
Save<span v-if="$device.desktop"> (Ctrl-S)</span>
|
||||
{{ stubMode ? 'Regenerate' : $t(createMode ? 'dialogs.create' : 'dialogs.save') }} <span v-if="$device.desktop"> (Ctrl-S)</span>
|
||||
</f7-link>
|
||||
</template>
|
||||
<f7-link v-else icon-f7="lock_fill" icon-only tooltip="This rule is not editable through the UI" />
|
||||
</f7-nav-right>
|
||||
</f7-navbar>
|
||||
<f7-toolbar tabbar position="top">
|
||||
|
@ -18,13 +19,17 @@
|
|||
<f7-link @click="switchTab('code', toYaml)" :tab-link-active="currentTab === 'code'" class="tab-link">
|
||||
Code
|
||||
</f7-link>
|
||||
<f7-link v-if="ready && hasSource" @click="switchTab('source')" :tab-link-active="currentTab === 'source'" class="tab-link">
|
||||
Source
|
||||
</f7-link>
|
||||
</f7-toolbar>
|
||||
<f7-tabs class="sitemap-editor-tabs">
|
||||
<f7-tab id="design" @tab:show="() => this.currentTab = 'design'" :tab-active="currentTab === 'design'">
|
||||
<f7-block v-if="ready && rule.status && (!createMode)" class="block-narrow padding-left padding-right" strong>
|
||||
<f7-col v-if="!createMode">
|
||||
<f7-block v-if="ready && rule.status && !createMode && !stubMode" class="block-narrow padding-left padding-right" strong>
|
||||
<f7-col v-if="!createMode && !stubMode">
|
||||
<div class="float-right align-items-flex-start align-items-center">
|
||||
<!-- <f7-toggle class="enable-toggle"></f7-toggle> -->
|
||||
<f7-link v-if="canRegenerate" :icon-color="'deeppurple'" :tooltip="'Regenerate from template'" icon-md="f7:arrow_2_circlepath" icon-ios="f7:arrow_2_circlepath" icon-aurora="f7:arrow_2_circlepath" icon-size="32" color="deeppurple" @click="regenerateFromTemplate" />
|
||||
<f7-link :icon-color="(rule.status.statusDetail === 'DISABLED') ? 'orange' : 'gray'" :tooltip="((rule.status.statusDetail === 'DISABLED') ? 'Enable' : 'Disable') + (($device.desktop) ? ' (Ctrl-D)' : '')" icon-ios="f7:pause_circle" icon-md="f7:pause_circle" icon-aurora="f7:pause_circle" icon-size="32" color="orange" @click="toggleDisabled" />
|
||||
<f7-link :tooltip="'Run Now' + (($device.desktop) ? ' (Ctrl-R)' : '')" icon-ios="f7:play_round" icon-md="f7:play_round" icon-aurora="f7:play_round" icon-size="32" :color="(rule.status.status === 'IDLE') ? 'blue' : 'gray'" @click="runNow" />
|
||||
</div>
|
||||
|
@ -42,7 +47,7 @@
|
|||
</f7-col>
|
||||
</f7-block>
|
||||
<!-- skeletons for not ready -->
|
||||
<f7-block v-else-if="!createMode" class="block-narrow padding-left padding-right skeleton-text skeleton-effect-blink" strong>
|
||||
<f7-block v-else-if="!createMode && !stubMode" class="block-narrow padding-left padding-right skeleton-text skeleton-effect-blink" strong>
|
||||
<f7-col>
|
||||
______:
|
||||
<f7-chip class="margin-left" text="________" />
|
||||
|
@ -53,15 +58,15 @@
|
|||
</f7-col>
|
||||
</f7-block>
|
||||
|
||||
<rule-general-settings :rule="rule" :ready="ready" :createMode="createMode" :hasTemplate="hasTemplate" />
|
||||
<rule-general-settings :rule="rule" :ready="ready" :createMode="createMode" :stubMode="stubMode" :templateName="templateName" />
|
||||
|
||||
<f7-block v-if="ready" class="block-narrow">
|
||||
<f7-block-footer v-if="!isEditable" class="no-margin padding-left">
|
||||
<f7-icon f7="lock_fill" size="12" color="gray" /> Note: this rule is not editable because it has been provisioned from a file.
|
||||
<f7-icon f7="lock_fill" size="12" color="gray" /> Note: this rule is not editable.
|
||||
</f7-block-footer>
|
||||
<!-- <f7-col v-if="isEditable" class="text-align-right justify-content-flex-end">
|
||||
</f7-col> -->
|
||||
<f7-col v-if="createMode && templates.length > 0" class="new-rule-from-template">
|
||||
<f7-col v-if="createMode && templates.length > 0 && !ruleCopy" class="new-rule-from-template">
|
||||
<f7-block-title medium class="margin-bottom">
|
||||
Create from Template
|
||||
</f7-block-title>
|
||||
|
@ -91,7 +96,50 @@
|
|||
:parameter-groups="[]" :parameters="currentTemplate.configDescriptions"
|
||||
:configuration="rule.configuration" />
|
||||
</f7-col>
|
||||
<f7-col v-if="!hasTemplate" class="rule-modules">
|
||||
<f7-col v-else-if="currentTemplate && stubMode" class="show-associated-template">
|
||||
<f7-block-title medium class="margin-vertical padding-top">
|
||||
Template
|
||||
</f7-block-title>
|
||||
<f7-list media-list>
|
||||
<f7-list-item :title="currentTemplate.label" :footer="currentTemplate.description" :value="currentTemplate.uid" />
|
||||
</f7-list>
|
||||
<f7-block-title medium class="margin-vertical padding-top">
|
||||
Template Configuration
|
||||
</f7-block-title>
|
||||
<f7-link v-if="templateTopicLink" target="_blank" class="external margin-left" color="blue" :href="templateTopicLink">
|
||||
Template Community Marketplace Topic
|
||||
</f7-link>
|
||||
<config-sheet :parameter-groups="[]" :parameters="currentTemplate.configDescriptions" :configuration="rule.configuration" />
|
||||
</f7-col>
|
||||
<f7-col v-else-if="currentTemplate && createMode && ruleCopy?.templateUID" class="select-integrate-template">
|
||||
<f7-block-title medium class="margin-vertical padding-top">
|
||||
Template
|
||||
</f7-block-title>
|
||||
<f7-list media-list>
|
||||
<f7-list-item
|
||||
:title="'Keep template: ' + currentTemplate.label"
|
||||
footer="The rule will still be linked to the template and can be regenerated if the template changes."
|
||||
:value="currentTemplate.uid"
|
||||
radio :checked="Boolean(rule.templateUID)" radio-icon="start"
|
||||
@change="keepTemplate(true)" />
|
||||
<f7-list-item
|
||||
title="Integrate template"
|
||||
footer="Integrates the template in the rule so that the rule is no longer linked to the template."
|
||||
value="integrate"
|
||||
radio :checked="!rule.templateUID" radio-icon="start"
|
||||
@change="keepTemplate(false)" />
|
||||
</f7-list>
|
||||
<div v-if="rule.templateUID">
|
||||
<f7-block-title medium class="margin-vertical padding-top">
|
||||
Template Configuration
|
||||
</f7-block-title>
|
||||
<f7-link v-if="templateTopicLink" target="_blank" class="external margin-left" color="blue" :href="templateTopicLink">
|
||||
Template Community Marketplace Topic
|
||||
</f7-link>
|
||||
<config-sheet :parameter-groups="[]" :parameters="currentTemplate.configDescriptions" :configuration="rule.configuration" />
|
||||
</div>
|
||||
</f7-col>
|
||||
<f7-col v-if="!hasTemplate || (createMode && ruleCopy?.templateUID && !rule.templateUID)" class="rule-modules">
|
||||
<div v-if="isEditable" class="no-padding float-right">
|
||||
<f7-button @click="toggleModuleControls" small outline :fill="showModuleControls" sortable-toggle=".sortable" style="margin-top: -3px; margin-right: 5px"
|
||||
color="gray" icon-size="12" icon-ios="material:wrap_text" icon-md="material:wrap_text" icon-aurora="material:wrap_text">
|
||||
|
@ -112,7 +160,7 @@
|
|||
: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"
|
||||
:link="!showModuleControls && !isOpaqueModule(mod)"
|
||||
@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-swipeout-actions right v-if="isEditable">
|
||||
|
@ -130,22 +178,46 @@
|
|||
</f7-list>
|
||||
</div>
|
||||
</f7-col>
|
||||
<f7-col v-if="isEditable && (!createMode)">
|
||||
<f7-col v-if="!createMode && !stubMode">
|
||||
<f7-list>
|
||||
<f7-list-button color="blue" @click="duplicateRule">
|
||||
<f7-list-button v-if="isEditable || !hasOpaqueModule" color="blue" @click="duplicateRule">
|
||||
Duplicate Rule
|
||||
</f7-list-button>
|
||||
<f7-list-button color="red" @click="deleteRule">
|
||||
Remove Rule
|
||||
<f7-list-button v-if="isEditable" color="red" @click="deleteRule">
|
||||
Delete Rule
|
||||
</f7-list-button>
|
||||
</f7-list>
|
||||
</f7-col>
|
||||
</f7-block>
|
||||
</f7-tab>
|
||||
<f7-tab id="code" @tab:show="() => { this.currentTab = 'code'; toYaml() }" :tab-active="currentTab === 'code'">
|
||||
<editor v-if="currentTab === 'code'" class="rule-code-editor" mode="application/vnd.openhab.rule+yaml" :value="ruleYaml" @input="onEditorInput" />
|
||||
<f7-icon v-if="!createMode && !isEditable" f7="lock" class="float-right margin" style="opacity:0.5; z-index: 4000; user-select: none;" size="50" color="gray" tooltip="This code is not editable" />
|
||||
<editor v-if="currentTab === 'code'" class="rule-code-editor" mode="application/vnd.openhab.rule+yaml" :value="ruleYaml" :readOnly="!isEditable" @input="onEditorInput" />
|
||||
<!-- <pre class="yaml-message padding-horizontal" :class="[yamlError === 'OK' ? 'text-color-green' : 'text-color-red']">{{yamlError}}</pre> -->
|
||||
</f7-tab>
|
||||
<f7-tab v-if="ready && (hasSource)" id="source" @tab:show="() => { this.currentTab = 'source'} " :tab-active="currentTab === 'source'">
|
||||
<f7-block v-if="ready && !createMode && !stubMode" class="block-narrow padding-left padding-right">
|
||||
<f7-col>
|
||||
<div v-if="rule.status" class="left">
|
||||
Status:
|
||||
<f7-chip class="margin-left"
|
||||
:text="rule.status.status"
|
||||
:color="ruleStatusBadgeColor(rule.status)"
|
||||
:tooltip="rule.status.statusDetail !== 'NONE' ? rule.status.statusDetail : undefined" />
|
||||
</div>
|
||||
<div v-if="sourceType" class="middle source-type-text">
|
||||
{{ sourceTypeText }}
|
||||
</div>
|
||||
<div class="right">
|
||||
<f7-link v-if="canRegenerate" :icon-color="'deeppurple'" :tooltip="'Regenerate from template'" icon-md="f7:arrow_2_circlepath" icon-ios="f7:arrow_2_circlepath" icon-aurora="f7:arrow_2_circlepath" icon-size="32" color="deeppurple" @click="regenerateFromTemplate" />
|
||||
<f7-link :icon-color="(rule.status.statusDetail === 'DISABLED') ? 'orange' : 'gray'" :tooltip="((rule.status.statusDetail === 'DISABLED') ? 'Enable' : 'Disable') + (($device.desktop) ? ' (Ctrl-D)' : '')" icon-ios="f7:pause_circle" icon-md="f7:pause_circle" icon-aurora="f7:pause_circle" icon-size="32" color="orange" @click="toggleDisabled" />
|
||||
<f7-link :tooltip="'Run Now' + (($device.desktop) ? ' (Ctrl-R)' : '')" icon-ios="f7:play_round" icon-md="f7:play_round" icon-aurora="f7:play_round" icon-size="32" :color="(rule.status.status === 'IDLE') ? 'blue' : 'gray'" @click="runNow" />
|
||||
</div>
|
||||
</f7-col>
|
||||
</f7-block>
|
||||
<f7-icon f7="lock" class="float-right margin" style="opacity:0.5; z-index: 4000; user-select: none;" size="50" color="gray" tooltip="Source code is not editable" />
|
||||
<editor v-if="currentTab === 'source'" class="rule-source-viewer" :mode="sourceType" :value="source" :readOnly="true" />
|
||||
</f7-tab>
|
||||
</f7-tabs>
|
||||
</f7-page>
|
||||
</template>
|
||||
|
@ -178,7 +250,36 @@
|
|||
position absolute
|
||||
top 80%
|
||||
white-space pre-wrap
|
||||
|
||||
#source
|
||||
.block-narrow
|
||||
position relative
|
||||
height var(--f7-toolbar-height)
|
||||
color var(--f7-block-strong-text-color)
|
||||
background-color var(--f7-block-strong-bg-color)
|
||||
.col
|
||||
display flex
|
||||
position relative
|
||||
top 50%
|
||||
transform translate(0, -50%)
|
||||
flex-wrap nowrap
|
||||
justify-content space-between
|
||||
align-items center
|
||||
.left
|
||||
margin-right auto
|
||||
.middle
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
.right
|
||||
margin-left auto
|
||||
.rule-source-viewer.vue-codemirror
|
||||
display block
|
||||
top calc(var(--f7-navbar-height) + var(--f7-tabbar-height))
|
||||
height calc(100% - 2*var(--f7-navbar-height) - var(--f7-toolbar-height) - var(--f7-block-margin-vertical) - 1rem)
|
||||
width 100%
|
||||
.source-type-text
|
||||
font-size var(--f7-navbar-subtitle-font-size)
|
||||
color var(--f7-block-footer-text-color)
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
@ -195,6 +296,7 @@ import DirtyMixin from '../dirty-mixin'
|
|||
|
||||
import ConfigSheet from '@/components/config/config-sheet.vue'
|
||||
import RuleGeneralSettings from '@/components/rule/rule-general-settings.vue'
|
||||
import AUTOMATION_LANGUAGES from '@/assets/automation-languages'
|
||||
|
||||
export default {
|
||||
mixins: [RuleMixin, ModuleDescriptionSuggestions, RuleStatus, DirtyMixin],
|
||||
|
@ -203,7 +305,7 @@ export default {
|
|||
ConfigSheet,
|
||||
'editor': () => import(/* webpackChunkName: "script-editor" */ '@/components/config/controls/script-editor.vue')
|
||||
},
|
||||
props: ['ruleId', 'createMode', 'ruleCopy', 'schedule'],
|
||||
props: ['ruleId', 'createMode', 'ruleCopy', 'stubMode', 'schedule'],
|
||||
data () {
|
||||
return {
|
||||
SECTION_LABELS: {
|
||||
|
@ -258,49 +360,104 @@ export default {
|
|||
if (this.loading) return
|
||||
this.loading = true
|
||||
|
||||
const loadModules1 = this.$oh.api.get('/rest/module-types?type=action')
|
||||
const loadModules2 = this.$oh.api.get('/rest/module-types?type=condition')
|
||||
const loadModules3 = this.$oh.api.get('/rest/module-types?type=trigger')
|
||||
|
||||
const loadingFinished = () => {
|
||||
this.$nextTick(() => {
|
||||
this.savedRule = cloneDeep(this.rule)
|
||||
this.ready = true
|
||||
this.loading = false
|
||||
if (!this.createMode && !this.stubMode && this.hasOpaqueModule && this.hasSource) {
|
||||
this.switchTab('source')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Promise.all([loadModules1, loadModules2, loadModules3]).then((data) => {
|
||||
this.moduleTypes.actions = data[0]
|
||||
this.moduleTypes.conditions = data[1]
|
||||
this.moduleTypes.triggers = data[2]
|
||||
this.$oh.api.get('/rest/module-types?asMap=true').then((data) => {
|
||||
this.moduleTypes = data
|
||||
if (this.createMode) {
|
||||
const newRule = this.ruleCopy || {
|
||||
uid: this.$f7.utils.id(),
|
||||
name: '',
|
||||
triggers: [],
|
||||
actions: [],
|
||||
conditions: [],
|
||||
tags: (this.schedule) ? ['Schedule'] : [],
|
||||
configuration: {},
|
||||
templateUID: null,
|
||||
visibility: 'VISIBLE',
|
||||
status: {
|
||||
status: 'NEW'
|
||||
let newRule
|
||||
if (this.ruleCopy) {
|
||||
newRule = cloneDeep(this.ruleCopy)
|
||||
newRule.uid = this.$f7.utils.id()
|
||||
if (newRule.templateUID) {
|
||||
newRule.triggers = []
|
||||
newRule.actions = []
|
||||
newRule.conditions = []
|
||||
if (newRule.templateState === 'instantiated') {
|
||||
newRule.templateState = 'pending'
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newRule = {
|
||||
uid: this.$f7.utils.id(),
|
||||
name: '',
|
||||
triggers: [],
|
||||
actions: [],
|
||||
conditions: [],
|
||||
tags: (this.schedule) ? ['Schedule'] : [],
|
||||
configuration: {},
|
||||
templateUID: null,
|
||||
visibility: 'VISIBLE',
|
||||
status: {
|
||||
status: 'NEW'
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.ruleCopy) newRule.uid = this.$f7.utils.id()
|
||||
this.$set(this, 'rule', newRule)
|
||||
this.$oh.api.get('/rest/templates').then((data2) => {
|
||||
this.$set(this, 'templates', data2)
|
||||
this.$oh.api.get('/rest/templates').then((templateData) => {
|
||||
this.$set(this, 'templates', templateData)
|
||||
if (newRule.templateUID) {
|
||||
const currentTemplate = templateData.find((t) => t.uid === newRule.templateUID) || {
|
||||
uid: newRule.templateUID,
|
||||
label: newRule.templateUID
|
||||
}
|
||||
this.$set(this, 'currentTemplate', currentTemplate)
|
||||
}
|
||||
loadingFinished()
|
||||
})
|
||||
// no need for an event source, the rule doesn't exist yet
|
||||
} else if (this.stubMode) {
|
||||
if (!this.ruleCopy || !this.ruleCopy.templateUID) {
|
||||
this.$f7.toast.create({
|
||||
text: !this.ruleCopy ? 'Failed to create rule stub because there\'s no source rule' : 'Failed to create rule stub because there\'s no template UID',
|
||||
destroyOnClose: true,
|
||||
closeTimeout: 4000
|
||||
}).open()
|
||||
this.$f7router.back()
|
||||
}
|
||||
const ruleStub = this.ruleCopy
|
||||
ruleStub.triggers = []
|
||||
ruleStub.actions = []
|
||||
ruleStub.conditions = []
|
||||
ruleStub.templateState = 'pending'
|
||||
this.$set(this, 'rule', ruleStub)
|
||||
this.$oh.api.get('/rest/templates').then((templateData) => {
|
||||
this.$set(this, 'templates', templateData)
|
||||
let template = this.templates.find((t) => t.uid === ruleStub.templateUID)
|
||||
if (!template) {
|
||||
this.$f7.toast.create({
|
||||
text: 'Template "' + ruleStub.templateUID + '" not found',
|
||||
destroyOnClose: true,
|
||||
closeTimeout: 4000
|
||||
}).open()
|
||||
this.$f7router.back()
|
||||
}
|
||||
this.$set(this, 'currentTemplate', template)
|
||||
loadingFinished()
|
||||
})
|
||||
// no need for an event source, we're going to overwrite the existing rule
|
||||
} else {
|
||||
this.$oh.api.get('/rest/rules/' + this.ruleId).then((data2) => {
|
||||
this.$set(this, 'rule', data2)
|
||||
if (!this.eventSource) this.startEventSource()
|
||||
loadingFinished()
|
||||
if (data2.templateUID) {
|
||||
this.$oh.api.get('/rest/templates').then((templateData) => {
|
||||
this.$set(this, 'templates', templateData)
|
||||
if (!this.eventSource) this.startEventSource()
|
||||
loadingFinished()
|
||||
})
|
||||
} else {
|
||||
if (!this.eventSource) this.startEventSource()
|
||||
loadingFinished()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
@ -336,6 +493,16 @@ export default {
|
|||
.replace('/duplicate', '/' + this.rule.uid)
|
||||
.replace('/schedule/', '/rules/'), { reloadCurrent: true })
|
||||
this.load()
|
||||
} else if (this.stubMode) {
|
||||
this.$f7.toast.create({
|
||||
text: 'Rule regenerated',
|
||||
destroyOnClose: true,
|
||||
closeTimeout: 2000
|
||||
}).open()
|
||||
this.$f7router.navigate(this.$f7route.url
|
||||
.replace('/stub', '/' + this.rule.uid)
|
||||
.replace('/schedule/', '/rules/'), { reloadCurrent: true })
|
||||
this.load()
|
||||
} else {
|
||||
if (!noToast) {
|
||||
this.$f7.toast.create({
|
||||
|
@ -357,6 +524,8 @@ export default {
|
|||
},
|
||||
duplicateRule () {
|
||||
let ruleClone = cloneDeep(this.rule)
|
||||
ruleClone.name = (ruleClone.name || '') + ' copy'
|
||||
ruleClone.editable = true
|
||||
this.$f7router.navigate({
|
||||
url: '/settings/rules/duplicate'
|
||||
}, {
|
||||
|
@ -365,6 +534,33 @@ export default {
|
|||
}
|
||||
})
|
||||
},
|
||||
regenerateFromTemplate () {
|
||||
if (this.isEditable) {
|
||||
this.createStub()
|
||||
} else {
|
||||
this.$oh.api.postPlain('/rest/rules/' + this.rule.uid + '/regenerate').then(() => {
|
||||
this.$f7.toast.create({
|
||||
text: 'Rule regenerated from template',
|
||||
destroyOnClose: true,
|
||||
closeTimeout: 2000
|
||||
}).open()
|
||||
this.load()
|
||||
}).catch((err) => {
|
||||
this.$f7.dialog.alert('An error occurred when trying to regenerate rule "' + this.rule.uid + '" from template: ' + err)
|
||||
})
|
||||
}
|
||||
},
|
||||
createStub () {
|
||||
let ruleClone = cloneDeep(this.rule)
|
||||
this.$f7router.navigate({
|
||||
url: '/settings/rules/stub'
|
||||
}, {
|
||||
reloadCurrent: true,
|
||||
props: {
|
||||
ruleCopy: ruleClone
|
||||
}
|
||||
})
|
||||
},
|
||||
runNow () {
|
||||
if (this.createMode) return
|
||||
if (this.rule.status.status === 'RUNNING' || this.rule.status.status === 'UNINITIALIZED') {
|
||||
|
@ -415,10 +611,43 @@ export default {
|
|||
}
|
||||
this.$set(this, 'currentTemplate', this.templates.find((t) => t.uid === uid))
|
||||
this.rule.templateUID = uid
|
||||
this.rule.templateState = 'pending'
|
||||
},
|
||||
keepTemplate (keep) {
|
||||
if (!this.ruleCopy) return
|
||||
let newRule = this.rule
|
||||
if (keep) {
|
||||
newRule.triggers = []
|
||||
newRule.actions = []
|
||||
newRule.conditions = []
|
||||
newRule.configuration = this.ruleCopy.configuration
|
||||
newRule.templateUID = this.ruleCopy.templateUID
|
||||
newRule.templateState = 'pending'
|
||||
if (!newRule.tags?.some((t) => t.indexOf('marketplace:') === 0)) {
|
||||
const tag = this.ruleCopy.tags?.find((t) => t.indexOf('marketplace:') === 0)
|
||||
if (tag) {
|
||||
if (!newRule.tags) {
|
||||
newRule.tags = [tag]
|
||||
} else {
|
||||
newRule.tags.push(tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newRule.triggers = this.ruleCopy.triggers
|
||||
newRule.actions = this.ruleCopy.actions
|
||||
newRule.conditions = this.ruleCopy.conditions
|
||||
newRule.configuration = {}
|
||||
newRule.templateUID = null
|
||||
newRule.templateState = 'no-template'
|
||||
if (newRule.tags) {
|
||||
newRule.tags = newRule.tags.filter((t) => t.indexOf('marketplace:') !== 0)
|
||||
}
|
||||
}
|
||||
this.$set(this, 'rule', newRule)
|
||||
},
|
||||
editModule (ev, section, mod) {
|
||||
if (this.showModuleControls) return
|
||||
if (!this.isEditable) return
|
||||
if (this.showModuleControls || this.isOpaqueModule(mod)) return
|
||||
let swipeoutElement = ev.target
|
||||
ev.cancelBubble = true
|
||||
while (!swipeoutElement.classList.contains('swipeout')) {
|
||||
|
@ -452,15 +681,18 @@ export default {
|
|||
currentSection: this.currentSection,
|
||||
ruleModule: this.currentModule,
|
||||
ruleModuleType: this.currentModuleType,
|
||||
moduleTypes: this.moduleTypes
|
||||
moduleTypes: this.moduleTypes,
|
||||
readOnly: !this.isEditable
|
||||
}
|
||||
})
|
||||
|
||||
this.$f7.once('ruleModuleConfigUpdate', this.saveModule)
|
||||
this.$f7.once('ruleModuleConfigClosed', () => {
|
||||
this.$f7.off('ruleModuleConfigUpdate', this.saveModule)
|
||||
this.moduleConfigClosed()
|
||||
})
|
||||
if (this.isEditable) {
|
||||
this.$f7.once('ruleModuleConfigUpdate', this.saveModule)
|
||||
this.$f7.once('ruleModuleConfigClosed', () => {
|
||||
this.$f7.off('ruleModuleConfigUpdate', this.saveModule)
|
||||
this.moduleConfigClosed()
|
||||
})
|
||||
}
|
||||
},
|
||||
deleteModule (ev, section, mod) {
|
||||
let swipeoutElement = ev.target
|
||||
|
@ -564,7 +796,7 @@ export default {
|
|||
triggers: this.rule.triggers,
|
||||
conditions: this.rule.conditions,
|
||||
actions: this.rule.actions
|
||||
})
|
||||
}, this.isEditable ? undefined : this.replacer)
|
||||
},
|
||||
fromYaml () {
|
||||
if (!this.isEditable) return
|
||||
|
@ -579,11 +811,83 @@ export default {
|
|||
this.$f7.dialog.alert(e).open()
|
||||
return false
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Replaces CRLF (Windows) or CR (Mac) with LF in scripts before YAMLification.
|
||||
*
|
||||
* @param key the key being processed
|
||||
* @param value the value being processed
|
||||
*/
|
||||
replacer (key, value) {
|
||||
switch (key) {
|
||||
case 'script':
|
||||
return value ? value.replaceAll(/(\r\n|\r)/g, '\n') : value
|
||||
default:
|
||||
return value
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Determines if the module is "opaque" in that it doesn't actually execute the content of the module, but instead executes
|
||||
* a referenced in-memory runnable method.
|
||||
*
|
||||
* @param module the module to evaluate
|
||||
*/
|
||||
isOpaqueModule (module) {
|
||||
if (!module?.type) return false
|
||||
return module.type === 'jsr223.ScriptedAction' || module.type === 'jsr223.ScriptedCondition' || module.type === 'jsr223.ScriptedTrigger'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasTemplate () {
|
||||
return this.rule && this.currentTemplate !== null
|
||||
return this.rule && (this.stubMode || this.currentTemplate !== null)
|
||||
},
|
||||
templateName () {
|
||||
if (!this.rule || !this.rule.templateUID || !this.templates) {
|
||||
return undefined
|
||||
}
|
||||
let result = this.templates.find((t) => t.uid === this.rule.templateUID)
|
||||
return result ? result.label : this.rule.templateUID
|
||||
},
|
||||
canRegenerate () {
|
||||
if (!this.rule || !this.rule.templateUID || !this.rule.templateState || this.rule.templateState === 'no-template' || this.rule.templateState === 'template-missing') {
|
||||
return false
|
||||
}
|
||||
return this.templates ? this.templates.some((t) => t.uid === this.rule.templateUID) : false
|
||||
},
|
||||
hasOpaqueModule () {
|
||||
if (!this.rule) return false
|
||||
return [...this.rule.actions || [], this.rule.triggers || [], this.rule.conditions || []].some((m) => this.isOpaqueModule(m))
|
||||
},
|
||||
hasSource () {
|
||||
let sourceContainer = this.sourceSource
|
||||
return sourceContainer ? sourceContainer.source || sourceContainer.script : false
|
||||
},
|
||||
source () {
|
||||
let sourceContainer = this.sourceSource
|
||||
if (!sourceContainer) return ''
|
||||
return sourceContainer.source || sourceContainer.script || ''
|
||||
},
|
||||
sourceTypeText () {
|
||||
let result = this.sourceType
|
||||
return result ? AUTOMATION_LANGUAGES[result]?.name || result : result
|
||||
},
|
||||
sourceType () {
|
||||
let sourceContainer = this.sourceSource
|
||||
return sourceContainer ? sourceContainer.sourceType || sourceContainer.type : undefined
|
||||
},
|
||||
sourceSource () {
|
||||
if (!this.rule) return undefined
|
||||
if (this.rule.configuration?.source) {
|
||||
return this.rule.configuration
|
||||
}
|
||||
if (this.rule.actions?.length) {
|
||||
for (const action of this.rule.actions) {
|
||||
if (this.isOpaqueModule(action)) {
|
||||
return action.configuration
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
templateTopicLink () {
|
||||
if (!this.currentTemplate) return null
|
||||
|
|
|
@ -8,12 +8,18 @@
|
|||
<f7-nav-title v-if="ruleModule && ruleModule.new">
|
||||
Add {{ SectionLabels[currentSection][1] }}
|
||||
</f7-nav-title>
|
||||
<f7-nav-title v-else-if="readOnly">
|
||||
View {{ SectionLabels[currentSection][1] }}
|
||||
</f7-nav-title>
|
||||
<f7-nav-title v-else>
|
||||
Edit {{ SectionLabels[currentSection][1] }}
|
||||
</f7-nav-title>
|
||||
<f7-nav-right>
|
||||
<f7-link v-show="currentRuleModuleType && currentRuleModuleType.uid !== 'script.ScriptAction'" @click="updateModuleConfig">
|
||||
Save
|
||||
<f7-link v-if="!readOnly && currentRuleModuleType && dirty" @click="updateModuleConfig">
|
||||
{{ $t('dialogs.save') }}
|
||||
</f7-link>
|
||||
<f7-link v-else @click="close">
|
||||
{{ $t('dialogs.close') }}
|
||||
</f7-link>
|
||||
</f7-nav-right>
|
||||
</f7-navbar>
|
||||
|
@ -21,9 +27,9 @@
|
|||
<f7-col v-if="currentRuleModuleType" class="margin-top">
|
||||
<f7-list inline-labels no-hairlines-md class="no-margin">
|
||||
<f7-list-input type="text" :placeholder="moduleTitleSuggestion" :value="ruleModule.label" required
|
||||
@input="ruleModule.label = $event.target.value" clear-button />
|
||||
@input="ruleModule.label = $event.target.value" :disabled="readOnly" clear-button />
|
||||
<f7-list-input type="text" :placeholder="moduleDescriptionSuggestion" :value="ruleModule.description"
|
||||
@input="ruleModule.description = $event.target.value" clear-button />
|
||||
@input="ruleModule.description = $event.target.value" :disabled="readOnly" clear-button />
|
||||
</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> -->
|
||||
|
@ -44,14 +50,14 @@
|
|||
</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" />
|
||||
<condition-module-wizard v-else-if="!advancedTypePicker && currentSection === 'conditions'" :current-module="ruleModule" :current-module-type="currentRuleModuleType" :module-types="moduleTypes['conditions']" @typeSelect="setModuleType" @showAdvanced="advancedTypePicker = true" @startScript="startScripting" />
|
||||
<action-module-wizard v-else-if="!advancedTypePicker && currentSection === 'actions'" :current-module="ruleModule" :current-module-type="currentRuleModuleType" :module-types="moduleTypes['actions']" @typeSelect="setModuleType" @showAdvanced="advancedTypePicker = true" @startScript="startScripting" />
|
||||
</f7-col>
|
||||
|
||||
<!-- module configuration -->
|
||||
<f7-col v-if="ruleModule.type && (!ruleModule.new || advancedTypePicker)" class="margin-top">
|
||||
<f7-list>
|
||||
<f7-list-item :title="SectionLabels[currentSection][0]" ref="ruleModuleTypeSmartSelect" smart-select :smart-select-params="{ view: $f7.views.main, openIn: 'popup', closeOnSelect: true }">
|
||||
<f7-list-item :disabled="readOnly" :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()), true)">
|
||||
<optgroup v-for="(mt, scope) in groupedModuleTypes(currentSection)" :key="scope" :label="scope">
|
||||
|
@ -74,6 +80,7 @@
|
|||
:parameterGroups="[]"
|
||||
:parameters="currentRuleModuleType.configDescriptions"
|
||||
:configuration="ruleModule.configuration"
|
||||
:readOnly="readOnly"
|
||||
@updated="dirty = true" />
|
||||
<f7-block v-else>
|
||||
<f7-button @click="editBlockly" color="blue" outline fill>
|
||||
|
@ -105,7 +112,7 @@ export default {
|
|||
ActionModuleWizard,
|
||||
ConfigSheet
|
||||
},
|
||||
props: ['rule', 'ruleModule', 'ruleModuleType', 'moduleTypes', 'currentSection'],
|
||||
props: ['rule', 'ruleModule', 'ruleModuleType', 'moduleTypes', 'currentSection', 'readOnly'],
|
||||
data () {
|
||||
return {
|
||||
currentRuleModuleType: this.ruleModuleType,
|
||||
|
@ -187,6 +194,9 @@ export default {
|
|||
} else {
|
||||
this.$refs.modulePopup.close()
|
||||
}
|
||||
},
|
||||
close () {
|
||||
this.$refs.modulePopup.close()
|
||||
}
|
||||
},
|
||||
created () {
|
||||
|
|
|
@ -20,8 +20,8 @@
|
|||
</f7-subnavbar>
|
||||
</f7-navbar>
|
||||
<f7-toolbar class="contextual-toolbar" :class="{ 'navbar': $theme.md }" v-if="showCheckboxes" bottom-ios bottom-aurora>
|
||||
<f7-link color="red" v-show="selectedItems.length" v-if="!$theme.md" class="delete" icon-ios="f7:trash" icon-aurora="f7:trash" @click="removeSelected">
|
||||
Remove {{ selectedItems.length }}
|
||||
<f7-link color="red" v-show="selectedDeletableItems.length" v-if="!$theme.md" class="delete" icon-ios="f7:trash" icon-aurora="f7:trash" @click="removeSelected">
|
||||
Remove {{ selectedDeletableItems.length }}
|
||||
</f7-link>
|
||||
<f7-link color="orange" v-show="selectedItems.length" v-if="!$theme.md && !showScenes" class="disable" @click="doDisableEnableSelected(false)" icon-ios="f7:pause_circle" icon-aurora="f7:pause_circle">
|
||||
Disable {{ selectedItems.length }}
|
||||
|
@ -29,14 +29,18 @@
|
|||
<f7-link color="green" v-show="selectedItems.length" v-if="!$theme.md && !showScenes" class="enable" @click="doDisableEnableSelected(true)" icon-ios="f7:play_circle" icon-aurora="f7:play_circle">
|
||||
Enable {{ selectedItems.length }}
|
||||
</f7-link>
|
||||
<!-- <f7-link color="deeppurple" v-show="selectedItems.length === 1 && canRegenerateItem(selectedItems[0])" v-if="!$theme.md && !showScenes" class="enable" @click="regenerateSelected()" icon-ios="f7:arrow_2_circlepath" icon-aurora="f7:arrow_2_circlepath">
|
||||
Regenerate from template
|
||||
</f7-link> -->
|
||||
<f7-link v-if="$theme.md" icon-md="material:close" icon-color="white" @click="showCheckboxes = false" />
|
||||
<div class="title" v-if="$theme.md">
|
||||
{{ selectedItems.length }} selected
|
||||
</div>
|
||||
<div class="right" v-if="$theme.md">
|
||||
<f7-link v-if="!showScenes" v-show="selectedItems.length === 1 && canRegenerateItem(selectedItems[0])" tooltip="Regenerate selected from template" icon-md="material:autorenew" icon-color="white" @click="regenerateSelected()" />
|
||||
<f7-link v-if="!showScenes" v-show="selectedItems.length" tooltip="Disable selected" icon-md="material:pause_circle_outline" icon-color="white" @click="doDisableEnableSelected(false)" />
|
||||
<f7-link v-if="!showScenes" v-show="selectedItems.length" tooltip="Enable selected" icon-md="material:play_circle_outline" icon-color="white" @click="doDisableEnableSelected(true)" />
|
||||
<f7-link v-show="selectedItems.length" icon-md="material:delete" icon-color="white" @click="removeSelected" />
|
||||
<f7-link v-show="selectedDeletableItems.length" icon-md="material:delete" icon-color="white" @click="removeSelected" />
|
||||
</div>
|
||||
</f7-toolbar>
|
||||
|
||||
|
@ -130,7 +134,10 @@
|
|||
:footer="rule.description"
|
||||
:badge="showScenes ? '' : ruleStatusBadgeText(ruleStatuses[rule.uid])"
|
||||
:badge-color="ruleStatusBadgeColor(ruleStatuses[rule.uid])">
|
||||
<div slot="footer">
|
||||
<div slot="footer" class="footer-inner">
|
||||
<f7-chip v-if="rule.templateUID" :text="templateName(rule)" media-bg-color="orange" style="margin-right: 2px">
|
||||
<f7-icon slot="media" ios="f7:doc_on_doc_fill" md="material:file_copy" aurora="f7:doc_on_doc_fill" />
|
||||
</f7-chip>
|
||||
<f7-chip v-for="tag in rule.tags.filter((t) => t !== 'Script' && t !== 'Scene')" :key="tag" :text="tag" media-bg-color="blue" style="margin-right: 6px">
|
||||
<f7-icon slot="media" ios="f7:tag_fill" md="material:label" aurora="f7:tag_fill" />
|
||||
</f7-chip>
|
||||
|
@ -150,6 +157,14 @@
|
|||
</f7-page>
|
||||
</template>
|
||||
|
||||
<style lang="stylus">
|
||||
.item-footer
|
||||
margin-block-start 4px
|
||||
margin-block-end 2px
|
||||
.footer-inner
|
||||
margin-block-start 2px
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import RuleStatus from '@/components/rule/rule-status-mixin'
|
||||
|
||||
|
@ -170,8 +185,10 @@ export default {
|
|||
uniqueTags: [],
|
||||
selectedTags: [],
|
||||
selectedItems: [],
|
||||
selectedDeletableItems: [],
|
||||
showCheckboxes: false,
|
||||
eventSource: null
|
||||
eventSource: null,
|
||||
templates: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -218,6 +235,7 @@ export default {
|
|||
this.initSearchbar = false
|
||||
|
||||
this.$set(this, 'selectedItems', [])
|
||||
this.$set(this, 'selectedDeletableItems', [])
|
||||
this.showCheckboxes = false
|
||||
let filter = ''
|
||||
if (this.showScripts) {
|
||||
|
@ -227,47 +245,65 @@ export default {
|
|||
filter = '&tags=Scene'
|
||||
}
|
||||
|
||||
this.$oh.api.get('/rest/rules?summary=true' + filter).then(data => {
|
||||
this.rules = data.sort((a, b) => {
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
|
||||
if (!this.showScripts) {
|
||||
this.rules = this.rules.filter((r) => !r.tags || r.tags.indexOf('Script') < 0)
|
||||
const promises = [this.$oh.api.get('/rest/templates'), this.$oh.api.get('/rest/rules?summary=true' + filter)]
|
||||
Promise.allSettled(promises).then((results) => {
|
||||
const templateData = results[0]
|
||||
const ruleData = results[1]
|
||||
if (templateData.status === 'fulfilled') {
|
||||
this.$set(this, 'templates', templateData.value)
|
||||
} else {
|
||||
console.warn('Failed to retrieve rule templates. Status: "' + templateData.status + '", Reason: "' + templateData.reason + '"')
|
||||
}
|
||||
|
||||
if (!this.showScenes) {
|
||||
this.rules = this.rules.filter((r) => !r.tags || r.tags.indexOf('Scene') < 0)
|
||||
}
|
||||
|
||||
this.rules.forEach(rule => {
|
||||
this.ruleStatuses[rule.uid] = rule.status
|
||||
|
||||
rule.tags.forEach(t => {
|
||||
if (t === 'Scene' || t === 'Script') return
|
||||
if (t.startsWith('marketplace:')) t = 'Marketplace'
|
||||
if (!this.uniqueTags.includes(t)) this.uniqueTags.push(t)
|
||||
if (ruleData.status === 'fulfilled') {
|
||||
let rules = ruleData.value.sort((a, b) => {
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
})
|
||||
|
||||
this.uniqueTags.sort()
|
||||
this.initSearchbar = true
|
||||
|
||||
this.loading = false
|
||||
this.ready = true
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.listIndex) this.$refs.listIndex.update()
|
||||
if (this.$device.desktop && this.$refs.searchbar) {
|
||||
this.$refs.searchbar.f7Searchbar.$inputEl[0].focus()
|
||||
if (!this.showScripts) {
|
||||
rules = rules.filter((r) => !r.tags || r.tags.indexOf('Script') < 0)
|
||||
}
|
||||
this.$refs.searchbar?.f7Searchbar.search(this.$f7.data[`last${this.type}SearchQuery`] || '')
|
||||
})
|
||||
|
||||
if (!this.eventSource) this.startEventSource()
|
||||
}).catch((err, status) => {
|
||||
if (err === 'Not Found' || status === 404) {
|
||||
this.noRuleEngine = true
|
||||
if (!this.showScenes) {
|
||||
rules = rules.filter((r) => !r.tags || r.tags.indexOf('Scene') < 0)
|
||||
}
|
||||
this.$set(this, 'rules', rules)
|
||||
|
||||
rules.forEach(rule => {
|
||||
this.ruleStatuses[rule.uid] = rule.status
|
||||
|
||||
rule.tags.forEach(t => {
|
||||
if (t === 'Scene' || t === 'Script') return
|
||||
if (t.startsWith('marketplace:')) t = 'Marketplace'
|
||||
if (!this.uniqueTags.includes(t)) this.uniqueTags.push(t)
|
||||
})
|
||||
})
|
||||
|
||||
this.uniqueTags.sort()
|
||||
this.initSearchbar = true
|
||||
|
||||
this.loading = false
|
||||
this.ready = true
|
||||
this.noRuleEngine = false
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.listIndex) this.$refs.listIndex.update()
|
||||
if (this.$device.desktop && this.$refs.searchbar) {
|
||||
this.$refs.searchbar.f7Searchbar.$inputEl[0].focus()
|
||||
}
|
||||
this.$refs.searchbar?.f7Searchbar.search(this.$f7.data[`last${this.type}SearchQuery`] || '')
|
||||
})
|
||||
|
||||
if (!this.eventSource) this.startEventSource()
|
||||
} else {
|
||||
console.warn('Failed to retrieve rule templates. Status: "' + ruleData.status + '", Reason: "' + ruleData.reason + '"')
|
||||
if (ruleData.reason === 'Not Found') {
|
||||
this.noRuleEngine = true
|
||||
}
|
||||
this.loading = false
|
||||
let self = this
|
||||
setTimeout(() => {
|
||||
self.load()
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
@ -306,7 +342,7 @@ export default {
|
|||
if (this.showCheckboxes) {
|
||||
this.toggleItemCheck(event, item.uid, item)
|
||||
} else {
|
||||
this.$f7router.navigate((item.editable) ? item.uid : '/settings/scripts/' + item.uid)
|
||||
this.$f7router.navigate(item.uid)
|
||||
}
|
||||
},
|
||||
ctrlClick (event, item) {
|
||||
|
@ -317,15 +353,23 @@ export default {
|
|||
if (!this.showCheckboxes) this.showCheckboxes = true
|
||||
if (this.isChecked(item)) {
|
||||
this.selectedItems.splice(this.selectedItems.indexOf(item), 1)
|
||||
let idx = this.selectedDeletableItems.indexOf(item)
|
||||
if (idx >= 0) {
|
||||
this.selectedDeletableItems.splice(idx, 1)
|
||||
}
|
||||
} else {
|
||||
this.selectedItems.push(item)
|
||||
const rule = this.rules.find((r) => r.uid === item)
|
||||
if (rule?.editable) {
|
||||
this.selectedDeletableItems.push(item)
|
||||
}
|
||||
}
|
||||
},
|
||||
removeSelected () {
|
||||
const vm = this
|
||||
|
||||
this.$f7.dialog.confirm(
|
||||
`Remove ${this.selectedItems.length} selected rules?`,
|
||||
`Remove ${this.selectedDeletableItems.length} rules?`,
|
||||
'Remove Rules',
|
||||
() => {
|
||||
vm.doRemoveSelected()
|
||||
|
@ -333,14 +377,9 @@ export default {
|
|||
)
|
||||
},
|
||||
doRemoveSelected () {
|
||||
if (this.selectedItems.some((i) => this.rules.find((rule) => rule.uid === i).editable === false)) {
|
||||
this.$f7.dialog.alert('Some of the selected rules are not modifiable because they have been provisioned by files')
|
||||
return
|
||||
}
|
||||
|
||||
let dialog = this.$f7.dialog.progress('Deleting Rules...')
|
||||
|
||||
const promises = this.selectedItems.map((i) => this.$oh.api.delete('/rest/rules/' + i))
|
||||
const promises = this.selectedDeletableItems.map((i) => this.$oh.api.delete('/rest/rules/' + i))
|
||||
Promise.all(promises).then((data) => {
|
||||
this.$f7.toast.create({
|
||||
text: 'Rules removed',
|
||||
|
@ -348,6 +387,7 @@ export default {
|
|||
closeTimeout: 2000
|
||||
}).open()
|
||||
this.selectedItems = []
|
||||
this.selectedDeletableItems = []
|
||||
dialog.close()
|
||||
this.load()
|
||||
}).catch((err) => {
|
||||
|
@ -377,6 +417,39 @@ export default {
|
|||
this.$f7.dialog.alert('An error occurred while enabling/disabling: ' + err)
|
||||
})
|
||||
},
|
||||
regenerateSelected () {
|
||||
if (!this.selectedItems || this.selectedItems.length !== 1) {
|
||||
return
|
||||
}
|
||||
let selectedRule = this.rules.find((rule) => rule.uid === this.selectedItems[0])
|
||||
if (!selectedRule) {
|
||||
return
|
||||
}
|
||||
if (selectedRule.editable) {
|
||||
this.$oh.api.get('/rest/rules/' + this.selectedItems[0]).then((rule) => {
|
||||
this.$f7router.navigate({
|
||||
url: '/settings/rules/stub'
|
||||
}, {
|
||||
reloadCurrent: false,
|
||||
props: {
|
||||
ruleCopy: rule
|
||||
}
|
||||
})
|
||||
}).catch((err) => {
|
||||
this.$f7.dialog.alert('An error occurred when retrieving rule "' + this.selectedItems[0] + '": ' + err)
|
||||
})
|
||||
} else {
|
||||
this.$oh.api.postPlain('/rest/rules/' + this.selectedItems[0] + '/regenerate').then(() => {
|
||||
this.$f7.toast.create({
|
||||
text: 'Rule regenerated from template',
|
||||
destroyOnClose: true,
|
||||
closeTimeout: 2000
|
||||
}).open()
|
||||
}).catch((err) => {
|
||||
this.$f7.dialog.alert('An error occurred when trying to regenerate rule "' + this.selectedItems[0] + '" from template: ' + err)
|
||||
})
|
||||
}
|
||||
},
|
||||
toggleSearchTag (e, item) {
|
||||
const idx = this.selectedTags.indexOf(item)
|
||||
if (idx !== -1) {
|
||||
|
@ -389,6 +462,23 @@ export default {
|
|||
},
|
||||
isTagSelected (tag) {
|
||||
return this.selectedTags.includes(tag)
|
||||
},
|
||||
templateName (rule) {
|
||||
let template = this.templates ? this.templates.find((t) => t.uid === rule.templateUID) : undefined
|
||||
return template ? template.label : rule.templateUID
|
||||
},
|
||||
canRegenerateItem (item) {
|
||||
if (!this.rules) {
|
||||
return false
|
||||
}
|
||||
let rule = this.rules.find((r) => r.uid === item)
|
||||
return rule ? this.canRegenerate(rule) : false
|
||||
},
|
||||
canRegenerate (rule) {
|
||||
if (!rule || !rule.templateUID || !rule.templateState || rule.templateState === 'no-template' || rule.templateState === 'template-missing') {
|
||||
return false
|
||||
}
|
||||
return this.templates ? this.templates.some((t) => t.uid === rule.templateUID) : false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
Create
|
||||
</f7-link>
|
||||
</template>
|
||||
<f7-link v-if="!editable" icon-f7="lock_fill" icon-only tooltip="This script is not editable through the UI" />
|
||||
</f7-nav-right>
|
||||
</f7-navbar>
|
||||
|
||||
|
@ -75,7 +76,7 @@
|
|||
</f7-toolbar>
|
||||
|
||||
<f7-icon v-if="!createMode && (!isBlockly && !editable) || (blocklyCodePreview && isBlockly)" f7="lock" class="float-right margin" style="opacity:0.5; z-index: 4000; user-select: none;" size="50" color="gray"
|
||||
:tooltip="(isBlockly) ? 'Cannot edit the code generated by Blockly' : 'This rule is not editable because it has been provisioned from a file'" />
|
||||
:tooltip="(isBlockly) ? 'Cannot edit the code generated by Blockly' : 'This code is not editable'" />
|
||||
<editor v-if="!createMode && (!isBlockly || blocklyCodePreview)" class="rule-script-editor" :mode="mode" :value="script" @input="onEditorInput" :read-only="isBlockly || !editable" :tern-autocompletion-hook="true" />
|
||||
<blockly-editor ref="blocklyEditor" v-else-if="!createMode && isBlockly" :blocks="currentModule.configuration.blockSource" @change="scriptDirty = true" @mounted="onBlocklyMounted" @ready="onBlocklyReady" />
|
||||
<script-general-settings v-else-if="createMode" :createMode="true" :rule="rule" />
|
||||
|
@ -127,7 +128,7 @@
|
|||
</f7-link>
|
||||
</div>
|
||||
</f7-toolbar>
|
||||
<script-general-settings class="margin-top" :createMode="createMode" :rule="rule" :module="currentModule" :module-type="scriptModuleType" :module-types="moduleTypes" :isScriptRule="isScriptRule" :mode="mode" :languages="languages" @newLanguage="changeLanguage" />
|
||||
<script-general-settings class="margin-top" :createMode="createMode" :rule="rule" :module="currentModule" :module-type="scriptModuleType" :isScriptRule="isScriptRule" :mode="mode" :languages="languages" @newLanguage="changeLanguage" />
|
||||
<f7-block class="block-narrow" v-if="editable && isScriptRule">
|
||||
<f7-col>
|
||||
<f7-list>
|
||||
|
@ -192,12 +193,6 @@ export default {
|
|||
mode: '',
|
||||
savedMode: '',
|
||||
|
||||
moduleTypes: {
|
||||
actions: [],
|
||||
conditions: [],
|
||||
triggers: []
|
||||
},
|
||||
|
||||
currentModuleConfig: {},
|
||||
scriptModuleType: null,
|
||||
languages: null,
|
||||
|
@ -216,7 +211,7 @@ export default {
|
|||
if (this.createMode) return 'Create Script'
|
||||
if (this.isScriptRule) return this.rule.name
|
||||
if (this.currentModule) {
|
||||
let title = 'Edit'
|
||||
let title = this.editable ? 'Edit' : 'View'
|
||||
switch (this.currentModule.type) {
|
||||
case 'script.ScriptAction':
|
||||
case 'script.ScriptCondition':
|
||||
|
@ -229,7 +224,7 @@ export default {
|
|||
}
|
||||
return title
|
||||
}
|
||||
return 'Edit Script'
|
||||
return this.editable ? 'Edit Script' : 'View Script'
|
||||
},
|
||||
editable () {
|
||||
return this.rule && this.rule.editable !== false
|
||||
|
@ -351,7 +346,7 @@ export default {
|
|||
if (this.ruleCopy) this.rule.uid = this.$f7.utils.id()
|
||||
this.savedRule = cloneDeep(this.rule)
|
||||
this.savedMode = this.mode = 'application/javascript+blockly'
|
||||
this.loadScriptModuleTypes().then(() => {
|
||||
this.loadScriptModuleType().then(() => {
|
||||
this.ready = true
|
||||
})
|
||||
},
|
||||
|
@ -395,17 +390,17 @@ export default {
|
|||
return this.languages.map(l => l.contentType).includes(mimeType)
|
||||
},
|
||||
mimeTypeDescription (mode) {
|
||||
return AUTOMATION_LANGUAGES[mode]?.name || mode
|
||||
return mode ? AUTOMATION_LANGUAGES[mode]?.name || mode : mode
|
||||
},
|
||||
documentationLink (mode) {
|
||||
return AUTOMATION_LANGUAGES[mode]?.documentationLink
|
||||
return mode ? AUTOMATION_LANGUAGES[mode]?.documentationLink : mode
|
||||
},
|
||||
/**
|
||||
* Load the script module type, i.e. the available script languages
|
||||
* @returns {Promise}
|
||||
*/
|
||||
loadScriptModuleTypes () {
|
||||
return this.$oh.api.get('/rest/module-types/script.ScriptAction').then((data) => {
|
||||
loadScriptModuleType () {
|
||||
return this.$oh.api.get('/rest/module-types/' + (this.currentModule?.type ? this.currentModule.type : 'script.ScriptAction')).then((data) => {
|
||||
this.$set(this, 'scriptModuleType', data)
|
||||
let languages = this.scriptModuleType.configDescriptions
|
||||
.find((c) => c.name === 'type').options
|
||||
|
@ -425,10 +420,8 @@ export default {
|
|||
if (this.loading) return
|
||||
this.loading = true
|
||||
|
||||
Promise.all([this.$oh.api.get('/rest/module-types?type=trigger'), this.$oh.api.get('/rest/module-types?type=condition'), this.$oh.api.get('/rest/rules/' + this.ruleId)]).then((data) => {
|
||||
this.$set(this.moduleTypes, 'triggers', data[0])
|
||||
this.$set(this.moduleTypes, 'conditions', data[1])
|
||||
this.$set(this, 'rule', data[2])
|
||||
this.$oh.api.get('/rest/rules/' + this.ruleId).then((data) => {
|
||||
this.$set(this, 'rule', data)
|
||||
|
||||
if (this.moduleId) {
|
||||
this.$set(this, 'currentModule', this.rule.actions.concat(this.rule.conditions).find((m) => m.id === this.moduleId))
|
||||
|
@ -441,34 +434,7 @@ export default {
|
|||
|
||||
this.initDirty()
|
||||
|
||||
if (!this.rule.editable) {
|
||||
const commentChar = AUTOMATION_LANGUAGES[this.mode]?.commentChar
|
||||
let preamble = `${commentChar} Triggers:\n`
|
||||
for (const trigger of this.rule.triggers) {
|
||||
const triggerModuleType = this.moduleTypes.triggers.find((t) => t.uid === trigger.type)
|
||||
let description = trigger.label || this.suggestedModuleTitle(trigger, triggerModuleType, 'trigger')
|
||||
if (triggerModuleType.uid === 'timer.GenericCronTrigger') {
|
||||
description = description.charAt(0).toUpperCase() + description.slice(1)
|
||||
} else {
|
||||
description = 'When ' + description
|
||||
}
|
||||
preamble += `${commentChar} - ${description}\n`
|
||||
}
|
||||
|
||||
if (this.rule.conditions.length > 0) {
|
||||
preamble += `\n${commentChar} Conditions:\n`
|
||||
for (const condition of this.rule.conditions) {
|
||||
const conditionModuleType = this.moduleTypes.conditions.find((t) => t.uid === condition.type)
|
||||
let description = condition.label || this.suggestedModuleTitle(condition, conditionModuleType, 'condition')
|
||||
description = 'Only If ' + description
|
||||
preamble += `${commentChar} - ${description}\n`
|
||||
}
|
||||
}
|
||||
|
||||
this.script = preamble + '\n' + this.script
|
||||
}
|
||||
|
||||
this.loadScriptModuleTypes().then(() => {
|
||||
this.loadScriptModuleType().then(() => {
|
||||
if (this.rule.editable && this.mode === 'application/javascript;version=ECMAScript-2021') {
|
||||
const message = 'Your JavaScript script was created with a previous version of openHAB. Please save your script.'
|
||||
|
||||
|
@ -697,6 +663,12 @@ export default {
|
|||
case 'state':
|
||||
this.$set(this.rule, 'status', JSON.parse(event.payload)) // e.g. {"status":"RUNNING","statusDetail":"NONE"}
|
||||
break
|
||||
case 'added':
|
||||
case 'updated':
|
||||
if (!this.dirty) {
|
||||
this.load()
|
||||
}
|
||||
break
|
||||
}
|
||||
})
|
||||
},
|
||||
|
|
|
@ -5,13 +5,13 @@
|
|||
<f7-col v-if="!createMode && languages">
|
||||
<f7-list inline-labels style="margin-top: 0">
|
||||
<template v-if="module && !isScriptRule">
|
||||
<f7-list-input :label="scriptType + ' Title'" type="text" :value="module.label" :placeholder="suggestedModuleTitle(module, moduleType)" @input="$set(module, 'label', $event.target.value)" :disabled="!editable" :clear-button="editable" />
|
||||
<f7-list-input label="Description" type="text" :value="module.description" :placeholder="suggestedModuleDescription(module, moduleType)" @input="$set(module, 'description', $event.target.value)" :disabled="!editable" :clear-button="editable" />
|
||||
<f7-list-input :label="scriptType + ' Title'" type="text" :value="moduleTitle" :placeholder="sugModuleTitle" @input="$set(module, 'label', $event.target.value)" :disabled="!editable" :clear-button="editable" />
|
||||
<f7-list-input label="Description" type="text" :value="moduleDescription" :placeholder="sugModuleDescription" @input="$set(module, 'description', $event.target.value)" :disabled="!editable" :clear-button="editable" />
|
||||
</template>
|
||||
<f7-list-item title="Scripting Language" class="aligned-smart-select" :disabled="!editable" :key="mode" smart-select :smart-select-params="{openIn: 'sheet', closeOnSelect: true}">
|
||||
<select @change="$emit('newLanguage', $event.target.value)">
|
||||
<option v-if="!languages.map(l => l.contentType).includes(mode)" :key="mode" :value="mode" selected="true">
|
||||
{{ mode }} (not installed)
|
||||
{{ mode ? mode + ' (not installed)' : 'Unknown' }}
|
||||
</option>
|
||||
<option v-for="language in languages" :key="language.contentType" :value="language.contentType" :selected="language.contentType === mode">
|
||||
{{ language.name }} ({{ language.version }})
|
||||
|
@ -30,20 +30,38 @@ import ModuleDescriptionSuggestions from '../module-description-suggestions'
|
|||
|
||||
export default {
|
||||
mixins: [ModuleDescriptionSuggestions],
|
||||
props: ['rule', 'module', 'moduleType', 'moduleTypes', 'createMode', 'isScriptRule', 'languages', 'mode'],
|
||||
props: ['rule', 'module', 'moduleType', 'createMode', 'isScriptRule', 'languages', 'mode'],
|
||||
emits: ['newLanguage'],
|
||||
components: {
|
||||
RuleGeneralSettings
|
||||
},
|
||||
computed: {
|
||||
moduleTitle () {
|
||||
return this.editable || this.module?.label ? this.module.label : this.sugModuleTitle
|
||||
},
|
||||
sugModuleTitle () {
|
||||
return this.suggestedModuleTitle(this.module, this.moduleType)
|
||||
},
|
||||
moduleDescription () {
|
||||
return this.editable || this.module?.description ? this.module.description : this.sugModuleDescription
|
||||
},
|
||||
sugModuleDescription () {
|
||||
return this.suggestedModuleDescription(this.module, this.moduleType)
|
||||
},
|
||||
editable () {
|
||||
return this.createMode || (this.rule && this.rule.editable)
|
||||
},
|
||||
scriptType () {
|
||||
switch (this.module.type) {
|
||||
case 'script.ScriptAction':
|
||||
case 'jsr223.ScriptedAction':
|
||||
return 'Action'
|
||||
case 'script.ScriptCondition':
|
||||
return this.module.type.slice('script.Script'.length)
|
||||
case 'jsr223.ScriptedCondition':
|
||||
return 'Condition'
|
||||
case 'script.ScriptTrigger':
|
||||
case 'jsr223.ScriptedTrigger':
|
||||
return 'Trigger'
|
||||
default:
|
||||
return 'Module'
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue