diff --git a/bundles/org.openhab.ui/web/src/assets/items-lexer.nearley b/bundles/org.openhab.ui/web/src/assets/items-lexer.nearley index 887daa2457..7c793c0eef 100644 --- a/bundles/org.openhab.ui/web/src/assets/items-lexer.nearley +++ b/bundles/org.openhab.ui/web/src/assets/items-lexer.nearley @@ -9,7 +9,7 @@ itemtype: ['Group ', 'Number ', 'Switch ', 'Rollershutter ', 'String ', 'Dimmer ', 'Contact ', 'DateTime ', 'Color ', 'Player ', 'Location ', 'Call ', 'Image '], membertype: [':Number', ':Switch', ':Rollershutter', ':String', ':Dimmer', ':Contact', ':DateTime', ':Color', ':Player', ':Location', ':Call', ':Image'], aggfunc: ['AVG', 'SUM', 'MIN', 'MAX', 'OR', 'AND', 'COUNT', 'LATEST', 'EARLIEST', 'EQUALITY'], - identifier: /[A-Za-z0-9_-]+/, + identifier: /(?:[A-Za-z_][A-Za-z0-9_]*)|(?:[0-9]+[A-Za-z_][A-Za-z0-9_]*)/, lparen: '(', rparen: ')', colon: ':', @@ -21,6 +21,8 @@ gt: '>', comma: ',', equals: '=', + colon: ':', + hyphen: '-', NL: { match: /\n/, lineBreaks: true }, }) %} @@ -54,7 +56,7 @@ Type -> %itemtype {% (d) => [d[0].text.trim()] | "Group" %membertype {% (d) => ['Group', d[1].text.substring(1)] %} | "Group" %membertype ":" %aggfunc AggArgs {% (d) => ['Group', d[1].text.substring(1), {name: d[3].text, params: d[4]}] %} | "Group" %membertype ":" %identifier {% (d) => ['Group', 'Number:' + d[3].text] %} - | "Group" %membertype ":" %identifier ":" %aggfunc AggArgs {% (d) => ['Group', 'Number:' + d[3].text, {name: d[5].text, params: d[6]}] %} + | "Group" %membertype ":" %identifier ":" %aggfunc AggArgs {% (d) => ['Group', 'Number:' + d[3].text, {name: d[5].text, params: d[6]}] %} # group function aggregation arguments AggArgs -> null {% (d) => undefined %} | "(" %identifier ")" {% (d) => [d[1].text] %} @@ -69,7 +71,13 @@ Label -> null {% (d) => undefined %} # Icon (category) Icon -> null {% (d) => undefined %} - | __ "<" _ %identifier _ ">" {% (d) => d[3].text %} + | __ "<" _ IconValue _ ">" {% (d) => d[3].join("") %} +IconValue -> %string + | IconName + | %identifier %colon IconName + | %identifier %colon %identifier %colon IconName +IconName -> %identifier + | %identifier %hyphen IconName {% (d) => d[0].value + "-" + d[2] %} # Groups Groups -> null {% (d) => undefined %} @@ -107,14 +115,14 @@ MetadataConfigValue -> %string {% (d) => d[0].value %} | %number {% (d) => parseInt(d[0].value) %} -_ -> null {% () => null %} - | _ %WS {% () => null %} - | _ %NL {% () => null %} +_ -> null {% () => null %} + | _ %WS {% () => null %} + | _ %NL {% () => null %} | _ %comment {% () => null %} -__ -> %WS {% () => null %} - | %NL {% () => null %} +__ -> %WS {% () => null %} + | %NL {% () => null %} | %comment {% () => null %} | __ %WS {% () => null %} | __ %NL {% () => null %} - | __ %comment {% () => null %} + | __ %comment {% () => null %} 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 dd53571024..ad87af4bea 100644 --- a/bundles/org.openhab.ui/web/src/assets/sitemap-lexer.nearley +++ b/bundles/org.openhab.ui/web/src/assets/sitemap-lexer.nearley @@ -33,6 +33,8 @@ gt: '>', equals: '=', comma: ',', + colon: ':', + hyphen: '-', NL: { match: /\n/, lineBreaks: true }, boolean: /(?:true)|(?:false)/, identifier: /(?:[A-Za-z_][A-Za-z0-9_]*)|(?:[0-9]+[A-Za-z_][A-Za-z0-9_]*)/, @@ -99,12 +101,19 @@ WidgetAttr -> %widgetswitchattr | %widgetfrcitmattr WidgetBooleanAttrValue {% (d) => ['forceAsItem', d[1]] %} | %widgetboolattr WidgetBooleanAttrValue {% (d) => [d[0].value, d[1]] %} | %widgetfreqattr WidgetAttrValue {% (d) => ['frequency', d[1]] %} + | %icon WidgetIconAttrValue {% (d) => [d[0].value, d[1].join("")] %} | WidgetAttrName WidgetAttrValue {% (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 | %icon | %widgetattr +WidgetAttrName -> %item | %label | %widgetattr WidgetBooleanAttrValue -> %boolean {% (d) => (d[0].value === 'true') %} | %string {% (d) => (d[0].value === 'true') %} +WidgetIconAttrValue -> %string + | WidgetIconName + | %identifier %colon WidgetIconName + | %identifier %colon %identifier %colon WidgetIconName +WidgetIconName -> %identifier + | %identifier %hyphen WidgetIconName {% (d) => d[0].value + "-" + d[2] %} WidgetAttrValue -> %number {% (d) => { return parseFloat(d[0].value) } %} | %identifier {% (d) => d[0].value %} | %string {% (d) => d[0].value %} 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 bdc587d501..0c69ae7040 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 @@ -102,10 +102,57 @@ describe('SitemapCode', () => { }) }) + it('parses a segmented icon name with hyphens', async () => { + expect(wrapper.vm.sitemapDsl).toBeDefined() + // simulate updating the sitemap in code + const sitemap = [ + 'sitemap test label="Test" {', + ' Default item=Item_Icon icon=iconify:wi:day-sunny-overcast', + ' Default item=Item_Icon_String icon="iconify:wi:day-sunny-overcast"', + '}', + '' + ].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(2) + expect(payload.slots.widgets[0]).toEqual({ + component: 'Default', + config: { + item: 'Item_Icon', + icon: 'iconify:wi:day-sunny-overcast' + } + }) + expect(payload.slots.widgets[1]).toEqual({ + component: 'Default', + config: { + item: 'Item_Icon_String', + icon: 'iconify:wi:day-sunny-overcast' + } + }) + }) + it('parses a mapping code back to a mapping on a component', async () => { expect(wrapper.vm.sitemapDsl).toBeDefined() // simulate updating the sitemap in code - wrapper.vm.updateSitemap('sitemap test label="Test" {\n Selection item=Scene_General mappings=[1=Morning,2="Evening",10="Cinéma",11=TV,3="Bed time",4=Night]\n}\n') + const sitemap = [ + 'sitemap test label="Test" {', + ' Selection item=Scene_General mappings=[1=Morning,2="Evening",10="Cinéma",11=TV,3="Bed time",4=Night]', + '}', + '' + ].join('\n') + wrapper.vm.updateSitemap(sitemap) expect(wrapper.vm.sitemapDsl).toMatch(/^sitemap test label="Test"/) expect(wrapper.vm.parsedSitemap.error).toBeFalsy() diff --git a/bundles/org.openhab.ui/web/src/css/app.styl b/bundles/org.openhab.ui/web/src/css/app.styl index 2f7d7ca505..643a3716a9 100644 --- a/bundles/org.openhab.ui/web/src/css/app.styl +++ b/bundles/org.openhab.ui/web/src/css/app.styl @@ -292,3 +292,6 @@ html --f7-toast-text-color white --f7-button-text-color white --f7-button-border-color white + +.sitemap-validation-dialog + --f7-dialog-width 80% 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 d2ead7e632..758b1588ba 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 @@ -46,10 +46,83 @@ describe('SitemapEdit', () => { expect(wrapper.vm.sitemap.component).toEqual('Sitemap') }) + it('validates frame does not contain frames', async () => { + wrapper.vm.selectWidget([wrapper.vm.sitemap, null]) + await wrapper.vm.$nextTick() + wrapper.vm.addWidget('Frame') + await wrapper.vm.$nextTick() + wrapper.vm.selectWidget([wrapper.vm.sitemap.slots.widgets[0], wrapper.vm.sitemap]) + await wrapper.vm.$nextTick() + wrapper.vm.addWidget('Frame') + await wrapper.vm.$nextTick() + wrapper.vm.selectWidget([wrapper.vm.sitemap.slots.widgets[0].slots.widgets[0], wrapper.vm.sitemap]) + await wrapper.vm.$nextTick() + localVue.set(wrapper.vm.selectedWidget.config, 'label', 'Frame Test') + + // should not validate as the frame contains a frame + lastDialogConfig = null + wrapper.vm.validateWidgets() + expect(lastDialogConfig).toBeTruthy() + expect(lastDialogConfig.content).toMatch(/Frame widget Frame Test, frame not allowed in frame/) + }) + + it('validates frame in text does not contain frames', async () => { + wrapper.vm.selectWidget([wrapper.vm.sitemap, null]) + await wrapper.vm.$nextTick() + wrapper.vm.addWidget('Frame') + await wrapper.vm.$nextTick() + wrapper.vm.selectWidget([wrapper.vm.sitemap.slots.widgets[0], wrapper.vm.sitemap]) + await wrapper.vm.$nextTick() + wrapper.vm.addWidget('Text') + await wrapper.vm.$nextTick() + wrapper.vm.selectWidget([wrapper.vm.sitemap.slots.widgets[0].slots.widgets[0], wrapper.vm.sitemap]) + await wrapper.vm.$nextTick() + wrapper.vm.addWidget('Frame') + await wrapper.vm.$nextTick() + + // should validate, as frame in text in frame is allowed + lastDialogConfig = null + wrapper.vm.validateWidgets() + expect(lastDialogConfig).toBeFalsy() + + // add a frame inside the frame in the text + wrapper.vm.selectWidget([wrapper.vm.sitemap.slots.widgets[0].slots.widgets[0].slots.widgets[0], wrapper.vm.sitemap]) + await wrapper.vm.$nextTick() + wrapper.vm.addWidget('Frame') + await wrapper.vm.$nextTick() + wrapper.vm.selectWidget([wrapper.vm.sitemap.slots.widgets[0].slots.widgets[0].slots.widgets[0].slots.widgets[0], wrapper.vm.sitemap]) + await wrapper.vm.$nextTick() + localVue.set(wrapper.vm.selectedWidget.config, 'label', 'Frame Test') + + // should not validate as the frame contains a frame + lastDialogConfig = null + wrapper.vm.validateWidgets() + expect(lastDialogConfig).toBeTruthy() + expect(lastDialogConfig.content).toMatch(/Frame widget Frame Test, frame not allowed in frame/) + }) + + it('validates only frames or no frames at all', async () => { + wrapper.vm.selectWidget([wrapper.vm.sitemap, null]) + await wrapper.vm.$nextTick() + wrapper.vm.addWidget('Frame') + await wrapper.vm.$nextTick() + wrapper.vm.selectWidget([wrapper.vm.sitemap, null]) + await wrapper.vm.$nextTick() + wrapper.vm.addWidget('Text') + await wrapper.vm.$nextTick() + + // should not validate as mix of frame and text + lastDialogConfig = null + wrapper.vm.validateWidgets() + expect(lastDialogConfig).toBeTruthy() + expect(lastDialogConfig.content).toMatch(/Widget without label, only frames or no frames at all allowed in linkable widget/) + }) + it('validates item name is checked', async () => { wrapper.vm.selectWidget([wrapper.vm.sitemap, null]) await wrapper.vm.$nextTick() wrapper.vm.addWidget('Frame') + await wrapper.vm.$nextTick() wrapper.vm.selectWidget([wrapper.vm.sitemap.slots.widgets[0], wrapper.vm.sitemap]) await wrapper.vm.$nextTick() wrapper.vm.addWidget('Switch') @@ -94,7 +167,7 @@ describe('SitemapEdit', () => { expect(lastDialogConfig).toBeFalsy() }) - it('validates period is checked', async () => { + it('validates period is configured and valid', async () => { wrapper.vm.selectWidget([wrapper.vm.sitemap, null]) await wrapper.vm.$nextTick() wrapper.vm.addWidget('Chart') @@ -108,7 +181,16 @@ describe('SitemapEdit', () => { lastDialogConfig = null wrapper.vm.validateWidgets() expect(lastDialogConfig).toBeTruthy() - expect(lastDialogConfig.content).toMatch(/Chart widget Chart Test, no period configured/) + expect(lastDialogConfig.content).toMatch(/Chart widget Chart Test, invalid period configured: undefined/) + + // configure an invalid period for the Chart + lastDialogConfig = null + wrapper.vm.selectWidget([wrapper.vm.sitemap.slots.widgets[0], wrapper.vm.sitemap]) + await wrapper.vm.$nextTick() + localVue.set(wrapper.vm.selectedWidget.config, 'period', '5h') + wrapper.vm.validateWidgets() + expect(lastDialogConfig).toBeTruthy() + expect(lastDialogConfig.content).toMatch(/Chart widget Chart Test, invalid period configured: 5h/) // configure a period for the Chart and check that there are no validation errors anymore lastDialogConfig = null @@ -119,6 +201,88 @@ describe('SitemapEdit', () => { expect(lastDialogConfig).toBeFalsy() }) + it('validates step is positive', async () => { + wrapper.vm.selectWidget([wrapper.vm.sitemap, null]) + await wrapper.vm.$nextTick() + wrapper.vm.addWidget('Slider') + 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', 'Slider Test') + + // no step, should validate + lastDialogConfig = null + wrapper.vm.validateWidgets() + expect(lastDialogConfig).toBeFalsy() + + // configure a negative step, should not validate + wrapper.vm.selectWidget([wrapper.vm.sitemap.slots.widgets[0], wrapper.vm.sitemap]) + await wrapper.vm.$nextTick() + localVue.set(wrapper.vm.selectedWidget.config, 'step', -1) + lastDialogConfig = null + wrapper.vm.validateWidgets() + expect(lastDialogConfig).toBeTruthy() + expect(lastDialogConfig.content).toMatch(/Slider widget Slider Test, step size cannot be 0 or negative: -1/) + + // configure a 0 step, should not validate + wrapper.vm.selectWidget([wrapper.vm.sitemap.slots.widgets[0], wrapper.vm.sitemap]) + await wrapper.vm.$nextTick() + localVue.set(wrapper.vm.selectedWidget.config, 'step', 0) + lastDialogConfig = null + wrapper.vm.validateWidgets() + expect(lastDialogConfig).toBeTruthy() + expect(lastDialogConfig.content).toMatch(/Slider widget Slider Test, step size cannot be 0 or negative: 0/) + + // configure a positive step, should validate + wrapper.vm.selectWidget([wrapper.vm.sitemap.slots.widgets[0], wrapper.vm.sitemap]) + await wrapper.vm.$nextTick() + localVue.set(wrapper.vm.selectedWidget.config, 'step', 5) + lastDialogConfig = null + wrapper.vm.validateWidgets() + expect(lastDialogConfig).toBeFalsy() + }) + + it('validates minValue less than or equal maxValue', async () => { + wrapper.vm.selectWidget([wrapper.vm.sitemap, null]) + await wrapper.vm.$nextTick() + wrapper.vm.addWidget('Setpoint') + 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', 'Setpoint Test') + + // no minValue or maxValue, should validate + lastDialogConfig = null + wrapper.vm.validateWidgets() + expect(lastDialogConfig).toBeFalsy() + + // configure a minValue more than maxValue, should not validate + lastDialogConfig = null + localVue.set(wrapper.vm.selectedWidget.config, 'minValue', 10) + localVue.set(wrapper.vm.selectedWidget.config, 'maxValue', 5) + wrapper.vm.validateWidgets() + expect(lastDialogConfig).toBeTruthy() + expect(lastDialogConfig.content).toMatch(/Setpoint widget Setpoint Test, minValue must be less than or equal maxValue: 10 > 5/) + + // configure a minValue equal to maxValue, should validate + lastDialogConfig = null + wrapper.vm.selectWidget([wrapper.vm.sitemap.slots.widgets[0], wrapper.vm.sitemap]) + await wrapper.vm.$nextTick() + localVue.set(wrapper.vm.selectedWidget.config, 'minValue', 5) + wrapper.vm.validateWidgets() + expect(lastDialogConfig).toBeFalsy() + + // configure a minValue less to maxValue, should validate + lastDialogConfig = null + wrapper.vm.selectWidget([wrapper.vm.sitemap.slots.widgets[0], wrapper.vm.sitemap]) + await wrapper.vm.$nextTick() + localVue.set(wrapper.vm.selectedWidget.config, 'minValue', 1) + wrapper.vm.validateWidgets() + expect(lastDialogConfig).toBeFalsy() + }) + it('validates mappings', 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 45944032e6..dd295c0003 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 @@ -82,7 +82,7 @@ - + {{ widgetType.type }} @@ -264,6 +264,24 @@ export default { if (!this.selectedWidget) return false if (this.linkableWidgetTypes.indexOf(this.selectedWidget.component) < 0) return false return true + }, + addableWidgetTypes () { + if (!this.selectedWidget) return + // No frames in frame + if (this.selectedWidget.component === 'Frame') return this.widgetTypes.filter(w => w.type !== 'Frame') + // Linkable widget types only contain frames or none at all + if (this.linkableWidgetTypes.includes(this.selectedWidget.component)) { + if (this.selectedWidget.slots && this.selectedWidget.slots.widgets && this.selectedWidget.slots.widgets.length > 0) { + if (this.selectedWidget.slots.widgets.find(w => w.component === 'Frame')) { + return this.widgetTypes.filter(w => w.type === 'Frame') + } else { + return this.widgetTypes.filter(w => w.type !== 'Frame') + } + } else { + return this.widgetTypes + } + } + return this.widgetTypes } }, watch: { @@ -362,6 +380,7 @@ export default { }, validateWidgets (stay) { if (this.sitemap.slots && Array.isArray(this.sitemap.slots.widgets)) { + let validationWarnings = [] const widgetList = this.sitemap.slots.widgets.reduce(function iter (widgets, widget) { widgets.push(widget) if (widget.slots && Array.isArray(widget.slots.widgets)) { @@ -369,17 +388,32 @@ export default { } return widgets }, []) - let validationWarnings = [] - widgetList.filter(widget => this.widgetTypesRequiringItem.includes(widget.component)).forEach(widget => { - if (!(widget.config && widget.config.item)) { + let isFrame = [false] + let siblingIsFrame = [undefined] + this.sitemap.slots.widgets.forEach(function iter (widget) { + if (isFrame[isFrame.length - 1] && widget.component === 'Frame') { let label = widget.config && widget.config.label ? widget.config.label : 'without label' - validationWarnings.push(widget.component + ' widget ' + label + ', no item configured') + validationWarnings.push('Frame widget ' + label + ', frame not allowed in frame') + } + if (siblingIsFrame[siblingIsFrame.length - 1] !== undefined) { + if ((siblingIsFrame[siblingIsFrame.length - 1] && (widget.component !== 'Frame')) || (!siblingIsFrame[siblingIsFrame.length - 1] && (widget.component === 'Frame'))) { + let label = widget.config && widget.config.label ? widget.config.label : 'without label' + validationWarnings.push('Widget ' + label + ', only frames or no frames at all allowed in linkable widget') + } + } + siblingIsFrame.push(siblingIsFrame.pop() || widget.component === 'Frame') + if (widget.slots && Array.isArray(widget.slots.widgets)) { + isFrame.push(widget.component === 'Frame') + siblingIsFrame.push(undefined) + widget.slots.widgets.forEach(iter) + isFrame.pop() + siblingIsFrame.pop() } }) - widgetList.filter(widget => widget.component === 'List').forEach(widget => { - if (!(widget.config && widget.config.separator)) { + widgetList.filter(widget => this.widgetTypesRequiringItem.includes(widget.component)).forEach(widget => { + if (!(widget.config && widget.config.item)) { let label = widget.config && widget.config.label ? widget.config.label : 'without label' - validationWarnings.push(widget.component + ' widget ' + label + ', no separator configured') + validationWarnings.push(widget.component + ' widget ' + label + ', no item configured') } }) widgetList.filter(widget => widget.component === 'Video' || widget.component === 'Webview').forEach(widget => { @@ -389,17 +423,27 @@ export default { } }) widgetList.filter(widget => widget.component === 'Chart').forEach(widget => { - if (!(widget.config && widget.config.period)) { + if (!(widget.config && widget.config.period && ['h', '4h', '8h', '12h', 'D', '2D', '3D', 'W', '2W', 'M', '2M', '3M', 'Y'].includes(widget.config.period))) { let label = widget.config && widget.config.label ? widget.config.label : 'without label' - validationWarnings.push(widget.component + ' widget ' + label + ', no period configured') + validationWarnings.push(widget.component + ' widget ' + label + ', invalid period configured: ' + widget.config.period) } }) widgetList.filter(widget => widget.component === 'Input').forEach(widget => { - if (!(widget.config && widget.config.inputHint && ['text', 'number', 'date', 'time', 'datetime'].includes(widget.config.inputHint))) { + if (widget.config && widget.config.inputHint && !['text', 'number', 'date', 'time', 'datetime'].includes(widget.config.inputHint)) { let label = widget.config && widget.config.label ? widget.config.label : 'without label' validationWarnings.push(widget.component + ' widget ' + label + ', invalid inputHint configured: ' + widget.config.inputHint) } }) + widgetList.filter(widget => widget.component === 'Slider' || widget.component === 'Setpoint').forEach(widget => { + if (widget.config && (widget.config.step !== undefined) && (widget.config.step <= 0)) { + let label = widget.config && widget.config.label ? widget.config.label : 'without label' + validationWarnings.push(widget.component + ' widget ' + label + ', step size cannot be 0 or negative: ' + widget.config.step) + } + if (widget.config && (widget.config.minValue !== undefined) && (widget.config.maxValue !== undefined) && (widget.config.minValue > widget.config.maxValue)) { + let label = widget.config && widget.config.label ? widget.config.label : 'without label' + validationWarnings.push(widget.component + ' widget ' + label + ', minValue must be less than or equal maxValue: ' + widget.config.minValue + ' > ' + widget.config.maxValue) + } + }) widgetList.forEach(widget => { if (widget.config) { Object.keys(widget.config).filter(attr => ['mappings', 'visibility', 'valuecolor', 'labelcolor', 'iconcolor'].includes(attr)).forEach(attr => { @@ -416,9 +460,10 @@ export default { }) if (validationWarnings.length > 0) { this.$f7.dialog.create({ + cssClass: 'sitemap-validation-dialog', title: 'Validation errors', text: 'Sitemap definition has validation errors:', - content: '
  • ' + validationWarnings.join('
  • ') + '
', + content: '
  • ' + validationWarnings.join('
  • ') + '
', buttons: [ { text: 'Cancel', color: 'gray', close: true }, { text: 'Save Anyway', color: 'red', close: true, onClick: () => this.save(stay, true) }