Skip to content

Commit

Permalink
Widget config/code editing: Add live update, dirty checking, make pop…
Browse files Browse the repository at this point in the history
…ups movable (#3039)

- Make Widget Config and Widget Code Editor popups draggable by the
navbar, so they can be moved out of the way to view the widget being
edited
- Preview changes immediately on the widget
- The changes can be reverted if desired by clicking the "Reset" link in
the navbar.
- The changes are reverted if the popup is closed without saving.
- Implement dirty checking confirmation

---------

Signed-off-by: Jimmy Tanagra <[email protected]>
  • Loading branch information
jimtng authored Mar 10, 2025
1 parent 394c704 commit ed9aa31
Show file tree
Hide file tree
Showing 6 changed files with 287 additions and 34 deletions.
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
<template>
<f7-popup ref="widgetCode" class="widgetcode-popup" @popup:open="widgetCodeOpened" @popup:closed="widgetCodeClosed">
<f7-popup ref="widgetCode" class="widgetcode-popup" :close-by-backdrop-click="false" @popup:open="widgetCodeOpened" @popup:closed="widgetCodeClosed">
<f7-page v-if="component && code">
<f7-navbar>
<f7-navbar ref="navbar">
<f7-nav-left>
<f7-link icon-ios="f7:arrow_left" icon-md="material:arrow_back" icon-aurora="f7:arrow_left" popup-close />
<f7-link icon-ios="f7:arrow_left" icon-md="material:arrow_back" icon-aurora="f7:arrow_left" @click="closeWithDirtyCheck" />
</f7-nav-left>
<f7-nav-title>Edit Widget Code</f7-nav-title>
<f7-nav-title>Edit Widget Code{{ dirtyIndicator }}</f7-nav-title>
<f7-nav-right>
<f7-link @click="updateWidgetCode" popup-close>
Done
<f7-link v-if="dirty" @click="reset">
Reset
</f7-link>
<f7-link v-if="dirty" @click="save">
Save
</f7-link>
<f7-link v-else popup-close>
Close
</f7-link>
</f7-nav-right>
</f7-navbar>
<editor class="page-code-editor" :mode="`application/vnd.openhab.uicomponent+yaml;type=${componentType || 'widget'}`" :value="code" @input="(value) => code = value" />
<editor class="page-code-editor" :mode="`application/vnd.openhab.uicomponent+yaml;type=${componentType || 'widget'}`" :value="code" @input="update" />
<!-- <pre class="yaml-message padding-horizontal" :class="[widgetYamlError === 'OK' ? 'text-color-green' : 'text-color-red']">{{widgetYamlError}}</pre> -->
</f7-page>
</f7-popup>
Expand All @@ -34,14 +40,20 @@

<script>
import YAML from 'yaml'
import DirtyMixin from '@/pages/settings/dirty-mixin'
import MovablePopupMixin from '@/pages/settings/movable-popup-mixin'
export default {
props: ['component', 'componentType'],
mixins: [DirtyMixin, MovablePopupMixin],
components: {
'editor': () => import(/* webpackChunkName: "script-editor" */ '@/components/config/controls/script-editor.vue')
},
data () {
return {
leaveCancelled: false,
updateTimer: null,
originalCode: null,
code: null
}
},
Expand All @@ -58,14 +70,70 @@ export default {
methods: {
widgetCodeOpened () {
this.code = YAML.stringify(this.component)
this.originalCode = this.code
this.$nextTick(() => {
// this.$refs.navbar only exists after the code editor finished rendering
this.initializeMovablePopup(this.$refs.widgetCode, this.$refs.navbar)
})
window.addEventListener('keydown', this.onKeydown)
},
widgetCodeClosed () {
this.cleanupMovablePopup()
window.removeEventListener('keydown', this.onKeydown)
this.$f7.emit('widgetCodeClosed')
this.$emit('closed')
},
updateWidgetCode () {
this.$f7.emit('widgetCodeUpdate', this.code)
this.$emit('update', this.code)
onKeydown (evt) {
if (evt.key === 'Escape' && !this.leaveCancelled) {
this.closeWithDirtyCheck()
}
},
closeWithDirtyCheck () {
if (this.dirty) {
const dialog = this.confirmLeaveWithoutSaving(
() => {
this.updateWidgetCode(this.originalCode)
this.$refs.widgetCode.close()
},
() => {
// prevent re-triggering the confirm dialog when ESC is pressed to close the dialog
this.leaveCancelled = true
setTimeout(() => { this.leaveCancelled = false }, 100)
}
)
} else {
this.$refs.widgetCode.close()
}
},
reset () {
this.$f7.dialog.confirm('Do you want to revert your changes?', 'Revert Changes', () => {
this.code = this.originalCode
this.updateWidgetCode(this.code)
this.dirty = false
})
},
save () {
if (this.widgetYamlError !== 'OK') {
this.$f7.dialog.alert('Invalid YAML: ' + this.widgetYamlError, 'Unable to save changes').open()
return
}
this.updateWidgetCode(this.code)
this.$refs.widgetCode.close()
},
updateWidgetCode (value) {
this.$f7.emit('widgetCodeUpdate', value)
this.$emit('update', value)
},
update (value) {
this.code = value
clearTimeout(this.updateTimer)
this.updateTimer = setTimeout(() => {
// Since update will be called on every key stroke, debounce the dirty check too
this.dirty = this.code !== this.originalCode
if (this.widgetYamlError === 'OK') {
this.updateWidgetCode(this.code)
}
}, 200)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
<template>
<f7-popup ref="widgetConfig" class="widgetconfig-popup" close-on-escape @popup:open="widgetConfigOpened" @popup:closed="widgetConfigClosed">
<f7-popup ref="widgetConfig" class="widgetconfig-popup" :close-by-backdrop-click="false" @popup:opened="widgetConfigOpened" @popup:closed="widgetConfigClosed">
<f7-page v-if="component && widget">
<f7-navbar>
<f7-navbar ref="navbar">
<f7-nav-left>
<f7-link icon-ios="f7:arrow_left" icon-md="material:arrow_back" icon-aurora="f7:arrow_left" popup-close />
<f7-link icon-ios="f7:arrow_left" icon-md="material:arrow_back" icon-aurora="f7:arrow_left" @click="closeWithDirtyCheck" />
</f7-nav-left>
<f7-nav-title>Edit {{ widget.label || widget.uid }}</f7-nav-title>
<f7-nav-title>Edit {{ widget.label || widget.uid }}{{ dirtyIndicator }}</f7-nav-title>
<f7-nav-right>
<f7-link @click="updateWidgetConfig" popup-close>
Done
<f7-link v-if="dirty" @click="reset">
Reset
</f7-link>
<f7-link v-if="dirty" @click="save">
Save
</f7-link>
<f7-link v-else popup-close>
Close
</f7-link>
</f7-nav-right>
</f7-navbar>
Expand All @@ -18,7 +24,7 @@
:parameterGroups="widget.props.parameterGroups || []"
:parameters="widget.props.parameters || []"
:configuration="config"
@updated="dirty = true" />
@updated="updated" />
</f7-col>
</f7-block>
</f7-page>
Expand All @@ -32,28 +38,82 @@

<script>
import ConfigSheet from '@/components/config/config-sheet.vue'
import DirtyMixin from '@/pages/settings/dirty-mixin'
import cloneDeep from 'lodash/cloneDeep'
import fastDeepEqual from 'fast-deep-equal/es6'
import MovablePopupMixin from '@/pages/settings/movable-popup-mixin'
export default {
mixins: [DirtyMixin, MovablePopupMixin],
props: ['opened', 'component', 'widget'],
components: {
ConfigSheet
},
data () {
return {
originalConfig: null,
updateTimer: null,
leaveCancelled: false,
config: null
}
},
methods: {
widgetConfigOpened () {
this.config = JSON.parse(JSON.stringify(this.component.config))
this.config = cloneDeep(this.component.config)
this.originalConfig = cloneDeep(this.component.config)
this.initializeMovablePopup(this.$refs.widgetConfig, this.$refs.navbar)
window.addEventListener('keydown', this.onKeydown)
},
widgetConfigClosed () {
this.cleanupMovablePopup()
window.removeEventListener('keydown', this.onKeydown)
this.$f7.emit('widgetConfigClosed')
this.$emit('closed')
},
updateWidgetConfig () {
this.$f7.emit('widgetConfigUpdate', this.config)
this.$emit('update', this.config)
onKeydown (evt) {
if (evt.key === 'Escape' && !this.leaveCancelled) {
this.closeWithDirtyCheck()
}
},
closeWithDirtyCheck () {
if (this.dirty) {
const dialog = this.confirmLeaveWithoutSaving(
() => {
this.updateWidgetConfig(this.originalConfig)
this.$refs.widgetConfig.close()
},
() => {
// prevent re-triggering the confirm dialog when ESC is pressed to close the dialog
this.leaveCancelled = true
setTimeout(() => { this.leaveCancelled = false }, 100)
}
)
} else {
this.$refs.widgetConfig.close()
}
},
reset () {
this.$f7.dialog.confirm('Do you want to revert your changes?', 'Revert Changes', () => {
this.config = cloneDeep(this.originalConfig)
this.updateWidgetConfig(this.originalConfig)
this.dirty = false
})
},
save () {
this.updateWidgetConfig(this.config)
this.$refs.widgetConfig.close()
},
updateWidgetConfig (config) {
const newConfig = cloneDeep(config)
this.$f7.emit('widgetConfigUpdate', newConfig)
this.$emit('update', newConfig)
},
updated () {
this.dirty = !fastDeepEqual(this.config, this.originalConfig)
clearTimeout(this.updateTimer)
this.updateTimer = setTimeout(() => {
this.updateWidgetConfig(this.config)
}, 500)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,12 @@ export default {
}
}
})
this.$f7.once('widgetConfigUpdate', (config) => {
const updateWidgetConfig = (config) => {
this.$f7.emit('svgOnClickConfigUpdate', { id, config })
}
this.$f7.on('widgetConfigUpdate', updateWidgetConfig)
this.$f7.once('widgetConfigClosed', () => {
this.$f7.off('widgetConfigUpdate', updateWidgetConfig)
})
},
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,8 @@ export default {
])
}
})
this.$f7.once('widgetConfigUpdate', this.storeLocalConfig)
this.$f7.on('widgetConfigUpdate', this.storeLocalConfig)
this.$f7.once('widgetConfigClosed', () => this.$f7.off('widgetConfigUpdate', this.storeLocalConfig))
},
storeLocalConfig (config) {
this.localConfig = config
Expand Down
Loading

0 comments on commit ed9aa31

Please sign in to comment.