diff --git a/bundles/org.openhab.ui/web/package-lock.json b/bundles/org.openhab.ui/web/package-lock.json index 2ac13b3c5..7c16306d4 100644 --- a/bundles/org.openhab.ui/web/package-lock.json +++ b/bundles/org.openhab.ui/web/package-lock.json @@ -11,17 +11,21 @@ "dependencies": { "@blockly/field-slider": "^2.1.10", "@blockly/zoom-to-fit": "^2.0.24", + "@jsep-plugin/arrow": "^1.0.5", + "@jsep-plugin/object": "^1.2.1", + "@jsep-plugin/regex": "^1.0.3", + "@jsep-plugin/template": "^1.0.2", "blockly": "^6.20210701.0", "cronstrue": "^1.100.0", "dayjs": "^1.9.6", "dom7": "^2.1.5", "echarts": "^5.1.2", "event-source-polyfill": "^1.0.22", - "expression-eval": "^2.1.0", "fast-deep-equal": "^3.1.3", "framework7": "^5.7.12", "framework7-icons": "^3.0.1", "framework7-vue": "^5.7.12", + "jse-eval": "^1.5.1", "jssip": "^3.9.1", "leaflet": "^1.7.1", "leaflet-providers": "^1.11.0", @@ -3407,6 +3411,50 @@ "node": ">= 6" } }, + "node_modules/@jsep-plugin/arrow": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@jsep-plugin/arrow/-/arrow-1.0.5.tgz", + "integrity": "sha512-4Q9/6nETEf79DQdyynPk9G5CvYGw/TyRAw6IpkiIBm1z6eyDyjhcLjYxmBCqlKIUvjS8h8hfU8MzSjQRSntK5Q==", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@jsep-plugin/object": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsep-plugin/object/-/object-1.2.1.tgz", + "integrity": "sha512-6YoZP80h2QFCuxyqj+OvoqEnTu2r5cSRpgpvGauWlvnevFP/F/dibpvXDpnHeqwT2FIzzvg47YOe3QD/UT8vJw==", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@jsep-plugin/regex": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.3.tgz", + "integrity": "sha512-XfZgry4DwEZvSFtS/6Y+R48D7qJYJK6R9/yJFyUFHCIUMEEHuJ4X95TDgJp5QkmzfLYvapMPzskV5HpIDrREug==", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@jsep-plugin/template": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@jsep-plugin/template/-/template-1.0.2.tgz", + "integrity": "sha512-fGIPHL4W/YZ3YShDByfWlLEIMCLRUTZDUDii4Xat4sJ+DTYeS21RWrZkvbprICyiwmO/fRY7xdHZh7K/ng6xLg==", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, "node_modules/@relative-ci/agent": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@relative-ci/agent/-/agent-1.5.0.tgz", @@ -9963,14 +10011,6 @@ "node": ">=0.6" } }, - "node_modules/expression-eval": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/expression-eval/-/expression-eval-2.1.0.tgz", - "integrity": "sha512-FUJO/Akvl/JOWkvlqZaqbkhsEWlCJWDeZG4tzX96UH68D9FeRgYgtb55C2qtqbORC0Q6x5419EDjWu4IT9kQfg==", - "dependencies": { - "jsep": "^0.3.0" - } - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -12942,12 +12982,20 @@ "node": ">=0.4.0" } }, + "node_modules/jse-eval": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/jse-eval/-/jse-eval-1.5.1.tgz", + "integrity": "sha512-wnMRaxvZUuBMxMNZd5Xs5z30c9Glb8p7hFERxrzdNKcWnqSAdvQM7+c+NCloaTniV/NtYpuQSsqEF+Twc6T71Q==", + "dependencies": { + "jsep": "^1.2.0" + } + }, "node_modules/jsep": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/jsep/-/jsep-0.3.4.tgz", - "integrity": "sha512-ovGD9wE+wvudIIYxZGrRcZCxNyZ3Cl1N7Bzyp7/j4d/tA0BaUwcVM9bu0oZaSrefMiNwv6TwZ9X15gvZosteCQ==", + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.3.7.tgz", + "integrity": "sha512-NFbZTr1t13fPKw53swmZFKwBkEDWDnno7uLJk+a+Rw9tGDTkGgnGdZJ8A/o3gR1+XaAXmSsbpfIBIBgqRBZWDA==", "engines": { - "node": ">= 0.10.0" + "node": ">= 10.16.0" } }, "node_modules/jsesc": { @@ -25910,6 +25958,30 @@ "@types/yargs": "^13.0.0" } }, + "@jsep-plugin/arrow": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@jsep-plugin/arrow/-/arrow-1.0.5.tgz", + "integrity": "sha512-4Q9/6nETEf79DQdyynPk9G5CvYGw/TyRAw6IpkiIBm1z6eyDyjhcLjYxmBCqlKIUvjS8h8hfU8MzSjQRSntK5Q==", + "requires": {} + }, + "@jsep-plugin/object": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsep-plugin/object/-/object-1.2.1.tgz", + "integrity": "sha512-6YoZP80h2QFCuxyqj+OvoqEnTu2r5cSRpgpvGauWlvnevFP/F/dibpvXDpnHeqwT2FIzzvg47YOe3QD/UT8vJw==", + "requires": {} + }, + "@jsep-plugin/regex": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.3.tgz", + "integrity": "sha512-XfZgry4DwEZvSFtS/6Y+R48D7qJYJK6R9/yJFyUFHCIUMEEHuJ4X95TDgJp5QkmzfLYvapMPzskV5HpIDrREug==", + "requires": {} + }, + "@jsep-plugin/template": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@jsep-plugin/template/-/template-1.0.2.tgz", + "integrity": "sha512-fGIPHL4W/YZ3YShDByfWlLEIMCLRUTZDUDii4Xat4sJ+DTYeS21RWrZkvbprICyiwmO/fRY7xdHZh7K/ng6xLg==", + "requires": {} + }, "@relative-ci/agent": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@relative-ci/agent/-/agent-1.5.0.tgz", @@ -31455,14 +31527,6 @@ } } }, - "expression-eval": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/expression-eval/-/expression-eval-2.1.0.tgz", - "integrity": "sha512-FUJO/Akvl/JOWkvlqZaqbkhsEWlCJWDeZG4tzX96UH68D9FeRgYgtb55C2qtqbORC0Q6x5419EDjWu4IT9kQfg==", - "requires": { - "jsep": "^0.3.0" - } - }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -33890,10 +33954,18 @@ } } }, + "jse-eval": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/jse-eval/-/jse-eval-1.5.1.tgz", + "integrity": "sha512-wnMRaxvZUuBMxMNZd5Xs5z30c9Glb8p7hFERxrzdNKcWnqSAdvQM7+c+NCloaTniV/NtYpuQSsqEF+Twc6T71Q==", + "requires": { + "jsep": "^1.2.0" + } + }, "jsep": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/jsep/-/jsep-0.3.4.tgz", - "integrity": "sha512-ovGD9wE+wvudIIYxZGrRcZCxNyZ3Cl1N7Bzyp7/j4d/tA0BaUwcVM9bu0oZaSrefMiNwv6TwZ9X15gvZosteCQ==" + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.3.7.tgz", + "integrity": "sha512-NFbZTr1t13fPKw53swmZFKwBkEDWDnno7uLJk+a+Rw9tGDTkGgnGdZJ8A/o3gR1+XaAXmSsbpfIBIBgqRBZWDA==" }, "jsesc": { "version": "2.5.2", diff --git a/bundles/org.openhab.ui/web/package.json b/bundles/org.openhab.ui/web/package.json index c9e6078fa..3cab8ba56 100644 --- a/bundles/org.openhab.ui/web/package.json +++ b/bundles/org.openhab.ui/web/package.json @@ -63,17 +63,21 @@ "dependencies": { "@blockly/field-slider": "^2.1.10", "@blockly/zoom-to-fit": "^2.0.24", + "@jsep-plugin/arrow": "^1.0.5", + "@jsep-plugin/object": "^1.2.1", + "@jsep-plugin/regex": "^1.0.3", + "@jsep-plugin/template": "^1.0.2", "blockly": "^6.20210701.0", "cronstrue": "^1.100.0", "dayjs": "^1.9.6", "dom7": "^2.1.5", "echarts": "^5.1.2", "event-source-polyfill": "^1.0.22", - "expression-eval": "^2.1.0", "fast-deep-equal": "^3.1.3", "framework7": "^5.7.12", "framework7-icons": "^3.0.1", "framework7-vue": "^5.7.12", + "jse-eval": "^1.5.1", "jssip": "^3.9.1", "leaflet": "^1.7.1", "leaflet-providers": "^1.11.0", diff --git a/bundles/org.openhab.ui/web/src/components/cards/glance/location/status-badge.vue b/bundles/org.openhab.ui/web/src/components/cards/glance/location/status-badge.vue index 393392308..0933236fe 100644 --- a/bundles/org.openhab.ui/web/src/components/cards/glance/location/status-badge.vue +++ b/bundles/org.openhab.ui/web/src/components/cards/glance/location/status-badge.vue @@ -34,7 +34,7 @@ <script> import { findEquipment, allEquipmentPoints, findPoints } from '../glance-helpers' -import expr from 'expression-eval' +import expr from 'jse-eval' export default { props: ['element', 'type', 'badgeOverrides', 'invertColor', 'store'], @@ -187,7 +187,7 @@ export default { reduce () { const ast = this.overrideExpression() if (ast) { - return this.map.filter((state) => expr.eval(ast, { state: state, Number: Number })).length + return this.map.filter((state) => expr.evaluate(ast, { state: state, Number: Number })).length } switch (this.type) { case 'blinds': diff --git a/bundles/org.openhab.ui/web/src/components/config/controls/script-editor.vue b/bundles/org.openhab.ui/web/src/components/config/controls/script-editor.vue index 331032234..e026461ae 100644 --- a/bundles/org.openhab.ui/web/src/components/config/controls/script-editor.vue +++ b/bundles/org.openhab.ui/web/src/components/config/controls/script-editor.vue @@ -255,7 +255,8 @@ export default { 'Ctrl-Space': 'autocomplete', '\'.\'': autocomplete, '\'=\'': autocomplete, - 'Space': autocomplete + 'Space': autocomplete, + '\'@\'': autocomplete } cm.state.$oh = this.$oh cm.state.originalMode = this.mode diff --git a/bundles/org.openhab.ui/web/src/components/config/editor/hint-components.js b/bundles/org.openhab.ui/web/src/components/config/editor/hint-components.js index d19cbd9ff..e8551cd7f 100644 --- a/bundles/org.openhab.ui/web/src/components/config/editor/hint-components.js +++ b/bundles/org.openhab.ui/web/src/components/config/editor/hint-components.js @@ -50,7 +50,7 @@ function getWidgetDefinitions (cm) { } } -function hintItems (cm, line, replaceAfterColon, addStatePropertySuffix) { +function hintItems (cm, line, replaceAfterColon, addStatePropertySuffix, addQuotes) { const cursor = cm.getCursor() const promise = (itemsCache) ? Promise.resolve(itemsCache) : cm.state.$oh.api.get('/rest/items') return promise.then((data) => { @@ -58,7 +58,7 @@ function hintItems (cm, line, replaceAfterColon, addStatePropertySuffix) { let ret = { list: data.map((item) => { return { - text: item.name + ((addStatePropertySuffix ? '.state' : '')), + text: (addQuotes ? '\'' : '') + item.name + ((addStatePropertySuffix ? '.state' : '')) + (addQuotes ? '\'' : ''), displayText: item.name, description: `${(item.label) ? item.label + ' ' : ''}(${item.type})<br />${item.state}` } @@ -69,6 +69,9 @@ function hintItems (cm, line, replaceAfterColon, addStatePropertySuffix) { const colonPos = line.indexOf(':') ret.from = { line: cursor.line, ch: colonPos + 2 } ret.to = { line: cursor.line, ch: line.length } + } else if (addQuotes) { + const lastAtOp = line.substring(0, cursor.ch).replace(/@[A-Za-z0-9_-]*$/, '@') + ret.to = { line: cursor.line, ch: lastAtOp.length } } else { const lastDot = line.substring(0, cursor.ch).replace(/\.[A-Za-z0-9_-]*$/, '.') ret.to = { line: cursor.line, ch: lastDot.length } @@ -110,11 +113,16 @@ function hintExpression (cm, line) { { text: 'theme', displayText: 'theme', description: 'The current theme: aurora, ios, or md' }, { text: 'themeOptions', displayText: 'themeOptions', description: 'Object with current theme options' }, { text: 'device', displayText: 'device', description: 'Object with information about the current device & browser' }, + { text: 'user', displayText: 'user', description: 'Access the username and roles of the logged in user' }, { text: 'screen', displayText: 'screen', description: 'Object with information about the screen and available view area' }, { text: 'dayjs', displayText: 'dayjs', description: 'Access to the Day.js object for date manipulation & formatting' } ] } } else { + const lastAtOp = line.substring(0, cursor.ch).replace(/@[A-Za-z0-9_-]*$/, '@') + if (lastAtOp.endsWith('@')) { + return hintItems(cm, line, false, false, true) + } const lastDot = line.substring(0, cursor.ch).replace(/\.[A-Za-z0-9_-]*$/, '.') if (lastDot.endsWith('items.')) { return hintItems(cm, line, false, true) diff --git a/bundles/org.openhab.ui/web/src/components/config/editor/hint-utils.js b/bundles/org.openhab.ui/web/src/components/config/editor/hint-utils.js index 438d87e0d..7707912fc 100644 --- a/bundles/org.openhab.ui/web/src/components/config/editor/hint-utils.js +++ b/bundles/org.openhab.ui/web/src/components/config/editor/hint-utils.js @@ -31,7 +31,7 @@ export function remove (node) { export function filterPartialCompletions (cm, line, completions, property = 'text') { const cursor = cm.getCursor() const lineBeforeCursor = line.substring(0, cursor.ch) - const completionBeginPos = Math.max(lineBeforeCursor.lastIndexOf(' '), lineBeforeCursor.lastIndexOf('.')) + const completionBeginPos = Math.max(lineBeforeCursor.lastIndexOf(' '), lineBeforeCursor.lastIndexOf('.'), lineBeforeCursor.lastIndexOf('@')) const partialCompletion = lineBeforeCursor.substring(completionBeginPos + 1) return completions.filter((c) => c[property] && c[property].toLowerCase().indexOf(partialCompletion.toLowerCase()) >= 0) } diff --git a/bundles/org.openhab.ui/web/src/components/widgets/generic-widget-component.vue b/bundles/org.openhab.ui/web/src/components/widgets/generic-widget-component.vue index 53ed4ed4c..89630d63b 100644 --- a/bundles/org.openhab.ui/web/src/components/widgets/generic-widget-component.vue +++ b/bundles/org.openhab.ui/web/src/components/widgets/generic-widget-component.vue @@ -13,10 +13,21 @@ <div v-else-if="componentType && componentType === 'Label' && visible" :class="config.class" :style="config.style"> {{ config.text }} </div> + <fragment v-else-if="componentType && componentType === 'Content'"> + {{ config.text }} + </fragment> <pre v-else-if="componentType && componentType === 'Error' && visible" class="text-color-red" style="white-space: pre-wrap">{{ config.error }}</pre> + <component v-else-if="visible" :is="componentType" v-bind="config"> + {{ config.content }} + <template v-if="context.component.slots && context.component.slots.default"> + <generic-widget-component :context="childContext(slotComponent)" v-for="(slotComponent, idx) in context.component.slots.default" :key="'default-' + idx" /> + </template> + </component> </template> <script> +import { Fragment } from 'vue-fragment' + import mixin from './widget-mixin' import * as SystemWidgets from './system/index' @@ -28,6 +39,7 @@ import * as LayoutWidgets from './layout/index' export default { mixins: [mixin], components: { + Fragment, ...SystemWidgets, ...StandardWidgets, ...StandardListWidgets, diff --git a/bundles/org.openhab.ui/web/src/components/widgets/widget-mixin.js b/bundles/org.openhab.ui/web/src/components/widgets/widget-mixin.js index 2f232e16e..fa50ca521 100644 --- a/bundles/org.openhab.ui/web/src/components/widgets/widget-mixin.js +++ b/bundles/org.openhab.ui/web/src/components/widgets/widget-mixin.js @@ -1,6 +1,6 @@ // Import into widget components as a mixin! -import expr from 'expression-eval' +import expr from 'jse-eval' import dayjs from 'dayjs' import relativeTime from 'dayjs/plugin/relativeTime' import calendar from 'dayjs/plugin/calendar' @@ -10,6 +10,22 @@ import isToday from 'dayjs/plugin/isToday' import isYesterday from 'dayjs/plugin/isYesterday' import isTomorrow from 'dayjs/plugin/isTomorrow' import scope from 'scope-css' +import store from '@/js/store' + +import jsepRegex from '@jsep-plugin/regex' +import jsepArrow from '@jsep-plugin/arrow' +import jsepObject from '@jsep-plugin/object' +import jsepTemplate from '@jsep-plugin/template' +expr.jsep.plugins.register(jsepRegex, jsepArrow, jsepObject, jsepTemplate) + +expr.addUnaryOp('@', (itemName) => { + const itemState = store.getters.trackedItems[itemName] + if (itemState.displayState === undefined) return itemState.state + return itemState.displayState +}) +expr.addUnaryOp('@@', (itemName) => { + return store.getters.trackedItems[itemName].state +}) dayjs.extend(relativeTime) dayjs.extend(calendar) @@ -30,7 +46,7 @@ export default { }, computed: { componentType () { - return this.context.component.component + return this.evaluateExpression('type', this.context.component.component) }, childWidgetComponentType () { if (!this.componentType.startsWith('widget:')) return null @@ -112,7 +128,7 @@ export default { if (!this.exprAst[key] || ctx.editmode) { this.exprAst[key] = expr.parse(value.substring(1)) } - return expr.eval(this.exprAst[key], { + return expr.evaluate(this.exprAst[key], { items: ctx.store, props: this.props, vars: ctx.vars, @@ -124,7 +140,8 @@ export default { device: this.$device, screen: this.getScreenInfo(), JSON: JSON, - dayjs: dayjs + dayjs: dayjs, + user: this.$store.getters.user }) } catch (e) { return e