diff --git a/bundles/org.openhab.ui/web/src/assets/sitemap-lexer.nearley b/bundles/org.openhab.ui/web/src/assets/sitemap-lexer.nearley index 237119c75..d57c53eea 100644 --- a/bundles/org.openhab.ui/web/src/assets/sitemap-lexer.nearley +++ b/bundles/org.openhab.ui/web/src/assets/sitemap-lexer.nearley @@ -44,7 +44,7 @@ comma: ',', colon: ':', hyphen: '-', - number: /-?[0-9]+(?:\.[0-9]*)?/, + number: /[+-]?[0-9]+(?:\.[0-9]*)?/, string: { match: /"(?:\\["\\]|[^\n"\\])*"/, value: x => x.slice(1, -1) } }) const requiresItem = ['Group', 'Chart', 'Switch', 'Mapview', 'Slider', 'Selection', 'Setpoint', 'Input ', 'Colorpicker', 'Default'] @@ -154,17 +154,21 @@ WidgetColorAttrValue -> %lbracket _ Colors _ %rbracket Mappings -> Mapping {% (d) => [d[0]] %} | Mappings _ %comma _ Mapping {% (d) => d[0].concat([d[4]]) %} -Mapping -> Command _ %equals _ Label {% (d) => d[0][0].value + '=' + d[4][0].value %} - | Command _ %equals _ Label _ %equals _ WidgetIconAttrValue {% (d) => d[0][0].value + '=' + d[4][0].value + '=' + d[8].join("") %} +Mapping -> Command _ %colon _ Command _ %equals _ Label {% (d) => d[0] + ':' + d[4] + '=' + d[8] %} + | Command _ %equals _ Label {% (d) => d[0] + '=' + d[4] %} + | Command _ %colon _ Command _ %equals _ Label _ %equals _ WidgetIconAttrValue {% (d) => d[0] + ':' + d[4] + '=' + d[8] + '=' + d[12].join("") %} + | Command _ %equals _ Label _ %equals _ WidgetIconAttrValue {% (d) => d[0] + '=' + d[4] + '=' + d[8].join("") %} Buttons -> Button {% (d) => [d[0]] %} | Buttons _ %comma _ Button {% (d) => d[0].concat([d[4]]) %} Button -> %number _ %colon _ %number _ %colon _ ButtonValue {% (d) => { return { 'row': parseInt(d[0].value), 'column': parseInt(d[4].value), 'command': d[8] } } %} -ButtonValue -> Command _ %equals _ Label {% (d) => d[0][0].value + '=' + d[4][0].value %} - | Command _ %equals _ Label _ %equals _ WidgetIconAttrValue {% (d) => d[0][0].value + '=' + d[4][0].value + '=' + d[8].join("") %} +ButtonValue -> Command _ %equals _ Label {% (d) => d[0][0].value + '=' + d[4] %} + | Command _ %equals _ Label _ %equals _ WidgetIconAttrValue {% (d) => d[0][0].value + '=' + d[4] + '=' + d[8].join("") %} -Command -> %number | %identifier | %string -Label -> %number | %identifier | %string +Command -> %number | %identifier {% (d) => d[0].value %} + | %string {% (d) => '"' + d[0].value + '"' %} +Label -> %number | %identifier {% (d) => d[0].value %} + | %string {% (d) => '"' + d[0].value + '"' %} Visibilities -> Conditions {% (d) => d[0] %} | Visibilities _ %comma _ Conditions {% (d) => d[0].concat(d[4]) %} diff --git a/bundles/org.openhab.ui/web/src/components/pagedesigner/sitemap/__tests__/dslUtil_jest.spec.js b/bundles/org.openhab.ui/web/src/components/pagedesigner/sitemap/__tests__/dslUtil_jest.spec.js index a1abd5420..8560285f3 100644 --- a/bundles/org.openhab.ui/web/src/components/pagedesigner/sitemap/__tests__/dslUtil_jest.spec.js +++ b/bundles/org.openhab.ui/web/src/components/pagedesigner/sitemap/__tests__/dslUtil_jest.spec.js @@ -113,9 +113,9 @@ describe('dslUtil', () => { mappings: [ '1=Morning', '2=Evening', - '10=Cinéma', + '10="Cinéma"', '11=TV', - '3=Bed time', + '3="Bed time"', '4=Night=moon' ] }) @@ -132,9 +132,9 @@ describe('dslUtil', () => { buttons: [ { row: 1, column: 1, command: '1=Morning' }, { row: 1, column: 2, command: '2=Evening' }, - { row: 1, column: 3, command: '10=Cinéma' }, + { row: 1, column: 3, command: '10="Cinéma"' }, { row: 2, column: 1, command: '11=TV' }, - { row: 2, column: 2, command: '3=Bed time' }, + { row: 2, column: 2, command: '3="Bed time"' }, { row: 2, column: 3, command: '4=Night=moon' } ] }) @@ -149,15 +149,39 @@ describe('dslUtil', () => { addWidget(component, 'Selection', { item: 'Echos', mappings: [ - 'EchoDot1=Echo 1', - 'EchoDot2=Echo 2', - 'EchoDot1,EchoDot2=Alle' + 'EchoDot1="Echo 1"', + 'EchoDot2="Echo 2"', + '"EchoDot1,EchoDot2"=Alle' ] }) const sitemap = dslUtil.toDsl(component).split('\n') expect(sitemap[1]).toEqual(' Selection item=Echos mappings=[EchoDot1="Echo 1", EchoDot2="Echo 2", "EchoDot1,EchoDot2"=Alle]') }) + it('renders a widget with mappings and release command correctly', () => { + const component = createSitemapComponent('test', 'Test') + const widget = { + } + addWidget(component, 'Switch', { + item: 'pressAndRelease', + mappings: ['ON:OFF=ON'] + }) + const sitemap = dslUtil.toDsl(component).split('\n') + expect(sitemap[1]).toEqual(' Switch item=pressAndRelease mappings=[ON:OFF=ON]') + }) + + it('renders a widget with mappings and release command and string commands correctly', () => { + const component = createSitemapComponent('test', 'Test') + const widget = { + } + addWidget(component, 'Switch', { + item: 'pressAndRelease', + mappings: ['"ON command":"OFF command"="ON"'] + }) + const sitemap = dslUtil.toDsl(component).split('\n') + expect(sitemap[1]).toEqual(' Switch item=pressAndRelease mappings=["ON command":"OFF command"="ON"]') + }) + it('renders a widget with 0 value parameter correctly', () => { const component = createSitemapComponent('test', 'Test') const widget = { diff --git a/bundles/org.openhab.ui/web/src/components/pagedesigner/sitemap/__tests__/sitemap-code_jest.spec.js b/bundles/org.openhab.ui/web/src/components/pagedesigner/sitemap/__tests__/sitemap-code_jest.spec.js index 883cebe45..1cbb46bb6 100644 --- a/bundles/org.openhab.ui/web/src/components/pagedesigner/sitemap/__tests__/sitemap-code_jest.spec.js +++ b/bundles/org.openhab.ui/web/src/components/pagedesigner/sitemap/__tests__/sitemap-code_jest.spec.js @@ -219,10 +219,10 @@ describe('SitemapCode', () => { item: 'Scene_General', buttons: [ { row: 1, column: 1, command: '1=Morning' }, - { row: 1, column: 2, command: '2=Evening' }, - { row: 1, column: 3, command: '10=Cinéma' }, + { row: 1, column: 2, command: '2="Evening"' }, + { row: 1, column: 3, command: '10="Cinéma"' }, { row: 2, column: 1, command: '11=TV' }, - { row: 2, column: 2, command: '3=Bed time' }, + { row: 2, column: 2, command: '3="Bed time"' }, { row: 2, column: 3, command: '4=Night=moon' } ] } @@ -259,10 +259,10 @@ describe('SitemapCode', () => { item: 'Scene_General', mappings: [ '1=Morning', - '2=Evening', - '10=Cinéma', + '2="Evening"', + '10="Cinéma"', '11=TV', - '3=Bed time', + '3="Bed time"', '4=Night=moon' ] } @@ -298,9 +298,79 @@ describe('SitemapCode', () => { config: { item: 'Echos', mappings: [ - 'EchoDot1=Echo 1', - 'EchoDot2=Echo 2', - 'EchoDot1,EchoDot2=Alle' + 'EchoDot1="Echo 1"', + 'EchoDot2="Echo 2"', + '"EchoDot1,EchoDot2"=Alle' + ] + } + }) + }) + + it('parses a mapping with release command', async () => { + expect(wrapper.vm.sitemapDsl).toBeDefined() + // simulate updating the sitemap in code + const sitemap = [ + 'sitemap test label="Test" {', + ' Switch item=PressAndRelease mappings=[ON:OFF=ON]', + '}', + '' + ].join('\n') + wrapper.vm.updateSitemap(sitemap) + expect(wrapper.vm.sitemapDsl).toMatch(/^sitemap test label="Test"/) + expect(wrapper.vm.parsedSitemap.error).toBeFalsy() + + await wrapper.vm.$nextTick() + + // check whether an 'updated' event was emitted and its payload + // (should contain the parsing result for the new sitemap definition) + const events = wrapper.emitted().updated + expect(events).toBeTruthy() + expect(events.length).toBe(1) + const payload = events[0][0] + expect(payload.slots).toBeDefined() + expect(payload.slots.widgets).toBeDefined() + expect(payload.slots.widgets.length).toBe(1) + expect(payload.slots.widgets[0]).toEqual({ + component: 'Switch', + config: { + item: 'PressAndRelease', + mappings: [ + 'ON:OFF=ON' + ] + } + }) + }) + + it('parses a mapping with release command and string commands', async () => { + expect(wrapper.vm.sitemapDsl).toBeDefined() + // simulate updating the sitemap in code + const sitemap = [ + 'sitemap test label="Test" {', + ' Switch item=PressAndRelease mappings=["ON command":"OFF command"="ON"]', + '}', + '' + ].join('\n') + wrapper.vm.updateSitemap(sitemap) + expect(wrapper.vm.sitemapDsl).toMatch(/^sitemap test label="Test"/) + expect(wrapper.vm.parsedSitemap.error).toBeFalsy() + + await wrapper.vm.$nextTick() + + // check whether an 'updated' event was emitted and its payload + // (should contain the parsing result for the new sitemap definition) + const events = wrapper.emitted().updated + expect(events).toBeTruthy() + expect(events.length).toBe(1) + const payload = events[0][0] + expect(payload.slots).toBeDefined() + expect(payload.slots.widgets).toBeDefined() + expect(payload.slots.widgets.length).toBe(1) + expect(payload.slots.widgets[0]).toEqual({ + component: 'Switch', + config: { + item: 'PressAndRelease', + mappings: [ + '"ON command":"OFF command"="ON"' ] } }) diff --git a/bundles/org.openhab.ui/web/src/components/pagedesigner/sitemap/dslUtil.js b/bundles/org.openhab.ui/web/src/components/pagedesigner/sitemap/dslUtil.js index 502d9a483..20f7f7f9d 100644 --- a/bundles/org.openhab.ui/web/src/components/pagedesigner/sitemap/dslUtil.js +++ b/bundles/org.openhab.ui/web/src/components/pagedesigner/sitemap/dslUtil.js @@ -29,11 +29,11 @@ function writeWidget (widget, indent) { dsl += widget.config[key] } else if (key === 'mappings') { dsl += '[' + widget.config[key].filter(Boolean).map(mapping => { - return writeCommand(mapping) + return mapping }).join(', ') + ']' } else if (key === 'buttons') { dsl += '[' + widget.config[key].filter(Boolean).map(button => { - return button.row + ':' + button.column + ':' + writeCommand(button.command) + return button.row + ':' + button.column + ':' + button.command }).join(', ') + ']' } else if (key === 'visibility') { dsl += '[' + widget.config[key].filter(Boolean).map(rule => { @@ -61,15 +61,6 @@ function writeWidget (widget, indent) { return dsl } -function writeCommand (command) { - return command.split('=').map(value => { - if (/^.*\W.*$/.test(value) && /^[^"'].*[^"']$/.test(value)) { - return '"' + value + '"' - } - return value - }).join('=') -} - function writeCondition (rule, hasArgument = false) { let argument = '' let conditions = rule diff --git a/bundles/org.openhab.ui/web/src/pages/settings/pages/sitemap/__tests__/sitemap-edit_jest.spec.js b/bundles/org.openhab.ui/web/src/pages/settings/pages/sitemap/__tests__/sitemap-edit_jest.spec.js index fa2fb0335..3a00eebf9 100644 --- a/bundles/org.openhab.ui/web/src/pages/settings/pages/sitemap/__tests__/sitemap-edit_jest.spec.js +++ b/bundles/org.openhab.ui/web/src/pages/settings/pages/sitemap/__tests__/sitemap-edit_jest.spec.js @@ -343,13 +343,63 @@ describe('SitemapEdit', () => { '2=Evening', '10=Cinéma', '11=TV', - '3=Bed time', + '"3 time"=Bed time', '4=Night=moon' ]) wrapper.vm.validateWidgets() expect(lastDialogConfig).toBeFalsy() }) + it('validates mappings with release command', async () => { + wrapper.vm.selectWidget([wrapper.vm.sitemap, null]) + await wrapper.vm.$nextTick() + wrapper.vm.addWidget('Switch') + await wrapper.vm.$nextTick() + wrapper.vm.selectWidget([wrapper.vm.sitemap.slots.widgets[0], wrapper.vm.sitemap]) + await wrapper.vm.$nextTick() + localVue.set(wrapper.vm.selectedWidget.config, 'item', 'Item1') + localVue.set(wrapper.vm.selectedWidget.config, 'label', 'Switch Test') + localVue.set(wrapper.vm.selectedWidget.config, 'mappings', [ + 'Morning' + ]) + + // should not validate as the mapping has a syntax error + lastDialogConfig = null + wrapper.vm.validateWidgets() + expect(lastDialogConfig).toBeTruthy() + expect(lastDialogConfig.content).toMatch(/Switch widget Switch Test, syntax error in mappings: Morning/) + + // configure a correct mapping and check that there are no validation errors anymore + lastDialogConfig = null + wrapper.vm.selectWidget([wrapper.vm.sitemap.slots.widgets[0], wrapper.vm.sitemap]) + await wrapper.vm.$nextTick() + localVue.set(wrapper.vm.selectedWidget.config, 'mappings', [ + 'ON="ON"' + ]) + wrapper.vm.validateWidgets() + expect(lastDialogConfig).toBeFalsy() + + // configure mapping for a press and release button and check that there are no validation errors + lastDialogConfig = null + wrapper.vm.selectWidget([wrapper.vm.sitemap.slots.widgets[0], wrapper.vm.sitemap]) + await wrapper.vm.$nextTick() + localVue.set(wrapper.vm.selectedWidget.config, 'mappings', [ + 'ON:OFF="ON"' + ]) + wrapper.vm.validateWidgets() + expect(lastDialogConfig).toBeFalsy() + + // configure mapping for a press and release button with string commands and check that there are no validation errors + lastDialogConfig = null + wrapper.vm.selectWidget([wrapper.vm.sitemap.slots.widgets[0], wrapper.vm.sitemap]) + await wrapper.vm.$nextTick() + localVue.set(wrapper.vm.selectedWidget.config, 'mappings', [ + '"ON command":"OFF command"=ON=icon' + ]) + wrapper.vm.validateWidgets() + expect(lastDialogConfig).toBeFalsy() + }) + it('validates buttons', async () => { wrapper.vm.selectWidget([wrapper.vm.sitemap, null]) await wrapper.vm.$nextTick() diff --git a/bundles/org.openhab.ui/web/src/pages/settings/pages/sitemap/sitemap-edit.vue b/bundles/org.openhab.ui/web/src/pages/settings/pages/sitemap/sitemap-edit.vue index cceb3cc25..593c9326f 100644 --- a/bundles/org.openhab.ui/web/src/pages/settings/pages/sitemap/sitemap-edit.vue +++ b/bundles/org.openhab.ui/web/src/pages/settings/pages/sitemap/sitemap-edit.vue @@ -57,11 +57,14 @@ {column: {width: '10%', type: 'number', min: 1, placeholder: 'col'}}, {command: {}}])" /> - + +
Mappings
+ +
+
Mappings
-
- +
Icon Rules
@@ -505,7 +508,7 @@ export default { let label = widget.config && widget.config.label ? widget.config.label : 'without label' Object.keys(widget.config).filter(attr => ['buttons', 'mappings', 'visibility', 'valuecolor', 'labelcolor', 'iconcolor', 'iconrules'].includes(attr)).forEach(attr => { widget.config[attr].forEach(param => { - if (((attr === 'mappings') && !this.validateMapping(param)) || + if (((attr === 'mappings') && !this.validateMapping(widget.component, param)) || ((attr === 'visibility') && !this.validateRule(param)) || ((['valuecolor', 'labelcolor', 'iconcolor', 'iconrules'].includes(attr)) && !this.validateRule(param, true))) { validationWarnings.push(widget.component + ' widget ' + label + ', syntax error in ' + attr + ': ' + param) @@ -517,7 +520,7 @@ export default { if (!param.column || isNaN(param.column)) { validationWarnings.push(widget.component + ' widget ' + label + ', invalid column configured: ' + param.column) } - if (!this.validateMapping(param.command)) { + if (!this.validateMapping(widget.component, param.command)) { validationWarnings.push(widget.component + ' widget ' + label + ', syntax error in button command: ' + param.command) } } @@ -542,7 +545,11 @@ export default { return true } }, - validateMapping (mapping) { + validateMapping (component, mapping) { + if (component === 'Switch') { + // for Switch widget, also check for releaseCommand + return /^\s*("[^\n"]*"|[^\n="]+)\s*(:\s*("[^\n"]*"|[^\n="]+)\s*)?=\s*("[^\n"]*"|[^\n="]+)\s*(=\s*("[^\n"]*"|[^\n="]+))?$/u.test(mapping) + } return /^\s*("[^\n"]*"|[^\n="]+)\s*=\s*("[^\n"]*"|[^\n="]+)\s*(=\s*("[^\n"]*"|[^\n="]+))?$/u.test(mapping) }, validateRule (rule, hasArgument = false) { @@ -562,10 +569,6 @@ export default { widget.config[key] = widget.config[key].filter(Boolean) if (key === 'buttons') { widget.config[key].sort((value1, value2) => (value1.row - value2.row) || (value1.column - value2.column)) - widget.config[key].forEach(value => this.removeQuotes(value.command)) - } - if (['mappings', 'visibility', 'valuecolor', 'labelcolor', 'iconcolor', 'iconrules'].includes(key)) { - widget.config[key].forEach(this.removeQuotes) } } if (!widget.config[key] && widget.config[key] !== 0) { @@ -577,17 +580,6 @@ export default { widget.slots.widgets.forEach(this.cleanConfig) } }, - removeQuotes (value) { - if (value) { - if (typeof value === 'string') { - value = value.replace(/"|'/g, '') - return - } else if (typeof value === 'number') { - return - } - Object.keys(value).forEach(k => this.removeQuotes(value[k])) - } - }, preProcessSitemapLoad (sitemap) { const processed = JSON.parse(JSON.stringify(sitemap)) if (processed.slots && processed.slots.widgets) {