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 34fff7c2af..237119c752 100644
--- a/bundles/org.openhab.ui/web/src/assets/sitemap-lexer.nearley
+++ b/bundles/org.openhab.ui/web/src/assets/sitemap-lexer.nearley
@@ -9,7 +9,7 @@
item: 'item=',
staticIcon: 'staticIcon=',
icon: 'icon=',
- widgetattr: ['url=', 'refresh=', 'service=', 'period=', 'height=', 'minValue=', 'maxValue=', 'step=', 'encoding=', 'yAxisDecimalPattern=', 'inputHint=', 'columns='],
+ widgetattr: ['url=', 'refresh=', 'service=', 'height=', 'minValue=', 'maxValue=', 'step=', 'encoding=', 'yAxisDecimalPattern=', 'inputHint=', 'columns='],
widgetboolattr: ['legend='],
widgetfreqattr: 'sendFrequency=',
widgetfrcitmattr: 'forceasitem=',
@@ -19,6 +19,7 @@
widgetcolorattr: ['labelcolor=', 'valuecolor=', 'iconcolor='],
widgetswitchattr: 'switchSupport',
widgetronlyattr: 'releaseOnly',
+ widgetperiodattr: 'period=',
nlwidget: ['Switch ', 'Selection ', 'Slider ', 'Setpoint ', 'Input ', 'Video ', 'Chart ', 'Webview ', 'Colorpicker ', 'Mapview ', 'Buttongrid ', 'Default '],
lwidget: ['Text ', 'Group ', 'Image ', 'Frame '],
lparen: '(',
@@ -40,10 +41,10 @@
ML_COMMENT: /\/\*[\s\S]*?\*\//,
boolean: /(?:true)|(?:false)/,
identifier: /(?:[A-Za-z_][A-Za-z0-9_]*)|(?:[0-9]+[A-Za-z_][A-Za-z0-9_]*)/,
- number: /-?[0-9]+(?:\.[0-9]*)?/,
comma: ',',
colon: ':',
hyphen: '-',
+ 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']
@@ -116,6 +117,7 @@ WidgetAttr -> %widgetswitchattr
| %widgetfrcitmattr _ WidgetBooleanAttrValue {% (d) => ['forceAsItem', d[2]] %}
| %widgetboolattr _ WidgetBooleanAttrValue {% (d) => [d[0].value, d[2]] %}
| %widgetfreqattr _ WidgetAttrValue {% (d) => ['frequency', d[2]] %}
+ | %widgetperiodattr _ WidgetPeriodAttrValue {% (d) => ['period', d[2]] %}
| %icon _ WidgetIconRulesAttrValue {% (d) => ['iconrules', d[2]] %}
| %icon _ WidgetIconAttrValue {% (d) => [d[0].value, d[2].join("")] %}
| %staticIcon _ WidgetIconAttrValue {% (d) => [d[0].value, d[2].join("")] %}
@@ -134,6 +136,10 @@ WidgetIconAttrValue -> %string
WidgetIconRulesAttrValue -> %lbracket _ IconRules _ %rbracket {% (d) => d[2] %}
WidgetIconName -> %identifier
| WidgetIconName %hyphen %identifier {% (d) => d[0] + "-" + d[2].value %}
+WidgetPeriodAttrValue -> %identifier %hyphen %identifier {% (d) => d[0].value + "-" + d[2].value %}
+ | %hyphen %identifier {% (d) => "-" + d[1].value %}
+ | %identifier {% (d) => d[0].value %}
+ | %string {% (d) => d[0].value %}
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 14ec623e25..883cebe45d 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
@@ -452,4 +452,103 @@ describe('SitemapCode', () => {
}
})
})
+
+ it('parses a chart widget correctly', async () => {
+ expect(wrapper.vm.sitemapDsl).toBeDefined()
+ // simulate updating the sitemap in code
+ const sitemap = [
+ 'sitemap test label="Test" {',
+ ' Chart item=Temperature period=4h',
+ '}',
+ ''
+ ].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: 'Chart',
+ config: {
+ item: 'Temperature',
+ period: '4h'
+ }
+ })
+ })
+
+ it('parses a chart widget with future period correctly', async () => {
+ expect(wrapper.vm.sitemapDsl).toBeDefined()
+ // simulate updating the sitemap in code
+ const sitemap = [
+ 'sitemap test label="Test" {',
+ ' Chart item=Temperature period=-4h',
+ '}',
+ ''
+ ].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: 'Chart',
+ config: {
+ item: 'Temperature',
+ period: '-4h'
+ }
+ })
+ })
+
+ it('parses a chart widget with past and ISO-8601 future period correctly', async () => {
+ expect(wrapper.vm.sitemapDsl).toBeDefined()
+ // simulate updating the sitemap in code
+ const sitemap = [
+ 'sitemap test label="Test" {',
+ ' Chart item=Temperature period=4h-P1DT12H',
+ '}',
+ ''
+ ].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: 'Chart',
+ config: {
+ item: 'Temperature',
+ period: '4h-P1DT12H'
+ }
+ })
+ })
})
diff --git a/bundles/org.openhab.ui/web/src/components/pagedesigner/sitemap/widget-details.vue b/bundles/org.openhab.ui/web/src/components/pagedesigner/sitemap/widget-details.vue
index 584a330e57..5624a9898b 100644
--- a/bundles/org.openhab.ui/web/src/components/pagedesigner/sitemap/widget-details.vue
+++ b/bundles/org.openhab.ui/web/src/components/pagedesigner/sitemap/widget-details.vue
@@ -24,7 +24,8 @@
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 3699d0a832..fa2fb0335c 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
@@ -200,6 +200,22 @@ describe('SitemapEdit', () => {
wrapper.vm.validateWidgets()
expect(lastDialogConfig).toBeFalsy()
+ // configure a future period for the Chart 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, 'period', '-4h')
+ wrapper.vm.validateWidgets()
+ expect(lastDialogConfig).toBeFalsy()
+
+ // configure a combined past and future period for the Chart 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, 'period', '4h-4h')
+ wrapper.vm.validateWidgets()
+ expect(lastDialogConfig).toBeFalsy()
+
// configure an ISO-8601 period for the Chart and check that there are no validation errors
lastDialogConfig = null
wrapper.vm.selectWidget([wrapper.vm.sitemap.slots.widgets[0], wrapper.vm.sitemap])
@@ -207,6 +223,14 @@ describe('SitemapEdit', () => {
localVue.set(wrapper.vm.selectedWidget.config, 'period', 'P10M2W1DT12H30M')
wrapper.vm.validateWidgets()
expect(lastDialogConfig).toBeFalsy()
+
+ // configure a combined past and future ISO-8601 and classic period for the Chart 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, 'period', '4h-P10M2W1DT12H30M')
+ wrapper.vm.validateWidgets()
+ expect(lastDialogConfig).toBeFalsy()
})
it('validates step is positive', async () => {
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 74267d62a8..cceb3cc25a 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
@@ -460,7 +460,7 @@ export default {
}
})
widgetList.filter(widget => widget.component === 'Chart').forEach(widget => {
- if (!(widget.config && widget.config.period && /^P(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(\d+H)?(\d+M)?(\d+S)?)?$|^\d*[YMWDh]$/.test(widget.config.period))) {
+ if (!(widget.config && widget.config.period && /^((P(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(\d+H)?(\d+M)?(\d+S)?)?|\d*[YMWDh])-)?-?(P(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(\d+H)?(\d+M)?(\d+S)?)?|\d*[YMWDh])$/.test(widget.config.period))) {
let label = widget.config && widget.config.label ? widget.config.label : 'without label'
validationWarnings.push(widget.component + ' widget ' + label + ', invalid period configured: ' + widget.config.period)
}