Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sitemap Editor: Buttongrid support #2193

Merged
merged 7 commits into from
Dec 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions bundles/org.openhab.ui/web/src/assets/sitemap-lexer.nearley
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@
item: 'item=',
staticIcon: 'staticIcon=',
icon: 'icon=',
widgetattr: ['url=', 'refresh=', 'service=', 'period=', 'height=', 'minValue=', 'maxValue=', 'step=', 'encoding=', 'yAxisDecimalPattern=', 'inputHint='],
widgetattr: ['url=', 'refresh=', 'service=', 'period=', 'height=', 'minValue=', 'maxValue=', 'step=', 'encoding=', 'yAxisDecimalPattern=', 'inputHint=', 'columns='],
widgetboolattr: ['legend='],
widgetfreqattr: 'sendFrequency=',
widgetfrcitmattr: 'forceasitem=',
widgetmapattr: 'mappings=',
widgetbuttonattr: 'buttons=',
widgetvisiattr: 'visibility=',
widgetcolorattr: ['labelcolor=', 'valuecolor=', 'iconcolor='],
widgetswitchattr: 'switchSupport',
nlwidget: ['Switch ', 'Selection ', 'Slider ', 'Setpoint ', 'Input ', 'Video ', 'Chart ', 'Webview ', 'Colorpicker ', 'Mapview ', 'Default '],
nlwidget: ['Switch ', 'Selection ', 'Slider ', 'Setpoint ', 'Input ', 'Video ', 'Chart ', 'Webview ', 'Colorpicker ', 'Mapview ', 'Buttongrid ', 'Default '],
lwidget: ['Text ', 'Group ', 'Image ', 'Frame '],
lparen: '(',
rparen: ')',
Expand Down Expand Up @@ -118,6 +119,7 @@ WidgetAttr -> %widgetswitchattr
| %staticIcon _ WidgetIconAttrValue {% (d) => [d[0].value, d[2].join("")] %}
| WidgetAttrName _ WidgetAttrValue {% (d) => [d[0][0].value, d[2]] %}
| WidgetMappingsAttrName WidgetMappingsAttrValue {% (d) => [d[0][0].value, d[1]] %}
| WidgetButtonsAttrName WidgetButtonsAttrValue {% (d) => [d[0][0].value, d[1]] %}
| WidgetVisibilityAttrName WidgetVisibilityAttrValue {% (d) => [d[0][0].value, d[1]] %}
| WidgetColorAttrName WidgetColorAttrValue {% (d) => [d[0][0].value, d[1]] %}
WidgetAttrName -> %item | %label | %widgetattr
Expand All @@ -135,17 +137,26 @@ WidgetAttrValue -> %number
| %string {% (d) => d[0].value %}
WidgetMappingsAttrName -> %widgetmapattr
WidgetMappingsAttrValue -> %lbracket _ Mappings _ %rbracket {% (d) => d[2] %}
WidgetButtonsAttrName -> %widgetbuttonattr
WidgetButtonsAttrValue -> %lbracket _ Buttons _ %rbracket {% (d) => d[2] %}
WidgetVisibilityAttrName -> %widgetvisiattr
WidgetVisibilityAttrValue -> %lbracket _ Visibilities _ %rbracket {% (d) => d[2] %}
WidgetColorAttrName -> %widgetcolorattr
WidgetColorAttrValue -> %lbracket _ Colors _ %rbracket {% (d) => d[2] %}

Mappings -> Mapping {% (d) => [d[0]] %}
| Mappings _ %comma _ Mapping {% (d) => d[0].concat([d[4]]) %}
Mapping -> MappingCommand _ %equals _ MappingLabel {% (d) => d[0][0].value + '=' + d[4][0].value %}
| MappingCommand _ %equals _ MappingLabel _ %equals _ WidgetIconAttrValue {% (d) => d[0][0].value + '=' + d[4][0].value + '=' + d[8].join("") %}
MappingCommand -> %number | %identifier | %string
MappingLabel -> %number | %identifier | %string
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("") %}

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("") %}

Command -> %number | %identifier | %string
Label -> %number | %identifier | %string

Visibilities -> Conditions {% (d) => d[0] %}
| Visibilities _ %comma _ Conditions {% (d) => d[0].concat(d[4]) %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,26 @@ describe('dslUtil', () => {
]
})
const sitemap = dslUtil.toDsl(component).split('\n')
expect(sitemap[1]).toEqual(' Selection item=Scene_General mappings=[1=Morning,2=Evening,10="Cinéma",11=TV,3="Bed time",4=Night=moon]')
expect(sitemap[1]).toEqual(' Selection item=Scene_General mappings=[1=Morning, 2=Evening, 10="Cinéma", 11=TV, 3="Bed time", 4=Night=moon]')
})

it('renders a Buttongrid widget correctly', () => {
const component = createSitemapComponent('test', 'Test')
const widget = {
}
addWidget(component, 'Buttongrid', {
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: 2, column: 1, command: '11=TV' },
{ row: 2, column: 2, command: '3=Bed time' },
{ row: 2, column: 3, command: '4=Night=moon' }
]
})
const sitemap = dslUtil.toDsl(component).split('\n')
expect(sitemap[1]).toEqual(' Buttongrid item=Scene_General buttons=[1:1:1=Morning, 1:2:2=Evening, 1:3:10="Cinéma", 2:1:11=TV, 2:2:3="Bed time", 2:3:4=Night=moon]')
})

it('renders a widget with mappings and string keys correctly', () => {
Expand All @@ -136,7 +155,7 @@ describe('dslUtil', () => {
]
})
const sitemap = dslUtil.toDsl(component).split('\n')
expect(sitemap[1]).toEqual(' Selection item=Echos mappings=[EchoDot1="Echo 1",EchoDot2="Echo 2","EchoDot1,EchoDot2"=Alle]')
expect(sitemap[1]).toEqual(' Selection item=Echos mappings=[EchoDot1="Echo 1", EchoDot2="Echo 2", "EchoDot1,EchoDot2"=Alle]')
})

it('renders a widget with 0 value parameter correctly', () => {
Expand Down Expand Up @@ -166,7 +185,7 @@ describe('dslUtil', () => {
]
})
const sitemap = dslUtil.toDsl(component).split('\n')
expect(sitemap[1]).toEqual(' Text item=Test visibility=[Battery<30,Battery>50,Battery_Level>=20]')
expect(sitemap[1]).toEqual(' Text item=Test visibility=[Battery<30, Battery>50, Battery_Level>=20]')
})

it('renders widget with visibility and text condition correctly', () => {
Expand All @@ -181,7 +200,7 @@ describe('dslUtil', () => {
]
})
const sitemap = dslUtil.toDsl(component).split('\n')
expect(sitemap[1]).toEqual(' Switch item=Test visibility=[Day_Time=="Morning Time",Temperature>19]')
expect(sitemap[1]).toEqual(' Switch item=Test visibility=[Day_Time=="Morning Time", Temperature>19]')
})

it('renders widget with valuecolor correctly', () => {
Expand All @@ -199,7 +218,7 @@ describe('dslUtil', () => {
]
})
const sitemap = dslUtil.toDsl(component).split('\n')
expect(sitemap[1]).toEqual(' Text item=Temperature valuecolor=[Last_Update==Uninitialized="gray",>=25="orange",==15="green",0="white","blue"]')
expect(sitemap[1]).toEqual(' Text item=Temperature valuecolor=[Last_Update==Uninitialized="gray", >=25="orange", ==15="green", 0="white", "blue"]')
})

it('renders widget with valuecolor and text condition correctly', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,47 @@ describe('SitemapCode', () => {
})
})

it('parses a Buttongrid component correctly', async () => {
expect(wrapper.vm.sitemapDsl).toBeDefined()
// simulate updating the sitemap in code
const sitemap = [
'sitemap test label="Test" {',
' Buttongrid item=Scene_General buttons=[1:1:1=Morning, 1:2:2="Evening", 1:3:10="Cinéma",',
' 2:1:11=TV, 2:2:3="Bed time", 2:3:4=Night=moon]',
'}',
''
].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: 'Buttongrid',
config: {
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: 2, column: 1, command: '11=TV' },
{ row: 2, column: 2, command: '3=Bed time' },
{ row: 2, column: 3, command: '4=Night=moon' }
]
}
})
})

it('parses a mapping code back to a mapping on a component', async () => {
expect(wrapper.vm.sitemapDsl).toBeDefined()
// simulate updating the sitemap in code
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,19 @@
<f7-card v-if="widget">
<f7-card-content v-if="attributes.length">
<f7-list inline-labels sortable sortable-opposite sortable-enabled @sortable:sort="onSort">
<f7-list-input v-for="(attr, idx) in attributes" :key="attr.key"
type="text" :placeholder="placeholder" :value="attr.value" @change="updateAttribute(idx, $event)" clear-button />
<f7-list-item v-for="(attr, idx) in attributes" :key="attr.key">
<f7-input v-if="!fields" type="text" :placeholder="placeholder" :value="attr.value" @change="updateAttribute($event, idx, attr)" />
<f7-input v-for="(field, fieldidx) in fieldDefs" :key="JSON.stringify(field)"
:style="fieldStyle(field, fieldidx)"
:inputStyle="inputFieldStyle(field, fieldidx)"
:type="fieldProp(field, 'type')"
:min="fieldProp(field, 'min')"
:max="fieldProp(field, 'max')"
:placeholder="fieldProp(field, 'placeholder')"
:value="attr.value[Object.keys(field)[0]]"
validate @change="updateAttribute($event, idx, attr, Object.keys(field)[0])" />
<f7-button text="" icon-material="clear" small @click="removeAttribute(idx)" />
</f7-list-item>
</f7-list>
</f7-card-content>
<f7-card-footer key="item-card-buttons-edit-mode" v-if="widget.component !== 'Sitemap'">
Expand All @@ -14,25 +25,77 @@
</f7-card>
</template>

<style lang="stylus">
.button
padding-left 5px
padding-right 0px
.input
width inherit
</style>

<script>
export default {
props: ['widget', 'attribute', 'placeholder'],
props: ['widget', 'attribute', 'placeholder', 'fields'],
data () {
return {
fieldDefaults: {
type: 'text'
}
}
},
computed: {
fieldDefs () {
return this.fields ? JSON.parse(this.fields) : []
},
attributes () {
if (this.widget && this.widget.config && this.widget.config[this.attribute]) {
return this.widget.config[this.attribute].map((attr, idx) => ({ key: idx + ': ' + attr, value: attr }))
return this.widget.config[this.attribute].map((attr, idx) => ({ key: idx + ': ' + JSON.stringify(attr), value: attr }))
}
return []
}
},
methods: {
updateAttribute (idx, $event) {
const value = $event.target.value
fieldProp (field, prop) {
const fieldProps = field[Object.keys(field)[0]]
if (fieldProps[prop] !== undefined) {
return fieldProps[prop]
}
if (prop === 'placeholder') {
return this.placeholder
}
return this.fieldDefaults[prop]
},
fieldStyle (field, fieldidx) {
let style = {}
if (this.fieldProp(field, 'width') !== undefined) {
style.width = this.fieldProp(field, 'width')
}
if (fieldidx > 0) {
style.paddingLeft = '5px'
}
return style
},
inputFieldStyle (field, fieldidx) {
let style = {}
if (this.fieldProp(field, 'type') === 'number') {
style.textAlign = 'end'
}
return style
},
updateAttribute ($event, idx, attr, field) {
let value = $event.target.value
if (!value) {
this.widget.config[this.attribute].splice(idx, 1)
} else {
this.$set(this.widget.config[this.attribute], idx, value)
this.removeAttribute(idx)
return
}
if (field) {
value = attr.value ? attr.value : {}
value[field] = $event.target.value
}
this.$set(this.widget.config[this.attribute], idx, value)
},
removeAttribute (idx) {
this.widget.config[this.attribute].splice(idx, 1)
},
addAttribute () {
if (this.widget && this.widget.config && this.widget.config[this.attribute]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,21 @@ function writeWidget (widget, indent) {
if (key === 'item' || key === 'period' || key === 'legend' || Number.isFinite(widget.config[key])) {
dsl += widget.config[key]
} else if (key === 'mappings') {
dsl += '['
dsl += widget.config[key].filter(Boolean).map(mapping => {
return mapping.split('=').map(value => {
if (/^.*\W.*$/.test(value) && /^[^"'].*[^"']$/.test(value)) {
return '"' + value + '"'
}
return value
}).join('=')
}).join(',')
dsl += ']'
dsl += '[' + widget.config[key].filter(Boolean).map(mapping => {
return writeCommand(mapping)
}).join(', ') + ']'
} else if (key === 'buttons') {
dsl += '[' + widget.config[key].filter(Boolean).map(button => {
return button.row + ':' + button.column + ':' + writeCommand(button.command)
}).join(', ') + ']'
} else if (key === 'visibility') {
dsl += '[' + writeConditions(widget.config[key]) + ']'
dsl += '[' + widget.config[key].filter(Boolean).map(rule => {
return writeCondition(rule)
}).join(', ') + ']'
} else if (['valuecolor', 'labelcolor', 'iconcolor', 'iconrules'].includes(key)) {
dsl += '[' + writeConditions(widget.config[key], true) + ']'
dsl += '[' + widget.config[key].filter(Boolean).map(rule => {
return writeCondition(rule, true)
}).join(', ') + ']'
} else {
dsl += '"' + widget.config[key] + '"'
}
Expand All @@ -58,28 +59,35 @@ function writeWidget (widget, indent) {
return dsl
}

function writeConditions (value, hasArgument = false) {
return value.filter(Boolean).map(rule => {
let argument = ''
let conditions = rule
if (hasArgument) {
let index = rule.lastIndexOf('=') + 1
argument = rule.substring(index).trim()
if (!/^(".*")|('.*')$/.test(argument)) {
argument = '"' + argument + '"'
}
argument = (index > 0 ? '=' + argument : argument)
conditions = rule.substring(0, index - 1)
function writeCommand (command) {
return command.split('=').map(value => {
if (/^.*\W.*$/.test(value) && /^[^"'].*[^"']$/.test(value)) {
return '"' + value + '"'
}
return conditions.split(' AND ').map(condition => {
let index = Math.max(condition.lastIndexOf('='), condition.lastIndexOf('>'), condition.lastIndexOf('<')) + 1
let conditionValue = condition.substring(index).trim()
if (/^.*\W.*$/.test(conditionValue) && /^[^"'].*[^"']$/.test(conditionValue)) {
conditionValue = '"' + conditionValue + '"'
}
return condition.substring(0, index) + conditionValue
}).join(' AND ') + argument
}).join(',')
return value
}).join('=')
}

function writeCondition (rule, hasArgument = false) {
let argument = ''
let conditions = rule
if (hasArgument) {
let index = rule.lastIndexOf('=') + 1
argument = rule.substring(index).trim()
if (!/^(".*")|('.*')$/.test(argument)) {
argument = '"' + argument + '"'
}
argument = (index > 0 ? '=' + argument : argument)
conditions = rule.substring(0, index - 1)
}
return conditions.split(' AND ').map(condition => {
let index = Math.max(condition.lastIndexOf('='), condition.lastIndexOf('>'), condition.lastIndexOf('<')) + 1
let conditionValue = condition.substring(index).trim()
if (/^.*\W.*$/.test(conditionValue) && /^[^"'].*[^"']$/.test(conditionValue)) {
conditionValue = '"' + conditionValue + '"'
}
return condition.substring(0, index) + conditionValue
}).join(' AND ') + argument
}

export default {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export default {
return 'f7:drop'
case 'Mapview':
return 'f7:map'
case 'Buttongrid':
return 'f7:square_grid_3x2'
case 'Default':
return 'f7:rectangle'
case 'Text':
Expand Down
Loading