Add Copy Thing YAML file definition & Refactor copy file definition code (#3130)

To support https://github.com/openhab/openhab-core/pull/4691

- Refactor file definition code into a mixin to create a consistent UI
in inbox, things-list, items-list, item-details, thing, details, single,
and multi-selection.
- Add `Copy` button in Things List which shows up when multi-selection
is active. This makes it in line with Items list and Inbox
- Standardize the list selection UI for inbox, things, items list.
Action buttons/links are closer together in the center now.
- Remove the selection counter off the buttons to make room. The
selection counter is shown in the list title.
- Use one "Copy" button which opens a dialog to select the file
definition format to export/copy to clipboard.

---------

Signed-off-by: Jimmy Tanagra <jcode@tanagra.id.au>
pull/2967/head
jimtng 2025-04-20 22:42:22 +10:00 committed by GitHub
parent 0b6159401a
commit e3812d5105
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 166 additions and 157 deletions

View File

@ -27,10 +27,10 @@
<f7-list-item media-item title="Block Libraries" footer="Develop custom extensions for Blockly scripts" link="blocks/">
<f7-icon slot="media" f7="ticket" color="gray" />
</f7-list-item>
<f7-list-item media-item title="Copy DSL Definitions for All Things" footer="Copy all Things' DSL definitions to clipboard" link="#" @click="copyThingsDsl">
<f7-list-item media-item title="Things File Definitions" footer="Copy all Things' file definitions to clipboard" link="#" @click="copyFileDefinitionToClipboard(ObjectType.THING)">
<f7-icon slot="media" f7="lightbulb" color="gray" />
</f7-list-item>
<f7-list-item media-item title="Copy DSL Definitions for All Items" footer="Copy all Items' DSL definitions to clipboard" link="#" @click="copyItemsDsl">
<f7-list-item media-item title="Items File Definitions" footer="Copy all Items' file definitions to clipboard" link="#" @click="copyFileDefinitionToClipboard(ObjectType.ITEM)">
<f7-icon slot="media" f7="square_on_circle" color="gray" />
</f7-list-item>
<f7-list-item media-item title="Add Items from DSL Definition" footer="Create or update items &amp; links in bulk" link="add-items-dsl">
@ -131,12 +131,10 @@
</style>
<script>
import Vue from 'vue'
import Clipboard from 'v-clipboard'
Vue.use(Clipboard)
import FileDefinition from '@/pages/settings/file-definition-mixin'
export default {
mixins: [FileDefinition],
components: {
},
data () {
@ -177,34 +175,6 @@ export default {
this.$oh.ws.close(this.wsClient)
this.wsClient = null
this.wsEvents = []
},
copyThingsDsl () {
this.$oh.api.getPlain({
url: '/rest/file-format/things',
headers: { accept: 'text/vnd.openhab.dsl.thing' }
}).then((definition) => {
if (this.$clipboard(definition)) {
this.$f7.toast.show({
text: 'Things DSL definitions copied to clipboard',
destroyOnClose: true,
closeTimeout: 2000
}).open()
}
})
},
copyItemsDsl () {
this.$oh.api.getPlain({
url: '/rest/file-format/items',
headers: { accept: 'text/vnd.openhab.dsl.item' }
}).then((definition) => {
if (this.$clipboard(definition)) {
this.$f7.toast.show({
text: 'Items DSL definitions copied to clipboard',
destroyOnClose: true,
closeTimeout: 2000
}).open()
}
})
}
},
asyncComputed: {

View File

@ -0,0 +1,96 @@
import Vue from 'vue'
import Clipboard from 'v-clipboard'
Vue.use(Clipboard)
function executeFileDefinitionCopy (vueInstance, objectType, objectTypeLabel, objectIds, copiedObjectsLabel, fileFormatLabel, mediaType) {
const progressDialog = vueInstance.$f7.dialog.progress(`Loading ${objectTypeLabel} ${fileFormatLabel} definition...`)
const path = `/rest/file-format/${objectType}s`
let apiCalls = []
if (objectIds !== null) {
apiCalls = objectIds.map((id) => vueInstance.$oh.api.getPlain({
url: path + '/' + id,
headers: { accept: mediaType }
}))
} else {
apiCalls = [vueInstance.$oh.api.getPlain({
url: path,
headers: { accept: mediaType }
})]
}
Promise.all(apiCalls)
.then(definitions => {
const definition = definitions.join('\n')
progressDialog.close()
if (vueInstance.$clipboard(definition)) {
vueInstance.$f7.toast.create({
text: `${objectTypeLabel} ${fileFormatLabel} definition copied to clipboard:\n${copiedObjectsLabel}`,
destroyOnClose: true,
closeTimeout: 2000
}).open()
} else {
vueInstance.$f7.dialog.alert(`Error copying ${objectTypeLabel} ${fileFormatLabel} definition to the clipboard`, 'Error')
}
})
.catch(error => {
progressDialog.close()
vueInstance.$f7.dialog.alert(`Error copying ${objectTypeLabel} ${fileFormatLabel} definition: ${error}`, 'Error')
})
}
export default {
created () {
// Define the ObjectType enum to be used when calling the copyFileDefinitionToClipboard method
this.ObjectType = Object.freeze({
THING: 'thing',
ITEM: 'item'
})
},
methods: {
/**
* Copies the file definitions of the given list of thingUIDs or item names to the clipboard.
*
* @param {string} objectType - The type of the objects (`thing` or `item`). Use {ObjectType} enum for clarity.
* @param {Array} objectIds - The list of object ids to copy. For Things, this should be an array of Thing UIDs.
* For Items, this should be an array of Item names.
* When `null`, all objects of the given type will be copied.
*/
copyFileDefinitionToClipboard (objectType, objectIds = null) {
const objectTypeLabel = objectType.charAt(0).toUpperCase() + objectType.slice(1) + 's'
let copiedObjectsLabel = null
if (objectIds === null) {
copiedObjectsLabel = `All ${objectTypeLabel}`
} else if (objectIds.length === 1) {
copiedObjectsLabel = '<b>' + objectIds[0] + '</b>'
} else {
copiedObjectsLabel = `${objectIds.length} ${objectTypeLabel}`
}
this.$f7.dialog
.create({
title: `Copy ${objectTypeLabel} File Definition`,
text: `Select the file format to copy ${copiedObjectsLabel} to clipboard`,
buttons: [
{
text: 'Cancel',
color: 'gray'
},
{
text: 'DSL',
color: 'teal',
onClick: () => executeFileDefinitionCopy(this, objectType, objectTypeLabel, objectIds, copiedObjectsLabel, 'DSL', `text/vnd.openhab.dsl.${objectType}`)
},
{
text: 'YAML',
color: 'blue',
onClick: () => executeFileDefinitionCopy(this, objectType, objectTypeLabel, objectIds, copiedObjectsLabel, 'YAML', 'application/yaml')
}
]
})
.open()
}
}
}

View File

@ -90,8 +90,8 @@
<f7-list-button color="blue" @click="duplicateItem">
Duplicate Item
</f7-list-button>
<f7-list-button color="blue" @click="copyItemDslDefinition">
Copy DSL Definition
<f7-list-button color="blue" @click="copyFileDefinitionToClipboard(ObjectType.ITEM, [item.name])">
Copy File Definition
</f7-list-button>
<f7-list-button v-if="item.editable" color="red" @click="deleteItem">
Remove Item
@ -166,9 +166,10 @@ import LinkDetails from '@/components/model/link-details.vue'
import GroupMembers from '@/components/item/group-members.vue'
import MetadataMenu from '@/components/item/metadata/item-metadata-menu.vue'
import ItemMixin from '@/components/item/item-mixin'
import FileDefinition from '@/pages/settings/file-definition-mixin'
export default {
mixins: [ItemMixin],
mixins: [ItemMixin, FileDefinition],
props: ['itemName'],
components: {
LinkDetails,
@ -220,20 +221,6 @@ export default {
}
})
},
copyItemDslDefinition () {
this.$oh.api.getPlain({
url: '/rest/file-format/items/' + this.item.name,
headers: { accept: 'text/vnd.openhab.dsl.item' }
}).then(definition => {
if (this.$clipboard(definition)) {
this.$f7.toast.create({
text: `DSL Item definition for '${this.item.name}' copied to clipboard`,
destroyOnClose: true,
closeTimeout: 2000
}).open()
}
})
},
deleteItem () {
this.$f7.dialog.confirm(
`Are you sure you want to delete ${this.item.label || this.item.name}?`,

View File

@ -19,12 +19,14 @@
</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 right-margin" icon-ios="f7:trash" icon-aurora="f7:trash" @click="removeSelected">
Remove {{ selectedItems.length }}
</f7-link>
<f7-link color="blue" v-show="selectedItems.length" v-if="!$theme.md" class="copy" icon-ios="f7:square_on_square" icon-aurora="f7:square_on_square" @click="copySelected">
&nbsp;Copy DSL Definitions
</f7-link>
<div class="display-flex justify-content-center" v-if="!$theme.md && selectedItems.length > 0" style="width: 100%">
<f7-link color="red" v-show="selectedItems.length" class="delete display-flex flex-direction-row margin-right" icon-ios="f7:trash" icon-aurora="f7:trash" @click="removeSelected">
Remove
</f7-link>
<f7-link color="blue" v-show="selectedItems.length" class="copy display-flex flex-direction-row" icon-ios="f7:square_on_square" icon-aurora="f7:square_on_square" @click="copySelected">
&nbsp;Copy
</f7-link>
</div>
<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
@ -143,15 +145,11 @@
</style>
<script>
import Vue from 'vue'
import Clipboard from 'v-clipboard'
Vue.use(Clipboard)
import ItemMixin from '@/components/item/item-mixin'
import FileDefinition from '@/pages/settings/file-definition-mixin'
export default {
mixins: [ItemMixin],
mixins: [ItemMixin, FileDefinition],
components: {
'empty-state-placeholder': () => import('@/components/empty-state-placeholder.vue')
},
@ -304,19 +302,10 @@ export default {
}
},
copySelected () {
const promises = this.selectedItems.map((itemName) => this.$oh.api.getPlain({
url: '/rest/file-format/items/' + itemName,
headers: { accept: 'text/vnd.openhab.dsl.item' }
}))
Promise.all(promises).then((data) => {
if (this.$clipboard(data.join('\n'))) {
this.$f7.toast.create({
text: 'DSL definitions copied to clipboard',
destroyOnClose: true,
closeTimeout: 2000
}).open()
}
})
// When _all_ (not just filtered) items are selected, pass null to copyFileDefinitionToClipboard
// so that it only makes one call to the backend
const selectedItems = this.selectedItems.length === this.items.length ? null : this.selectedItems
this.copyFileDefinitionToClipboard(this.ObjectType.ITEM, selectedItems)
},
removeSelected () {
const vm = this

View File

@ -37,20 +37,20 @@
</f7-button>
<!-- buttons for wider screen -->
<template v-if="$f7.width >= 500">
<f7-button @click="performActionOnSelection('copy')" color="blue" class="delete wider-screen display-flex flex-direction-row"
<f7-button @click="copyFileDefinitionToClipboard(ObjectType.THING, selectedItems)" color="blue" class="copy wider-screen display-flex flex-direction-row"
icon-ios="f7:square_on_square" icon-aurora="f7:square_on_square">
&nbsp;Copy
</f7-button>
</template>
<!-- buttons for narrower screen -->
<template v-else>
<f7-button color="blue" class="delete narrower-screen" popover-open=".item-popover">
<f7-button color="blue" class="popover-button narrower-screen" popover-open=".item-popover">
...
</f7-button>
<f7-popover class="item-popover" ref="popover" :backdrop="false" :close-by-backdrop-click="true"
:style="{ width: '96px' }" :animate="false">
<div class="margin-vertical display-flex justify-content-center" style="width: 100%">
<f7-link @click="performActionOnSelection('copy')" color="blue" class="delete display-flex flex-direction-column margin-right"
<f7-link @click="performActionOnSelection('copy')" color="blue" class="copy display-flex flex-direction-column margin-right"
icon-ios="f7:square_on_square" icon-aurora="f7:square_on_square" popover-close=".item-popover">
Copy
</f7-link>
@ -66,7 +66,7 @@
<f7-link v-show="selectedItems.length" icon-md="material:delete" icon-color="white" @click="confirmActionOnSelection('delete')" />
<f7-link v-show="selectedItems.length" icon-md="material:visibility_off" icon-color="white" @click="confirmActionOnSelection('ignore')" />
<f7-link v-show="selectedItems.length" icon-md="material:thumb_up" icon-color="white" @click="confirmActionOnSelection('approve')" />
<f7-link v-show="selectedItems.length" icon-md="material:content_copy" icon-color="white" @click="performActionOnSelection('copy')" />
<f7-link v-show="selectedItems.length" icon-md="material:content_copy" icon-color="white" @click="copyFileDefinitionToClipboard(ObjectType.THING, selectedItems)" />
</div>
</f7-toolbar>
@ -445,7 +445,6 @@ export default {
performActionOnSelection (action) {
let progressMessage, successMessage, promises
let navigateToThingsPage = false
let clearSelectionAndReload = true
switch (action) {
case 'delete':
progressMessage = 'Removing Inbox Entries...'
@ -468,23 +467,6 @@ export default {
successMessage = `${this.selectedItems.length} entries unignored`
promises = this.filterSelectedItems().map((e) => this.$oh.api.postPlain('/rest/inbox/' + e.thingUID + '/unignore'))
break
case 'copy':
progressMessage = 'Copying Inbox Entries...'
successMessage = `${this.selectedItems.length} entries copied to clipboard`
promises = this.filterSelectedItems().map((e) => this.$oh.api.getPlain({
url: '/rest/file-format/things/' + e.thingUID,
headers: { accept: 'text/vnd.openhab.dsl.thing' }
}))
promises = [Promise.all(promises).then((data) => {
if (this.$clipboard(data.join('\n'))) {
Promise.resolve()
} else {
Promise.reject('Failed to copy to clipboard')
}
})]
clearSelectionAndReload = false
break
}
let dialog = this.$f7.dialog.progress(progressMessage)
@ -496,15 +478,16 @@ export default {
closeTimeout: 2000
}).open()
const searchFor = this.selectedItems.join(',')
if (clearSelectionAndReload) this.selectedItems = []
this.selectedItems = []
dialog.close()
if (clearSelectionAndReload) this.load()
if (navigateToThingsPage) {
this.$f7router.navigate('/settings/things/', {
props: {
searchFor
}
})
} else {
this.load()
}
}).catch((err) => {
dialog.close()

View File

@ -156,7 +156,7 @@
<f7-list-button v-if="thing.statusInfo.statusDetail === 'HANDLER_MISSING_ERROR'" color="blue"
title="Install Binding" @click="installBinding" />
<f7-list-button v-if="!error" color="blue" title="Duplicate Thing" @click="duplicateThing" />
<f7-list-button v-if="!error" color="blue" title="Copy DSL Definition" @click="copyThingDsl" />
<f7-list-button v-if="!error" color="blue" title="Copy File Definition" @click="copyFileDefinitionToClipboard(ObjectType.THING, [thingId])" />
<f7-list-button v-if="editable" color="red" title="Remove Thing" @click="deleteThing" />
</f7-list>
</f7-col>
@ -249,11 +249,6 @@ p.action-description
</style>
<script>
import Vue from 'vue'
import Clipboard from 'v-clipboard'
Vue.use(Clipboard)
import YAML from 'yaml'
import cloneDeep from 'lodash/cloneDeep'
import fastDeepEqual from 'fast-deep-equal/es6'
@ -274,9 +269,10 @@ import ThingStatus from '@/components/thing/thing-status-mixin'
import DirtyMixin from '../dirty-mixin'
import ThingActionPopup from '@/pages/settings/things/thing-action-popup.vue'
import FileDefinition from '@/pages/settings/file-definition-mixin'
export default {
mixins: [ThingStatus, DirtyMixin],
mixins: [ThingStatus, DirtyMixin, FileDefinition],
components: {
ConfigSheet,
ChannelList,
@ -617,20 +613,6 @@ export default {
}
})
},
copyThingDsl () {
this.$oh.api.getPlain({
url: '/rest/file-format/things/' + this.thingId,
headers: { accept: 'text/vnd.openhab.dsl.thing' }
}).then((definition) => {
if (this.$clipboard(definition)) {
this.$f7.toast.create({
text: 'Thing DSL definition copied to clipboard',
destroyOnClose: true,
closeTimeout: 2000
}).open()
}
})
},
deleteThing () {
let url, message
if (this.thing.statusInfo.status === 'REMOVING') {

View File

@ -1,11 +1,8 @@
import Vue from 'vue'
import Clipboard from 'v-clipboard'
Vue.use(Clipboard)
import ThingMixin from '@/components/thing/thing-mixin'
import FileDefinition from '@/pages/settings/file-definition-mixin'
export default {
mixins: [ThingMixin],
mixins: [ThingMixin, FileDefinition],
methods: {
/**
* Approve the given entry from the inbox.
@ -122,24 +119,10 @@ export default {
},
entryActionsCopyThingDefinitionButton (entry) {
return {
text: 'Copy DSL Definition',
text: 'Copy Thing File Definition',
color: 'blue',
bold: true,
onClick: () => {
const headers = { accept: 'text/vnd.openhab.dsl.thing' }
this.$oh.api.getPlain({
url: '/rest/file-format/things/' + entry.thingUID,
headers: { accept: 'text/vnd.openhab.dsl.thing' }
}).then(definition => {
if (this.$clipboard(definition)) {
this.$f7.toast.create({
text: `DSL Thing definition for '${entry.thingUID}' copied to clipboard`,
destroyOnClose: true,
closeTimeout: 2000
}).open()
}
})
}
onClick: () => this.copyFileDefinitionToClipboard(this.ObjectType.THING, [entry.thingUID])
}
}
}

View File

@ -20,15 +20,20 @@
</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>
<f7-link color="orange" v-show="selectedItems.length" v-if="!$theme.md" class="disable" @click="doDisableEnableSelected(false)" icon-ios="f7:pause_circle" icon-aurora="f7:pause_circle">
&nbsp;Disable {{ selectedItems.length }}
</f7-link>
<f7-link color="green" v-show="selectedItems.length" v-if="!$theme.md" class="enable" @click="doDisableEnableSelected(true)" icon-ios="f7:play_circle" icon-aurora="f7:play_circle">
&nbsp;Enable {{ selectedItems.length }}
</f7-link>
<div class="display-flex justify-content-center" v-if="!$theme.md && selectedItems.length > 0" style="width: 100%">
<f7-link color="red" v-show="selectedItems.length" class="delete display-flex flex-direction-row margin-right" icon-ios="f7:trash" icon-aurora="f7:trash" @click="removeSelected">
Remove
</f7-link>
<f7-link color="orange" v-show="selectedItems.length" class="disable display-flex flex-direction-row margin-right" @click="doDisableEnableSelected(false)" icon-ios="f7:pause_circle" icon-aurora="f7:pause_circle">
&nbsp;Disable
</f7-link>
<f7-link color="green" v-show="selectedItems.length" class="enable display-flex flex-direction-row margin-right" @click="doDisableEnableSelected(true)" icon-ios="f7:play_circle" icon-aurora="f7:play_circle">
&nbsp;Enable
</f7-link>
<f7-link color="blue" v-show="selectedItems.length" class="copy display-flex flex-direction-row" @click="copyFileDefinitionToClipboard(ObjectType.THING, selectedItems)" icon-ios="f7:square_on_square" icon-aurora="f7:square_on_square">
&nbsp;Copy
</f7-link>
</div>
<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
@ -37,6 +42,7 @@
<f7-link v-show="selectedItems.length" tooltip="Disable selected" icon-md="material:pause_circle_outline" icon-color="white" @click="doDisableEnableSelected(false)" />
<f7-link 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" tooltip="Remove selected" icon-md="material:delete" icon-color="white" @click="removeSelected" />
<f7-link v-show="selectedItems.length" tooltip="Copy selected" icon-md="material:content_copy" icon-color="white" @click="copyFileDefinitionToClipboard(ObjectType.THING, selectedItems)" />
</div>
</f7-toolbar>
@ -51,11 +57,11 @@
<f7-block class="block-narrow">
<f7-col v-show="ready">
<f7-block-title>
<span>{{ thingsCount }} <template v-if="searchQuery">of {{ things.length }} </template>Things<template v-if="searchQuery"> found</template></span>
<template v-if="showCheckboxes && filteredThings.length">
<span>{{ listTitle }}</span>
<span v-if="showCheckboxes && filteredThings.length">
-
<f7-link @click="selectDeselectAll()" :text="allSelected ? 'Deselect all' : 'Select all'" />
</template>
<f7-link @click="selectDeselectAll" :text="allSelected ? 'Deselect all' : 'Select all'" />
</span>
<template v-if="groupBy === 'location'">
<div v-if="!$device.desktop && $f7.width < 1024" style="text-align:right; color:var(--f7-block-text-color); font-weight: normal" class="float-right">
<f7-checkbox :checked="showNoLocation" @change="toggleShowNoLocation" /> <label @click="toggleShowNoLocation" style="cursor:pointer">Show no location</label>
@ -161,11 +167,12 @@
</style>
<script>
import thingStatus from '@/components/thing/thing-status-mixin'
import ThingStatus from '@/components/thing/thing-status-mixin'
import ClipboardIcon from '@/components/util/clipboard-icon.vue'
import FileDefinition from '@/pages/settings/file-definition-mixin'
export default {
mixins: [thingStatus],
mixins: [ThingStatus, FileDefinition],
props: ['searchFor'],
components: {
'empty-state-placeholder': () => import('@/components/empty-state-placeholder.vue'),
@ -246,6 +253,18 @@ export default {
},
searchPlaceholder () {
return window.innerWidth >= 1280 ? 'Search (for advanced search, use the developer sidebar (Shift+Alt+D))' : 'Search'
},
listTitle () {
let title = this.filteredThings.length
if (this.searchQuery) {
title += ` of ${this.things.length} Things found`
} else {
title += ' Things'
}
if (this.selectedItems.length > 0) {
title += `, ${this.selectedItems.length} selected`
}
return title
}
},
methods: {