Implement copy functionality for Things, Items, scenes & scripts (#2855)

Also improve the rule copy code.

Signed-off-by: Florian Hotze <dev@florianhotze.com>
pull/2857/head
Florian Hotze 2024-11-02 00:20:05 +01:00 committed by GitHub
parent 3022654195
commit c74af2dad3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 170 additions and 66 deletions

View File

@ -190,6 +190,11 @@ export default [
beforeEnter: [enforceAdminForRoute],
async: loadAsync(ItemEditPage, { createMode: true })
},
{
path: 'copy',
beforeEnter: [enforceAdminForRoute],
async: loadAsync(ItemEditPage, { createMode: true })
},
{
path: 'add-from-textual-definition',
beforeEnter: [enforceAdminForRoute],
@ -266,10 +271,6 @@ export default [
beforeEnter: [enforceAdminForRoute],
async: loadAsync(AddThingChooseBindingPage),
routes: [
// {
// path: 'install-binding',
// async: loadAsync(AddonsAddPage, { addonType: 'binding' })
// },
{
path: ':bindingId',
beforeEnter: [enforceAdminForRoute],
@ -284,6 +285,12 @@ export default [
}
]
},
{
path: 'copy',
beforeEnter: [enforceAdminForRoute],
beforeLeave: [checkDirtyBeforeLeave],
async: loadAsync(AddThingPage)
},
{
path: 'inbox',
beforeEnter: [enforceAdminForRoute],
@ -321,14 +328,23 @@ export default [
beforeEnter: [enforceAdminForRoute],
async: loadAsync(RulesListPage),
routes: [
{
path: 'add',
beforeEnter: [enforceAdminForRoute],
beforeLeave: [checkDirtyBeforeLeave],
async: loadAsync(RuleEditPage, { createMode: true })
},
{
path: 'copy',
beforeEnter: [enforceAdminForRoute],
beforeLeave: [checkDirtyBeforeLeave],
async: loadAsync(RuleEditPage, { createMode: true })
},
{
path: ':ruleId',
beforeEnter: [enforceAdminForRoute],
beforeLeave: [checkDirtyBeforeLeave],
async: loadAsync(RuleEditPage, (routeTo) =>
routeTo.params.ruleId === 'add' ? { createMode: true }
: routeTo.params.ruleId === 'copy' ? { copyMode: true }
: {}),
async: loadAsync(RuleEditPage),
routes: [
{
path: 'script/:moduleId',
@ -345,11 +361,23 @@ export default [
beforeEnter: [enforceAdminForRoute],
async: loadAsync(RulesListPage, { showScenes: true }),
routes: [
{
path: 'add',
beforeEnter: [enforceAdminForRoute],
beforeLeave: [checkDirtyBeforeLeave],
async: loadAsync(SceneEditPage, { createMode: true })
},
{
path: 'copy',
beforeEnter: [enforceAdminForRoute],
beforeLeave: [checkDirtyBeforeLeave],
async: loadAsync(SceneEditPage, { createMode: true })
},
{
path: ':ruleId',
beforeEnter: [enforceAdminForRoute],
beforeLeave: [checkDirtyBeforeLeave],
async: loadAsync(SceneEditPage, (routeTo) => (routeTo.params.ruleId === 'add') ? { createMode: true } : {})
async: loadAsync(SceneEditPage)
}
]
},
@ -358,11 +386,23 @@ export default [
beforeEnter: [enforceAdminForRoute],
async: loadAsync(RulesListPage, { showScripts: true }),
routes: [
{
path: 'add',
beforeEnter: [enforceAdminForRoute],
beforeLeave: [checkDirtyBeforeLeave],
async: loadAsync(ScriptEditPage, { createMode: true })
},
{
path: 'copy',
beforeEnter: [enforceAdminForRoute],
beforeLeave: [checkDirtyBeforeLeave],
async: loadAsync(ScriptEditPage, { createMode: true })
},
{
path: ':ruleId',
beforeEnter: [enforceAdminForRoute],
beforeLeave: [checkDirtyBeforeLeave],
async: loadAsync(ScriptEditPage, (routeTo) => (routeTo.params.ruleId === 'add') ? { createMode: true } : {})
async: loadAsync(ScriptEditPage)
}
]
},

View File

@ -87,6 +87,9 @@
<f7-row>
<f7-col>
<f7-list>
<f7-list-button color="blue" @click="copyItem">
Copy Item
</f7-list-button>
<f7-list-button v-if="item.editable" color="red" @click="deleteItem">
Remove Item
</f7-list-button>
@ -153,6 +156,8 @@
</style>
<script>
import cloneDeep from 'lodash/cloneDeep'
import ItemStatePreview from '@/components/item/item-state-preview.vue'
import LinkDetails from '@/components/model/link-details.vue'
import GroupMembers from '@/components/item/group-members.vue'
@ -202,6 +207,16 @@ export default {
this.iconUrl = '/icon/' + this.item.category + '?format=svg'
})
},
copyItem () {
let itemClone = cloneDeep(this.item)
this.$f7router.navigate({
url: '/settings/items/copy'
}, {
props: {
itemCopy: itemClone
}
})
},
deleteItem () {
this.$f7.dialog.confirm(
`Are you sure you want to delete ${this.item.label || this.item.name}?`,

View File

@ -73,7 +73,7 @@ import ItemMixin from '@/components/item/item-mixin'
export default {
mixins: [DirtyMixin, ItemMixin],
props: ['itemName', 'createMode'],
props: ['itemName', 'createMode', 'itemCopy'],
components: {
ItemForm,
'editor': () => import(/* webpackChunkName: "script-editor" */ '@/components/config/controls/script-editor.vue')
@ -144,7 +144,7 @@ export default {
if (this.loading) return
this.loading = true
if (this.createMode) {
const newItem = {
const newItem = this.itemCopy || {
name: '',
label: '',
category: '',

View File

@ -1,6 +1,6 @@
<template>
<f7-page @page:afterin="onPageAfterIn" @page:afterout="onPageAfterOut">
<f7-navbar :title="(isNewRule ? 'Create rule' : rule.name) + dirtyIndicator" back-link="Back" no-hairline>
<f7-navbar :title="(createMode ? 'Create rule' : rule.name) + dirtyIndicator" back-link="Back" no-hairline>
<f7-nav-right>
<developer-dock-icon />
<template v-if="isEditable">
@ -21,8 +21,8 @@
</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 && (!isNewRule)" class="block-narrow padding-left padding-right" strong>
<f7-col v-if="!isNewRule">
<f7-block v-if="ready && rule.status && (!createMode)" class="block-narrow padding-left padding-right" strong>
<f7-col v-if="!createMode">
<div class="float-right align-items-flex-start align-items-center">
<!-- <f7-toggle class="enable-toggle"></f7-toggle> -->
<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" />
@ -42,7 +42,7 @@
</f7-col>
</f7-block>
<!-- skeletons for not ready -->
<f7-block v-else-if="!isNewRule" class="block-narrow padding-left padding-right skeleton-text skeleton-effect-blink" strong>
<f7-block v-else-if="!createMode" class="block-narrow padding-left padding-right skeleton-text skeleton-effect-blink" strong>
<f7-col>
______:
<f7-chip class="margin-left" text="________" />
@ -53,7 +53,7 @@
</f7-col>
</f7-block>
<rule-general-settings :rule="rule" :ready="ready" :createMode="isNewRule" :hasTemplate="hasTemplate" />
<rule-general-settings :rule="rule" :ready="ready" :createMode="createMode" :hasTemplate="hasTemplate" />
<f7-block v-if="ready" class="block-narrow">
<f7-block-footer v-if="!isEditable" class="no-margin padding-left">
@ -125,7 +125,7 @@
</f7-list>
</div>
</f7-col>
<f7-col v-if="isEditable && (!isNewRule)">
<f7-col v-if="isEditable && (!createMode)">
<f7-list>
<f7-list-button color="blue" @click="copyRule">
Copy Rule
@ -198,7 +198,7 @@ export default {
ConfigSheet,
'editor': () => import(/* webpackChunkName: "script-editor" */ '@/components/config/controls/script-editor.vue')
},
props: ['ruleId', 'createMode', 'copyMode', 'ruleCopy', 'schedule'],
props: ['ruleId', 'createMode', 'ruleCopy', 'schedule'],
data () {
return {
SECTION_LABELS: {
@ -270,7 +270,7 @@ export default {
this.moduleTypes.conditions = data[1]
this.moduleTypes.triggers = data[2]
if (this.createMode) {
this.$set(this, 'rule', {
const newRule = this.ruleCopy || {
uid: this.$f7.utils.id(),
name: '',
triggers: [],
@ -283,28 +283,14 @@ export default {
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)
loadingFinished()
})
// no need for an event source, the rule doesn't exist yet
} else if (this.copyMode) {
this.$set(this, 'rule', {
uid: this.$f7.utils.id(),
name: this.ruleCopy.name + ' Copy',
triggers: this.ruleCopy.triggers,
actions: this.ruleCopy.actions,
conditions: this.ruleCopy.conditions,
tags: this.ruleCopy.tags,
configuration: this.ruleCopy.configuration,
visibility: 'VISIBLE',
status: {
status: 'NEW'
}
})
loadingFinished()
// no need for an event source, the rule doesn't exist yet
} else {
this.$oh.api.get('/rest/rules/' + this.ruleId).then((data2) => {
this.$set(this, 'rule', data2)
@ -329,12 +315,12 @@ export default {
this.$f7.dialog.alert('Please give a name to the rule')
return Promise.reject()
}
const promise = (this.isNewRule)
const promise = (this.createMode)
? this.$oh.api.postPlain('/rest/rules', JSON.stringify(this.rule), 'text/plain', 'application/json')
: this.$oh.api.put('/rest/rules/' + this.rule.uid, this.rule)
return promise.then((data) => {
this.dirty = false
if (this.isNewRule) {
if (this.createMode) {
this.$f7.toast.create({
text: 'Rule created',
destroyOnClose: true,
@ -375,7 +361,7 @@ export default {
})
},
runNow () {
if (this.isNewRule) return
if (this.createMode) return
if (this.rule.status.status === 'RUNNING' || this.rule.status.status === 'UNINITIALIZED') {
return this.$f7.toast.create({
text: `Rule cannot be run ${(this.rule.status.status === 'RUNNING') ? 'while already running, please wait' : 'if it is uninitialized'}!`,
@ -562,7 +548,7 @@ export default {
this.currentModuleType = mod.type
this.scriptCode = mod.configuration.script
const updatePromise = (this.rule.editable || this.isNewRule) && this.dirty ? this.save() : Promise.resolve()
const updatePromise = (this.rule.editable || this.createMode) && this.dirty ? this.save() : Promise.resolve()
updatePromise.then(() => {
this.$f7router.navigate('/settings/rules/' + this.rule.uid + '/script/' + mod.id, { transition: this.$theme.aurora ? 'f7-cover-v' : '' })
})
@ -594,9 +580,6 @@ export default {
hasTemplate () {
return this.rule && this.currentTemplate !== null
},
isNewRule () {
return this.createMode || this.copyMode
},
templateTopicLink () {
if (!this.currentTemplate) return null
if (!this.currentTemplate.tags) return null

View File

@ -115,6 +115,9 @@
</f7-col>
<f7-col v-if="isEditable && !createMode">
<f7-list>
<f7-list-button color="blue" @click="copyRule">
Copy Scene
</f7-list-button>
<f7-list-button color="red" @click="deleteRule">
Remove Scene
</f7-list-button>
@ -206,7 +209,7 @@ export default {
ItemPicker,
'editor': () => import(/* webpackChunkName: "script-editor" */ '@/components/config/controls/script-editor.vue')
},
props: ['ruleId', 'createMode'],
props: ['ruleId', 'createMode', 'ruleCopy'],
data () {
return {
ready: false,
@ -262,7 +265,7 @@ export default {
Promise.all([loadModules1]).then((data) => {
this.moduleTypes.actions = data[0]
if (this.createMode) {
this.$set(this, 'rule', {
const newRule = this.ruleCopy || {
uid: this.$f7.utils.id(),
name: '',
triggers: [],
@ -275,7 +278,9 @@ export default {
status: {
status: 'NEW'
}
})
}
if (this.ruleCopy) newRule.uid = this.$f7.utils.id()
this.$set(this, 'rule', newRule)
loadingFinished()
} else {
this.$oh.api.get('/rest/rules/' + this.ruleId).then((data2) => {
@ -367,6 +372,16 @@ export default {
})
})
},
copyRule () {
let ruleClone = cloneDeep(this.rule)
this.$f7router.navigate({
url: '/settings/scenes/copy'
}, {
props: {
ruleCopy: ruleClone
}
})
},
deleteRule () {
this.$f7.dialog.confirm(
`Are you sure you want to delete ${this.rule.name}?`,

View File

@ -79,7 +79,7 @@
<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" />
<f7-block class="block-narrow" v-if="createMode">
<f7-block class="block-narrow" v-if="createMode && !ruleCopy">
<f7-col>
<f7-block-title medium class="margin-left margin-bottom">
Scripting Method
@ -131,6 +131,9 @@
<f7-block class="block-narrow" v-if="editable && isScriptRule">
<f7-col>
<f7-list>
<f7-list-button color="blue" @click="copyRule">
Copy Script
</f7-list-button>
<f7-list-button color="red" @click="deleteRule">
Remove Script
</f7-list-button>
@ -168,7 +171,7 @@ export default {
'editor': () => import(/* webpackChunkName: "script-editor" */ '@/components/config/controls/script-editor.vue'),
'blockly-editor': () => import(/* webpackChunkName: "blockly-editor" */ '@/components/config/controls/blockly-editor.vue')
},
props: ['ruleId', 'moduleId', 'createMode'],
props: ['ruleId', 'moduleId', 'createMode', 'ruleCopy'],
data () {
return {
ready: false,
@ -336,7 +339,7 @@ export default {
}
},
initializeNewScript () {
this.rule = {
this.rule = this.ruleCopy || {
uid: this.$f7.utils.id(),
name: '',
description: '',
@ -345,6 +348,7 @@ export default {
actions: [],
tags: ['Script']
}
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(() => {
@ -361,19 +365,21 @@ export default {
return
}
const actionModule = {
id: 'script',
type: 'script.ScriptAction',
configuration: {
type: this.mode,
script: ''
if (!this.ruleCopy) {
const actionModule = {
id: 'script',
type: 'script.ScriptAction',
configuration: {
type: this.mode,
script: ''
}
}
if (this.mode === 'application/javascript+blockly') {
actionModule.configuration.type = this.GRAALJS_MIME_TYPE
actionModule.configuration.blockSource = '<xml xmlns="https://developers.google.com/blockly/xml"></xml>'
}
this.rule.actions.push(actionModule)
}
if (this.mode === 'application/javascript+blockly') {
actionModule.configuration.type = this.GRAALJS_MIME_TYPE
actionModule.configuration.blockSource = '<xml xmlns="https://developers.google.com/blockly/xml"></xml>'
}
this.rule.actions.push(actionModule)
this.$oh.api.postPlain('/rest/rules', JSON.stringify(this.rule), 'text/plain', 'application/json').then(() => {
this.resetDirty()
@ -382,7 +388,7 @@ export default {
destroyOnClose: true,
closeTimeout: 2000
}).open()
this.$f7router.navigate(this.$f7route.url.replace('/add', '/' + this.rule.uid), { reloadCurrent: true })
this.$f7router.navigate(this.$f7route.url.replace(/(\/add)|(\/copy)/, '/' + this.rule.uid), { reloadCurrent: true })
})
},
isMimeTypeAvailable (mimeType) {
@ -573,6 +579,16 @@ export default {
run(false)
}
},
copyRule () {
let ruleClone = cloneDeep(this.rule)
this.$f7router.navigate({
url: '/settings/scripts/copy'
}, {
props: {
ruleCopy: ruleClone
}
})
},
deleteRule () {
this.$f7.dialog.confirm(
`Are you sure you want to delete ${this.rule.name}?`,

View File

@ -67,12 +67,17 @@ export default {
ConfigSheet,
ThingGeneralSettings
},
props: ['thingTypeId'],
props: ['thingTypeId', 'thingCopy'],
data () {
if (this.thingCopy) {
delete this.thingCopy.editable
delete this.thingCopy.properties
delete this.thingCopy.statusInfo
}
return {
ready: false,
currentTab: 'info',
thing: {
thing: this.thingCopy || {
UID: '',
label: '',
configuration: {},
@ -83,6 +88,12 @@ export default {
codePopupOpened: false
}
},
computed: {
isExtensible () {
if (!this.thingType || !this.thingType.extensibleChannelTypeIds) return false
return this.thingType.extensibleChannelTypeIds.length > 0
}
},
methods: {
onPageAfterIn () {
if (this.ready) return
@ -95,6 +106,18 @@ export default {
console.log('Cannot generate ID: ' + e)
}
this.thing.label = this.thingType.label
if (this.thingCopy) {
if (this.thing.bridgeUID) this.thing.UID = [this.thing.thingTypeUID, this.thing.bridgeUID.substring(this.thing.bridgeUID.lastIndexOf(':') + 1), this.thing.ID].join(':')
if (this.isExtensible) {
this.thing.channels.forEach((ch) => {
ch.uid = this.thing.UID + ':' + ch.id
})
} else {
this.thing.channels = []
}
}
this.ready = true
})
},

View File

@ -135,10 +135,11 @@
</f7-block>
</div>
<f7-block class="block-narrow" v-if="ready && editable">
<f7-block class="block-narrow" v-if="ready">
<f7-col>
<f7-list>
<f7-list-button color="red" title="Delete Thing" @click="deleteThing" />
<f7-list-button color="blue" title="Copy Thing" @click="copyThing" />
<f7-list-button v-if="editable" color="red" title="Delete Thing" @click="deleteThing" />
</f7-list>
</f7-col>
</f7-block>
@ -610,6 +611,17 @@ export default {
}
})
},
copyThing () {
let thingClone = cloneDeep(this.thing)
this.$f7router.navigate({
url: '/settings/things/copy'
}, {
props: {
thingTypeId: this.thing.thingTypeUID,
thingCopy: thingClone
}
})
},
deleteThing () {
let url, message
if (this.thing.statusInfo.status === 'REMOVING') {