diff --git a/bundles/org.openhab.ui/web/src/components/config/controls/item-picker.vue b/bundles/org.openhab.ui/web/src/components/config/controls/item-picker.vue
index 68533e7c9..5f7bdf99f 100644
--- a/bundles/org.openhab.ui/web/src/components/config/controls/item-picker.vue
+++ b/bundles/org.openhab.ui/web/src/components/config/controls/item-picker.vue
@@ -7,11 +7,13 @@
{{ item.label ? item.label + ' (' + item.name + ')' : item.name }}
-
+
+
-
+
+
@@ -30,12 +32,16 @@
import ModelPickerPopup from '@/components/model/model-picker-popup.vue'
export default {
- props: ['title', 'name', 'value', 'items', 'multiple', 'filterType', 'required', 'editableOnly', 'disabled', 'setValueText'],
+ props: ['title', 'name', 'value', 'items', 'multiple', 'filterType', 'required', 'editableOnly', 'disabled', 'setValueText', 'noModelPicker',
+ 'iconColor', 'auroraIcon', 'iosIcon', 'mdIcon'],
data () {
return {
ready: false,
preparedItems: [],
- icons: {},
+ aurora: this.auroraIcon || 'f7:list_bullet_indent',
+ ios: this.iosIcon || 'f7:list_bullet_indent',
+ md: this.mdIcon || 'f7:list_bullet_indent',
+ color: this.iconColor || undefined,
smartSelectParams: {
view: this.$f7.view.main,
openIn: 'popup',
diff --git a/bundles/org.openhab.ui/web/src/pages/settings/persistence/persistence-edit.vue b/bundles/org.openhab.ui/web/src/pages/settings/persistence/persistence-edit.vue
index 5f6f7b933..a47a339dd 100644
--- a/bundles/org.openhab.ui/web/src/pages/settings/persistence/persistence-edit.vue
+++ b/bundles/org.openhab.ui/web/src/pages/settings/persistence/persistence-edit.vue
@@ -67,6 +67,14 @@
+
+
+ Aliases
+
+
+
+
+
@@ -173,6 +181,45 @@
+
+
+
+ Aliases
+
+
+
+
+
+ {{ i }}
+
+
+
+
+
+ deleteAlias(ev, i)"
+ style="background-color: var(--f7-swipeout-delete-button-bg-color)">
+ Delete
+
+
+
+
+
+
+
+
@@ -215,6 +262,21 @@
.list
margin-top 0
+.list-alias-item .item-content .item-inner
+ display: flex
+ align-items: center
+.alias-label
+ min-width: 20%
+ margin-right: 5%
+ flex-shrink: 0
+ font-weight: var(--f7-list-media-item-title-font-weight, var(--f7-list-item-title-font-weight, inherit))
+.alias-input
+ flex-grow: 1
+ .input input
+ text-align: right
+.alias-item-picker .item-picker .item-content
+ padding-left: calc(var(--f7-list-item-padding-horizontal) + var(--f7-safe-area-left))
+
.persistence-code-editor.vue-codemirror
display block
top calc(var(--f7-navbar-height) + var(--f7-tabbar-height))
@@ -230,6 +292,7 @@ import fastDeepEqual from 'fast-deep-equal/es6'
import DirtyMixin from '../dirty-mixin'
import { FilterTypes, PredefinedStrategies } from '@/assets/definitions/persistence'
import CronStrategyPopup from '@/pages/settings/persistence/cron-strategy-popup.vue'
+import ItemPicker from '@/components/config/controls/item-picker.vue'
import StrategyPicker from '@/pages/settings/persistence/strategy-picker.vue'
import ConfigurationPopup from '@/pages/settings/persistence/configuration-popup.vue'
import FilterPopup from '@/pages/settings/persistence/filter-popup.vue'
@@ -237,6 +300,7 @@ import FilterPopup from '@/pages/settings/persistence/filter-popup.vue'
export default {
mixins: [DirtyMixin],
components: {
+ ItemPicker,
StrategyPicker,
'editor': () => import(/* webpackChunkName: "script-editor" */ '@/components/config/controls/script-editor.vue')
},
@@ -277,6 +341,9 @@ export default {
if (this.persistence[filterTypeName]) names = names.concat(this.persistence[filterTypeName].map((f) => f.name))
}
return names
+ },
+ currentItemsWithAlias () {
+ return Object.keys(this.persistence.aliases).sort()
}
},
watch: {
@@ -306,6 +373,7 @@ export default {
this.persistence = {
serviceId: this.serviceId,
configs: [],
+ aliases: [],
defaults: [
'everyChange'
],
@@ -352,13 +420,16 @@ export default {
}
})
},
- save (noToast) {
+ async save (noToast) {
if (!this.editable) return
if (this.currentTab === 'code') this.fromYaml()
// Update the code tab
if (this.persistenceYaml) this.toYaml()
+ const saveConfirmed = await this.validateAliases()
+ if (!saveConfirmed) return
+
return this.$oh.api.put('/rest/persistence/' + this.persistence.serviceId, this.persistence).then((data) => {
this.dirty = false
if (this.newPersistence) {
@@ -525,6 +596,70 @@ export default {
})
this.deleteModule(ev, module, index)
},
+ updateAliasItems (items) {
+ if (!this.editable) return
+ const aliases = this.persistence.aliases
+ Object.keys(aliases)
+ .filter((i) => !items.includes(i))
+ .forEach((i) => { delete aliases[i] })
+ items
+ .filter((i) => !Object.keys(aliases).includes(i))
+ .forEach((i) => { aliases[i] = '' })
+ const newAliases = Object.keys(aliases)
+ .reduce((obj, key) => {
+ obj[key] = aliases[key]
+ return obj
+ }, {})
+ this.$set(this.persistence, 'aliases', newAliases)
+ },
+ editAlias (ev, item, alias) {
+ if (!this.editable) return
+ // Warn when alias already exists
+ const duplicate = Object.entries(this.persistence.aliases).find(([i, a]) => (item !== i) && (alias === a))
+ if (duplicate) {
+ this.$f7.dialog.alert('Alias ' + alias + ' for item ' + item + ' already exists for item ' + duplicate[0])
+ this.$set(this.persistence.aliases, item, '')
+ return
+ }
+ this.$set(this.persistence.aliases, item, alias)
+ },
+ deleteAlias (ev, item) {
+ this.deleteModuleKey(ev, 'aliases', item)
+ },
+ async validateAliases () {
+ const entries = Object.entries(this.persistence.aliases)
+ // Check for invalid alias format
+ const invalidEntry = entries.find(([i, a]) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(a))
+ if (invalidEntry) {
+ const confirmed = await this.showConfirmDialog(
+ `Alias not valid for item ${invalidEntry[0]}!\nSave anyway?`,
+ 'Alias Validation Error'
+ )
+ if (!confirmed) return false
+ }
+ // Check for duplicate aliases
+ for (let idx = 1; idx < entries.length; idx++) {
+ const firstIdx = entries.slice(0, idx).findIndex(([i, a]) => a === entries[idx][1])
+ if (firstIdx >= 0) {
+ const confirmed = await this.showConfirmDialog(
+ `Alias "${entries[idx][1]}" for item "${entries[idx][0]}" already exists for item "${entries[firstIdx][0]}".\nSave anyway?`,
+ 'Alias Validation Error'
+ )
+ if (!confirmed) return false
+ }
+ }
+ return true
+ },
+ showConfirmDialog (message, title) {
+ return new Promise((resolve) => {
+ this.$f7.dialog.confirm(
+ message,
+ title,
+ () => resolve(true),
+ () => resolve(false)
+ )
+ })
+ },
saveModule (module, index, updatedModule) {
if (index === null) {
console.debug(`Adding ${module}:`)
@@ -539,8 +674,8 @@ export default {
this.checkDirty()
},
deleteModule (ev, module, index) {
- let swipeoutElement = ev.target
if (!this.editable) return
+ let swipeoutElement = ev.target
ev.cancelBubble = true
while (!swipeoutElement.classList.contains('swipeout')) {
swipeoutElement = swipeoutElement.parentElement
@@ -552,6 +687,20 @@ export default {
this.checkDirty()
})
},
+ deleteModuleKey (ev, module, key) {
+ if (!this.editable) return
+ let swipeoutElement = ev.target
+ ev.cancelBubble = true
+ while (!swipeoutElement.classList.contains('swipeout')) {
+ swipeoutElement = swipeoutElement.parentElement
+ }
+ this.$f7.swipeout.delete(swipeoutElement, () => {
+ console.debug(`Removing ${module}:`)
+ console.debug(key)
+ this.$delete(this.persistence[module], key)
+ this.checkDirty()
+ })
+ },
onEditorInput (value) {
this.persistenceYaml = value
this.dirty = true
@@ -559,6 +708,7 @@ export default {
toYaml () {
const toCode = {
configurations: this.persistence.configs,
+ aliases: this.persistence.aliases,
cronStrategies: this.persistence.cronStrategies,
defaultStrategies: this.persistence.defaults
}
@@ -572,6 +722,7 @@ export default {
try {
const updatedPersistence = YAML.parse(this.persistenceYaml)
this.$set(this.persistence, 'configs', updatedPersistence.configurations)
+ this.$set(this.persistence, 'aliases', updatedPersistence.aliases)
this.$set(this.persistence, 'cronStrategies', updatedPersistence.cronStrategies)
this.$set(this.persistence, 'defaults', updatedPersistence.defaultStrategies)
this.FilterTypes.forEach((ft) => {
@@ -583,8 +734,25 @@ export default {
return false
}
},
- keyDown (ev) {
- if ((ev.ctrlKey || ev.metaKey) && !(ev.altKey || ev.shiftKey)) {
+ keyDown (ev, index) {
+ if (ev.key === 'Tab') {
+ ev.stopPropagation()
+ ev.preventDefault()
+ const newIndex = index || 0
+ const total = this.currentItemsWithAlias.length
+ let targetIndex
+ if (ev.shiftKey) {
+ targetIndex = newIndex - 1 < 0 ? total - 1 : newIndex - 1
+ } else {
+ targetIndex = newIndex + 1 >= total ? 0 : newIndex + 1
+ }
+ const ref = this.$refs[`alias-input-${targetIndex}`]
+ const target = Array.isArray(ref) ? ref[0] : ref
+ if (target && target.$el) {
+ const inputEl = target.$el.querySelector('input')
+ if (inputEl) inputEl.focus()
+ }
+ } else if ((ev.ctrlKey || ev.metaKey) && !(ev.altKey || ev.shiftKey)) {
switch (ev.keyCode) {
case 83:
this.save()