Persistence edit: Support configuring aliases (#3070)
Configuration in the UI for persistence alias changes introduced in https://github.com/openhab/openhab-core/pull/4363. Signed-off-by: Mark Herwege <mark.herwege@telenet.be>pull/3142/head
parent
d8a31769d2
commit
0864b52e09
|
@ -7,11 +7,13 @@
|
||||||
{{ item.label ? item.label + ' (' + item.name + ')' : item.name }}
|
{{ item.label ? item.label + ' (' + item.name + ')' : item.name }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<f7-button slot="media" icon-f7="list_bullet_indent" @click.native="pickFromModel" />
|
<f7-button v-if="!noModelPicker" slot="media" :icon-color="color" :icon-aurora="aurora" :icon-ios="ios" :icon-md="md" @click.native="pickFromModel" />
|
||||||
|
<f7-icon v-else slot="media" :color="color" :aurora="aurora" :ios="ios" :md="md" />
|
||||||
</f7-list-item>
|
</f7-list-item>
|
||||||
<!-- for placeholder purposes before items are loaded -->
|
<!-- for placeholder purposes before items are loaded -->
|
||||||
<f7-list-item link v-show="!ready" :title="title" disabled no-chevron>
|
<f7-list-item link v-show="!ready" :title="title" disabled no-chevron>
|
||||||
<f7-button slot="media" icon-f7="list_bullet_indent" @click.native="pickFromModel" />
|
<f7-button v-if="!noModelPicker" slot="media" :icon-color="color" :icon-aurora="aurora" :icon-ios="ios" :icon-md="md" @click.native="pickFromModel" />
|
||||||
|
<f7-icon v-else slot="media" :color="color" :aurora="aurora" :ios="ios" :md="md" />
|
||||||
</f7-list-item>
|
</f7-list-item>
|
||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
|
@ -30,12 +32,16 @@
|
||||||
import ModelPickerPopup from '@/components/model/model-picker-popup.vue'
|
import ModelPickerPopup from '@/components/model/model-picker-popup.vue'
|
||||||
|
|
||||||
export default {
|
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 () {
|
data () {
|
||||||
return {
|
return {
|
||||||
ready: false,
|
ready: false,
|
||||||
preparedItems: [],
|
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: {
|
smartSelectParams: {
|
||||||
view: this.$f7.view.main,
|
view: this.$f7.view.main,
|
||||||
openIn: 'popup',
|
openIn: 'popup',
|
||||||
|
|
|
@ -67,6 +67,14 @@
|
||||||
</f7-list>
|
</f7-list>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<f7-block-title medium style="margin-bottom: var(--f7-list-margin-vertical)">
|
||||||
|
Aliases
|
||||||
|
</f7-block-title>
|
||||||
|
<f7-list class="skeleton-text skeleton-effect-blink">
|
||||||
|
<f7-list-item />
|
||||||
|
</f7-list>
|
||||||
|
</div>
|
||||||
</f7-col>
|
</f7-col>
|
||||||
</f7-block>
|
</f7-block>
|
||||||
|
|
||||||
|
@ -173,6 +181,45 @@
|
||||||
</f7-list>
|
</f7-list>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Aliases -->
|
||||||
|
<div>
|
||||||
|
<f7-block-title medium style="margin-bottom: var(--f7-list-margin-vertical)">
|
||||||
|
Aliases
|
||||||
|
</f7-block-title>
|
||||||
|
<f7-list :media-list="editable" swipeout no-swipeout-opened>
|
||||||
|
<f7-list-item v-for="(i, index) in currentItemsWithAlias" class="swipeout list-alias-item" :key="i">
|
||||||
|
<f7-link slot="media" icon-color="red" icon-aurora="f7:minus_circle_filled"
|
||||||
|
icon-ios="f7:minus_circle_filled" icon-md="material:remove_circle_outline"
|
||||||
|
@click="showSwipeout" />
|
||||||
|
<div class="alias-label">
|
||||||
|
{{ i }}
|
||||||
|
</div>
|
||||||
|
<div class="alias-input">
|
||||||
|
<f7-input type="text"
|
||||||
|
:ref="'alias-input-' + index"
|
||||||
|
placeholder="alias"
|
||||||
|
validate pattern="[A-Za-z_][A-Za-z0-9_]*"
|
||||||
|
error-message="Required. Must not start with a number. A-Z,a-z,0-9,_ only"
|
||||||
|
:value="persistence.aliases[i]"
|
||||||
|
@input="editAlias($event, i, $event.target.value)"
|
||||||
|
@keydown.native="keyDown($event, index)" />
|
||||||
|
</div>
|
||||||
|
<f7-swipeout-actions right v-if="editable">
|
||||||
|
<f7-swipeout-button @click="(ev) => deleteAlias(ev, i)"
|
||||||
|
style="background-color: var(--f7-swipeout-delete-button-bg-color)">
|
||||||
|
Delete
|
||||||
|
</f7-swipeout-button>
|
||||||
|
</f7-swipeout-actions>
|
||||||
|
</f7-list-item>
|
||||||
|
</f7-list>
|
||||||
|
<f7-list v-if="editable">
|
||||||
|
<item-picker class="alias-item-picker" title="Add alias" name="items"
|
||||||
|
multiple="true" noModelPicker="true" :setValueText="false"
|
||||||
|
iconColor="green" auroraIcon="f7:plus_circle_fill" iosIcon="f7:plus_circle_fill" mdIcon="material:control_point"
|
||||||
|
:value="currentItemsWithAlias"
|
||||||
|
@input="updateAliasItems($event)" />
|
||||||
|
</f7-list>
|
||||||
|
</div>
|
||||||
</f7-col>
|
</f7-col>
|
||||||
<f7-col v-if="editable && !newPersistence">
|
<f7-col v-if="editable && !newPersistence">
|
||||||
<f7-list>
|
<f7-list>
|
||||||
|
@ -215,6 +262,21 @@
|
||||||
.list
|
.list
|
||||||
margin-top 0
|
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
|
.persistence-code-editor.vue-codemirror
|
||||||
display block
|
display block
|
||||||
top calc(var(--f7-navbar-height) + var(--f7-tabbar-height))
|
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 DirtyMixin from '../dirty-mixin'
|
||||||
import { FilterTypes, PredefinedStrategies } from '@/assets/definitions/persistence'
|
import { FilterTypes, PredefinedStrategies } from '@/assets/definitions/persistence'
|
||||||
import CronStrategyPopup from '@/pages/settings/persistence/cron-strategy-popup.vue'
|
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 StrategyPicker from '@/pages/settings/persistence/strategy-picker.vue'
|
||||||
import ConfigurationPopup from '@/pages/settings/persistence/configuration-popup.vue'
|
import ConfigurationPopup from '@/pages/settings/persistence/configuration-popup.vue'
|
||||||
import FilterPopup from '@/pages/settings/persistence/filter-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 {
|
export default {
|
||||||
mixins: [DirtyMixin],
|
mixins: [DirtyMixin],
|
||||||
components: {
|
components: {
|
||||||
|
ItemPicker,
|
||||||
StrategyPicker,
|
StrategyPicker,
|
||||||
'editor': () => import(/* webpackChunkName: "script-editor" */ '@/components/config/controls/script-editor.vue')
|
'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))
|
if (this.persistence[filterTypeName]) names = names.concat(this.persistence[filterTypeName].map((f) => f.name))
|
||||||
}
|
}
|
||||||
return names
|
return names
|
||||||
|
},
|
||||||
|
currentItemsWithAlias () {
|
||||||
|
return Object.keys(this.persistence.aliases).sort()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -306,6 +373,7 @@ export default {
|
||||||
this.persistence = {
|
this.persistence = {
|
||||||
serviceId: this.serviceId,
|
serviceId: this.serviceId,
|
||||||
configs: [],
|
configs: [],
|
||||||
|
aliases: [],
|
||||||
defaults: [
|
defaults: [
|
||||||
'everyChange'
|
'everyChange'
|
||||||
],
|
],
|
||||||
|
@ -352,13 +420,16 @@ export default {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
save (noToast) {
|
async save (noToast) {
|
||||||
if (!this.editable) return
|
if (!this.editable) return
|
||||||
if (this.currentTab === 'code') this.fromYaml()
|
if (this.currentTab === 'code') this.fromYaml()
|
||||||
|
|
||||||
// Update the code tab
|
// Update the code tab
|
||||||
if (this.persistenceYaml) this.toYaml()
|
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) => {
|
return this.$oh.api.put('/rest/persistence/' + this.persistence.serviceId, this.persistence).then((data) => {
|
||||||
this.dirty = false
|
this.dirty = false
|
||||||
if (this.newPersistence) {
|
if (this.newPersistence) {
|
||||||
|
@ -525,6 +596,70 @@ export default {
|
||||||
})
|
})
|
||||||
this.deleteModule(ev, module, index)
|
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) {
|
saveModule (module, index, updatedModule) {
|
||||||
if (index === null) {
|
if (index === null) {
|
||||||
console.debug(`Adding ${module}:`)
|
console.debug(`Adding ${module}:`)
|
||||||
|
@ -539,8 +674,8 @@ export default {
|
||||||
this.checkDirty()
|
this.checkDirty()
|
||||||
},
|
},
|
||||||
deleteModule (ev, module, index) {
|
deleteModule (ev, module, index) {
|
||||||
let swipeoutElement = ev.target
|
|
||||||
if (!this.editable) return
|
if (!this.editable) return
|
||||||
|
let swipeoutElement = ev.target
|
||||||
ev.cancelBubble = true
|
ev.cancelBubble = true
|
||||||
while (!swipeoutElement.classList.contains('swipeout')) {
|
while (!swipeoutElement.classList.contains('swipeout')) {
|
||||||
swipeoutElement = swipeoutElement.parentElement
|
swipeoutElement = swipeoutElement.parentElement
|
||||||
|
@ -552,6 +687,20 @@ export default {
|
||||||
this.checkDirty()
|
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) {
|
onEditorInput (value) {
|
||||||
this.persistenceYaml = value
|
this.persistenceYaml = value
|
||||||
this.dirty = true
|
this.dirty = true
|
||||||
|
@ -559,6 +708,7 @@ export default {
|
||||||
toYaml () {
|
toYaml () {
|
||||||
const toCode = {
|
const toCode = {
|
||||||
configurations: this.persistence.configs,
|
configurations: this.persistence.configs,
|
||||||
|
aliases: this.persistence.aliases,
|
||||||
cronStrategies: this.persistence.cronStrategies,
|
cronStrategies: this.persistence.cronStrategies,
|
||||||
defaultStrategies: this.persistence.defaults
|
defaultStrategies: this.persistence.defaults
|
||||||
}
|
}
|
||||||
|
@ -572,6 +722,7 @@ export default {
|
||||||
try {
|
try {
|
||||||
const updatedPersistence = YAML.parse(this.persistenceYaml)
|
const updatedPersistence = YAML.parse(this.persistenceYaml)
|
||||||
this.$set(this.persistence, 'configs', updatedPersistence.configurations)
|
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, 'cronStrategies', updatedPersistence.cronStrategies)
|
||||||
this.$set(this.persistence, 'defaults', updatedPersistence.defaultStrategies)
|
this.$set(this.persistence, 'defaults', updatedPersistence.defaultStrategies)
|
||||||
this.FilterTypes.forEach((ft) => {
|
this.FilterTypes.forEach((ft) => {
|
||||||
|
@ -583,8 +734,25 @@ export default {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
keyDown (ev) {
|
keyDown (ev, index) {
|
||||||
if ((ev.ctrlKey || ev.metaKey) && !(ev.altKey || ev.shiftKey)) {
|
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) {
|
switch (ev.keyCode) {
|
||||||
case 83:
|
case 83:
|
||||||
this.save()
|
this.save()
|
||||||
|
|
Loading…
Reference in New Issue