Sitemap editor: Add press release button support (#2553)
Related to https://github.com/openhab/openhab-core/pull/4183. Depends on https://github.com/openhab/openhab-core/pull/4204. This PR implements press/release button support. Refactor code to allow proper quoting of arguments in mappings and conditions. Quotes are now preserved, and therefore they need to be removed in core at usage (https://github.com/openhab/openhab-core/pull/4204). --------- Signed-off-by: Mark Herwege <mark.herwege@telenet.be>pull/2567/head
parent
02024c62b5
commit
3fa319248b
|
@ -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]) %}
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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"'
|
||||
]
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -57,11 +57,14 @@
|
|||
{column: {width: '10%', type: 'number', min: 1, placeholder: 'col'}},
|
||||
{command: {}}])" />
|
||||
</f7-block>
|
||||
<f7-block v-if="selectedWidget && ['Switch', 'Selection'].indexOf(selectedWidget.component) >= 0">
|
||||
<f7-block v-if="selectedWidget && selectedWidget.component === 'Switch'">
|
||||
<div><f7-block-title>Mappings</f7-block-title></div>
|
||||
<attribute-details :widget="selectedWidget" attribute="mappings" placeholder="command:releaseCommand = label = icon" />
|
||||
</f7-block>
|
||||
<f7-block v-if="selectedWidget && selectedWidget.component === 'Selection'">
|
||||
<div><f7-block-title>Mappings</f7-block-title></div>
|
||||
<attribute-details :widget="selectedWidget" attribute="mappings" placeholder="command = label = icon" />
|
||||
</f7-block>
|
||||
<f7-block v-if="selectedWidget && selectedWidget.component !== 'Sitemap'">
|
||||
</f7-block> <f7-block v-if="selectedWidget && selectedWidget.component !== 'Sitemap'">
|
||||
<div><f7-block-title>Icon Rules</f7-block-title></div>
|
||||
<attribute-details :widget="selectedWidget" attribute="iconrules" placeholder="item_name operator value = icon" />
|
||||
</f7-block>
|
||||
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue