Skip to content

Commit d0f45d0

Browse files
committed
Thing and Item Details: Use core's file-format service to display code in YAML and DSL
Signed-off-by: Jimmy Tanagra <[email protected]>
1 parent efe3fef commit d0f45d0

File tree

5 files changed

+268
-117
lines changed

5 files changed

+268
-117
lines changed
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
<template>
2+
<f7-block class="editor">
3+
<f7-icon v-if="readOnly" f7="lock" class="float-right margin"
4+
style="opacity:0.5; z-index: 4000; user-select: none;" size="50" color="gray" :tooltip="readOnlyMsg" />
5+
<editor class="thing-code-editor"
6+
:mode="editorMode"
7+
:value="code"
8+
:hint-context="hintContext"
9+
:read-only="readOnly"
10+
@input="onEditorInput" />
11+
<!-- <pre class="yaml-message padding-horizontal" :class="[yamlError === 'OK' ? 'text-color-green' : 'text-color-red']">{{yamlError}}</pre> -->
12+
<f7-toolbar bottom>
13+
<f7-segmented>
14+
<f7-button outline small v-for="type in Object.keys(mediaTypes)" :key="type" :active="codeType === type" @click="switchCodeType(type)">
15+
{{ type }}
16+
</f7-button>
17+
</f7-segmented>
18+
</f7-toolbar>
19+
</f7-block>
20+
</template>
21+
22+
<style lang="stylus">
23+
.editor
24+
position absolute
25+
top calc(var(--f7-tabbar-height))
26+
height calc(100% - 2*var(--f7-navbar-height))
27+
width 100%
28+
margin 0!important
29+
30+
.thing-code-editor.vue-codemirror
31+
top 0
32+
33+
.toolbar-bottom
34+
position absolute
35+
.segmented
36+
.button
37+
width 5em
38+
39+
</style>
40+
41+
<script>
42+
import FileDefinition from '@/pages/settings/file-definition-mixin'
43+
44+
export default {
45+
mixins: [FileDefinition],
46+
components: {
47+
'editor': () => import(/* webpackChunkName: "script-editor" */ '@/components/config/controls/script-editor.vue')
48+
},
49+
// objectType is the type of the object, e.g. 'items', 'things'. This corresponds to the yaml element name.
50+
props: ['object', 'objectType', 'objectId', 'hintContext', 'readOnly', 'readOnlyMsg'],
51+
// @updated event is emitted when the code has been parsed back into an object
52+
// as a result of calling the parseCode() method
53+
// NOT when the user is just typing in the editor
54+
// The parsed object is passed as the argument.
55+
// @changed event is emitted when the code is changed in the editor
56+
// The code editor's dirty status is passed as a boolean argument.
57+
emits: ['changed', 'updated'],
58+
beforeMount () {
59+
switch (this.objectType) {
60+
case 'items':
61+
this.mediaTypes = { YAML: this.MediaType.YAML, DSL: this.MediaType.ITEM_DSL }
62+
break
63+
case 'things':
64+
this.mediaTypes = { YAML: this.MediaType.YAML, DSL: this.MediaType.THING_DSL }
65+
break
66+
default:
67+
this.mediaTypes = { YAML: this.MediaType.YAML }
68+
}
69+
},
70+
data () {
71+
return {
72+
// the first key in the mediaTypes object is the default codeType (e.g. YAML)
73+
codeType: localStorage.getItem('openhab.ui:codeViewer.type') || Object.keys(this.mediaTypes)[0],
74+
code: null,
75+
originalCode: null,
76+
displayCodeSwitcher: false,
77+
dirty: false,
78+
yamlVersion: null
79+
}
80+
},
81+
computed: {
82+
editorMode () {
83+
if (this.codeType === 'DSL') {
84+
return 'text/x-java'
85+
}
86+
switch (this.objectType) {
87+
case 'items':
88+
return 'application/vnd.openhab.item+yaml'
89+
case 'things':
90+
return 'application/vnd.openhab.thing+yaml'
91+
default:
92+
return 'application/yaml'
93+
}
94+
}
95+
},
96+
methods: {
97+
/**
98+
* Generate code from object property
99+
*
100+
* Called from the parent component to update the code from object
101+
*
102+
* @param {string} codeType - Optional. The type of code to generate (e.g. YAML, DSL)
103+
* @param {function} onSuccessCallback - Optional. A callback function to call when the code has been generated
104+
*/
105+
generateCode (codeType, onSuccessCallback) {
106+
codeType ||= this.codeType
107+
const mediaType = this.mediaTypes[codeType]
108+
const payload = {}
109+
payload[this.objectType] = [this.object]
110+
this.$oh.api.postPlain('/rest/file-format/create', JSON.stringify(payload), null, 'application/json', { accept: mediaType })
111+
.then((code) => {
112+
if (codeType === 'YAML') {
113+
// skip version:, things: / items: and UID: lines, and unindent lines
114+
code = code.split('\n')
115+
this.yamlVersion = code[0]
116+
code = code.slice(3).map(line => line.replace(/^\s{4}/, '')).join('\n')
117+
}
118+
this.code = code
119+
this.originalCode = code.repeat(1) // duplicate the string
120+
this.dirty = false
121+
if (onSuccessCallback) {
122+
onSuccessCallback()
123+
}
124+
})
125+
.catch((err) => {
126+
this.$f7.dialog.alert(`Error creating ${codeType}: ${err}`).open()
127+
})
128+
},
129+
/**
130+
* Parse code back into an object
131+
*
132+
* Called from the parent component to update the object from code.
133+
* The resulting object is emitted in an {update} event.
134+
*
135+
* @param {function} onSuccessCallback - Optional. A callback function to call when the code has been parsed
136+
*/
137+
parseCode (onSuccessCallback) {
138+
const mediaType = this.mediaTypes[this.codeType]
139+
let payload = this.code
140+
if (this.codeType === 'YAML') {
141+
const indentedCode = payload.split('\n').map(line => ' ' + line).join('\n')
142+
payload = `${this.yamlVersion}\n${this.objectType}:\n ${this.objectId}:\n${indentedCode}`
143+
}
144+
this.$oh.api.postPlain('/rest/file-format/parse', payload, null, mediaType, { accept: 'application/json' })
145+
.then((data) => {
146+
let object = JSON.parse(data)
147+
object = object[this.objectType]
148+
if (object?.length > 0) {
149+
this.$emit('updated', object[0])
150+
if (onSuccessCallback) {
151+
onSuccessCallback()
152+
}
153+
} else {
154+
this.$f7.dialog.alert(`Error parsing ${this.codeType}: no ${this.objectType} found`).open()
155+
}
156+
})
157+
.catch((err) => {
158+
this.$f7.dialog.alert(`Error parsing ${this.codeType}: ${err}`).open()
159+
})
160+
},
161+
onEditorInput (value) {
162+
this.code = value
163+
this.dirty = this.code !== this.originalCode
164+
this.$emit('changed', this.dirty)
165+
},
166+
switchCodeType (type) {
167+
if (this.codeType === type) return
168+
if (this.readOnly || !this.dirty) {
169+
this.generateCode(type, () => {
170+
localStorage.setItem('openhab.ui:codeViewer.type', type)
171+
this.codeType = type
172+
})
173+
} else {
174+
this.parseCode((object) => {
175+
this.generateCode(type, () => {
176+
localStorage.setItem('openhab.ui:codeViewer.type', type)
177+
this.codeType = type
178+
})
179+
})
180+
}
181+
}
182+
}
183+
}
184+
185+
</script>

bundles/org.openhab.ui/web/src/components/config/controls/script-editor.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,12 @@ export default {
172172
this.codemirror.closeHint()
173173
}
174174
},
175+
watch: {
176+
mode (newMode) {
177+
this.cmOptions.mode = this.translateMode(newMode)
178+
this.codemirror.setOption('mode', this.cmOptions.mode)
179+
}
180+
},
175181
methods: {
176182
translateMode (mode) {
177183
// Translations required for some special modes used in MainUI

bundles/org.openhab.ui/web/src/pages/settings/file-definition-mixin.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ export default {
3535
THING: 'thing',
3636
ITEM: 'item'
3737
})
38+
this.MediaType = Object.freeze({
39+
YAML: 'application/yaml',
40+
THING_DSL: 'text/vnd.openhab.dsl.thing',
41+
ITEM_DSL: 'text/vnd.openhab.dsl.item',
42+
JSON: 'application/json'
43+
})
3844
},
3945
methods: {
4046
/**

bundles/org.openhab.ui/web/src/pages/settings/items/item-edit.vue

Lines changed: 43 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,16 @@
1010
</f7-nav-right>
1111
</f7-navbar>
1212
<f7-toolbar tabbar position="top">
13-
<f7-link @click="switchTab('design', fromYaml)" :tab-link-active="currentTab === 'design'" class="tab-link">
13+
<f7-link @click="switchTab('design')" :tab-link-active="currentTab === 'design'" class="tab-link">
1414
Design
1515
</f7-link>
16-
<f7-link @click="switchTab('code', toYaml)" :tab-link-active="currentTab === 'code'" class="tab-link">
16+
<f7-link @click="switchTab('code')" :tab-link-active="currentTab === 'code'" class="tab-link">
1717
Code
1818
</f7-link>
1919
</f7-toolbar>
2020

2121
<f7-tabs v-if="ready">
22-
<f7-tab id="design" @tab:show="() => this.currentTab = 'design'" :tab-active="currentTab === 'design'">
22+
<f7-tab id="design" :tab-active="currentTab === 'design'">
2323
<f7-block class="block-narrow" v-if="item.name || item.created === false">
2424
<f7-col v-if="!editable">
2525
<div class="padding-left">
@@ -38,53 +38,47 @@
3838
</f7-block>
3939
</f7-tab>
4040

41-
<f7-tab id="code" @tab:show="() => { this.currentTab = 'code'; toYaml() }" :tab-active="currentTab === 'code'">
42-
<f7-icon v-if="!editable" f7="lock" class="float-right margin" style="opacity:0.5; z-index: 4000; user-select: none;" size="50" color="gray" :tooltip="notEditableMsg" />
43-
<editor class="item-code-editor" mode="application/vnd.openhab.item+yaml" :value="itemYaml" @input="onEditorInput" :readOnly="!editable" />
41+
<f7-tab id="code" :tab-active="currentTab === 'code'">
42+
<code-editor ref="codeEditor"
43+
object-type="items"
44+
:object="item"
45+
:object-id="item.name"
46+
:read-only="!editable"
47+
:read-only-msg="notEditableMsg"
48+
@updated="updateItem"
49+
@changed="onCodeChanged" />
4450
</f7-tab>
4551
</f7-tabs>
4652
</f7-page>
4753
</template>
4854

49-
<style lang="stylus">
50-
.item-code-editor.vue-codemirror
51-
display block
52-
top calc(var(--f7-navbar-height) + var(--f7-tabbar-height))
53-
height calc(100% - 2*var(--f7-navbar-height))
54-
width 100%
55-
.yaml-message
56-
display block
57-
position absolute
58-
top 80%
59-
white-space pre-wrap
60-
</style>
61-
6255
<script>
6356
import cloneDeep from 'lodash/cloneDeep'
6457
import fastDeepEqual from 'fast-deep-equal/es6'
6558
6659
import * as Types from '@/assets/item-types.js'
67-
import YAML from 'yaml'
6860
6961
import ItemForm from '@/components/item/item-form.vue'
7062
7163
import DirtyMixin from '../dirty-mixin'
7264
import ItemMixin from '@/components/item/item-mixin'
65+
import CodeEditor from '@/components/config/controls/code-editor.vue'
7366
7467
export default {
7568
mixins: [DirtyMixin, ItemMixin],
7669
props: ['itemName', 'createMode', 'itemCopy'],
7770
components: {
7871
ItemForm,
79-
'editor': () => import(/* webpackChunkName: "script-editor" */ '@/components/config/controls/script-editor.vue')
72+
CodeEditor
8073
},
8174
data () {
8275
return {
8376
ready: false,
8477
loading: false,
8578
item: {},
8679
savedItem: {},
87-
itemYaml: '',
80+
itemDirty: false,
81+
codeDirty: false,
8882
items: [],
8983
types: Types,
9084
semanticClasses: this.$store.getters.semanticClasses,
@@ -110,12 +104,14 @@ export default {
110104
}
111105
},
112106
watch: {
107+
itemDirty: function () { this.dirty = this.itemDirty || this.codeDirty },
108+
codeDirty: function () { this.dirty = this.itemDirty || this.codeDirty },
113109
item: {
114110
handler: function () {
115111
if (!this.loading) { // ignore changes during loading
116112
const itemClone = cloneDeep(this.item)
117113
delete itemClone.functionKey
118-
this.dirty = !fastDeepEqual(itemClone, this.savedItem)
114+
this.itemDirty = !fastDeepEqual(itemClone, this.savedItem)
119115
}
120116
},
121117
deep: true
@@ -140,6 +136,19 @@ export default {
140136
ev.preventDefault()
141137
}
142138
},
139+
switchTab (tab) {
140+
if (this.currentTab === tab) return
141+
if (this.currentTab === 'code' && this.codeDirty) {
142+
this.$refs.codeEditor.parseCode(() => { this.codeDirty = false })
143+
}
144+
this.currentTab = tab
145+
if (this.currentTab === 'code') {
146+
this.$refs.codeEditor.generateCode()
147+
}
148+
},
149+
onCodeChanged (codeDirty) {
150+
this.codeDirty = codeDirty
151+
},
143152
load () {
144153
if (this.loading) return
145154
this.loading = true
@@ -173,9 +182,15 @@ export default {
173182
},
174183
save () {
175184
if (!this.editable) return
176-
if (this.currentTab === 'code') {
177-
if (!this.fromYaml()) return
185+
186+
if (this.currentTab === 'code' && this.codeDirty) {
187+
this.$refs.codeEditor.parseCode(() => {
188+
this.codeDirty = false
189+
this.save()
190+
})
191+
return
178192
}
193+
179194
if (this.validateItemName(this.item.name) !== '') return this.$f7.dialog.alert('Please give the Item a valid name: ' + this.validateItemName(this.item.name)).open()
180195
if (!this.item.type || !this.types.ItemTypes.includes(this.item.type.split(':')[0])) return this.$f7.dialog.alert('Please give Item a valid type').open()
181196
@@ -216,7 +231,7 @@ export default {
216231
}).open()
217232
}
218233
219-
this.dirty = false
234+
this.itemDirty = this.codeDirty = false
220235
if (this.createMode) {
221236
this.$f7router.navigate('/settings/items/' + this.item.name)
222237
} else {
@@ -230,34 +245,15 @@ export default {
230245
}).open()
231246
})
232247
},
233-
onEditorInput (value) {
234-
this.itemYaml = value
235-
},
236-
toYaml () {
237-
const yamlObj = {
238-
label: this.item.label,
239-
type: this.item.type,
240-
icon: this.item.category || '',
241-
groupNames: this.item.groupNames || [],
242-
tags: this.item.tags
243-
// metadata: this.item.metadata
244-
}
245-
if (this.item.type === 'Group') {
246-
yamlObj.groupType = this.item.groupType || 'None'
247-
yamlObj.function = this.item.function || 'None'
248-
}
249-
this.itemYaml = YAML.stringify(yamlObj)
250-
},
251-
fromYaml () {
248+
updateItem (updatedItem) {
252249
if (!this.editable) return false
253250
try {
254-
const updatedItem = YAML.parse(this.itemYaml)
255251
if (updatedItem === null) return false
256252
if (updatedItem.groupNames == null) updatedItem.groupNames = []
257253
if (updatedItem.tags == null) updatedItem.tags = []
258254
this.$set(this.item, 'label', updatedItem.label)
259255
this.$set(this.item, 'type', updatedItem.type)
260-
this.$set(this.item, 'category', updatedItem.icon)
256+
this.$set(this.item, 'category', updatedItem.category)
261257
this.$set(this.item, 'groupNames', updatedItem.groupNames)
262258
this.$set(this.item, 'groupType', updatedItem.groupType)
263259
this.$set(this.item, 'function', updatedItem.function)

0 commit comments

Comments
 (0)