diff --git a/cypress/fixtures/flows/dashboard-forms.json b/cypress/fixtures/flows/dashboard-forms.json index 5c9425769..5086c2545 100644 --- a/cypress/fixtures/flows/dashboard-forms.json +++ b/cypress/fixtures/flows/dashboard-forms.json @@ -15,7 +15,7 @@ "info": "", "env": [] }, - { + { "id": "dashboard-ui-base", "type": "ui-base", "name": "UI Name", @@ -128,7 +128,7 @@ "label": "", "order": 1, "width": "3", - "height": "1", + "height": "2", "options": [ { "label": "Name", diff --git a/cypress/tests/widgets/form.spec.js b/cypress/tests/widgets/form.spec.js index 6b7f62741..4c5a918ec 100644 --- a/cypress/tests/widgets/form.spec.js +++ b/cypress/tests/widgets/form.spec.js @@ -36,9 +36,10 @@ describe('Node-RED Dashboard 2.0 - Forms', () => { }) it('permits users to set default values via msg.payload', () => { // check that the form is empty - cy.get('#nrdb-ui-widget-dashboard-ui-form-dynamic').find('[data-form="form-row-name0"]').find('input[type="text"]').should('have.value', '') - cy.clickAndWait(cy.get('button').contains('Set Defaults')) - cy.get('#nrdb-ui-widget-dashboard-ui-form-dynamic').find('[data-form="form-row-name0"]').find('input[type="text"]').should('have.value', 'Overridden Default Name') + cy.get('#nrdb-ui-widget-dashboard-ui-form-dynamic').find('[data-form="form-row-name0"] input[type="text"]').should('have.value', '') + cy.clickAndWait(cy.get('button').contains('Set Defaults'), 200) + cy.clickAndWait(cy.get('button').contains('Set Defaults'), 200) + cy.get('#nrdb-ui-widget-dashboard-ui-form-dynamic').find('[data-form="form-row-name0"] input[type="text"]').should('have.value', 'Overridden Default Name') }) it('can have their content defined by msg.ui_update.options', () => { @@ -52,17 +53,17 @@ describe('Node-RED Dashboard 2.0 - Forms', () => { cy.get('#nrdb-ui-widget-dashboard-ui-form-dynamic').find('[data-form="form-row-dob"]').should('not.exist') cy.get('#nrdb-ui-widget-dashboard-ui-form-dynamic').find('[data-form="form-row-tob"]').should('not.exist') - cy.clickAndWait(cy.get('button').contains('Override Form Options')) + cy.clickAndWait(cy.get('button').contains('Override Form Options'), 200) - cy.get('#nrdb-ui-widget-dashboard-ui-form-dynamic').find('[data-form="form-row-name"]').find('input[type="text"]').should('exist') - cy.get('#nrdb-ui-widget-dashboard-ui-form-dynamic').find('[data-form="form-row-multiline"]').find('textarea').should('exist') - cy.get('#nrdb-ui-widget-dashboard-ui-form-dynamic').find('[data-form="form-row-password"]').find('input[type="password"]').should('exist') - cy.get('#nrdb-ui-widget-dashboard-ui-form-dynamic').find('[data-form="form-row-email"]').find('input[type="email"]').should('exist') - cy.get('#nrdb-ui-widget-dashboard-ui-form-dynamic').find('[data-form="form-row-age"]').find('input[type="number"]').should('exist') - cy.get('#nrdb-ui-widget-dashboard-ui-form-dynamic').find('[data-form="form-row-newsletter"]').find('input[type="checkbox"]').should('exist') - cy.get('#nrdb-ui-widget-dashboard-ui-form-dynamic').find('[data-form="form-row-notifications"]').find('input[type="checkbox"]').should('exist') - cy.get('#nrdb-ui-widget-dashboard-ui-form-dynamic').find('[data-form="form-row-dob"]').find('input[type="date"]').should('exist') - cy.get('#nrdb-ui-widget-dashboard-ui-form-dynamic').find('[data-form="form-row-tob"]').find('input[type="time"]').should('exist') + cy.get('#nrdb-ui-widget-dashboard-ui-form-dynamic').find('[data-form="form-row-name"] input[type="text"]').should('exist') + cy.get('#nrdb-ui-widget-dashboard-ui-form-dynamic').find('[data-form="form-row-multiline"] textarea').should('exist') + cy.get('#nrdb-ui-widget-dashboard-ui-form-dynamic').find('[data-form="form-row-password"] input[type="password"]').should('exist') + cy.get('#nrdb-ui-widget-dashboard-ui-form-dynamic').find('[data-form="form-row-email"] input[type="email"]').should('exist') + cy.get('#nrdb-ui-widget-dashboard-ui-form-dynamic').find('[data-form="form-row-age"] input[type="number"]').should('exist') + cy.get('#nrdb-ui-widget-dashboard-ui-form-dynamic').find('[data-form="form-row-newsletter"] input[type="checkbox"]').should('exist') + cy.get('#nrdb-ui-widget-dashboard-ui-form-dynamic').find('[data-form="form-row-notifications"] input[type="checkbox"]').should('exist') + cy.get('#nrdb-ui-widget-dashboard-ui-form-dynamic').find('[data-form="form-row-dob"] input[type="date"]').should('exist') + cy.get('#nrdb-ui-widget-dashboard-ui-form-dynamic').find('[data-form="form-row-tob"] input[type="time"]').should('exist') }) const payloadElId = '#nrdb-ui-widget-74cbdedad6183c40' @@ -71,13 +72,13 @@ describe('Node-RED Dashboard 2.0 - Forms', () => { it('Delivers topic from msg.topic', () => { const formElId = '#nrdb-ui-widget-3cd8df20415c4c04' // wait for the input to be actionable - cy.get(formElId).find('[data-form="form-row-name1"]').find('input[type="text"]').should('not.be.disabled') + cy.get(formElId).find('[data-form="form-row-name1"] input[type="text"]').should('not.be.disabled') // enter a value into the text input field - cy.get(formElId).find('[data-form="form-row-name1"]').find('input[type="text"]').clear() - cy.get(formElId).find('[data-form="form-row-name1"]').find('input[type="text"]').should('have.value', '') - cy.get(formElId).find('[data-form="form-row-name1"]').find('input[type="text"]').type('payload for msg.topic test') + cy.get(formElId).find('[data-form="form-row-name1"] input[type="text"]').clear() + cy.get(formElId).find('[data-form="form-row-name1"] input[type="text"]').should('have.value', '') + cy.get(formElId).find('[data-form="form-row-name1"] input[type="text"]').type('payload for msg.topic test') // submit the form - cy.clickAndWait(cy.get(formElId).find('[data-action="form-submit"]')) + cy.clickAndWait(cy.get(formElId).find('[data-action="form-submit"]'), 200) // check the output for the topic cy.get(payloadElId).find('.nrdb-ui-text-value').contains('{"name1":"payload for msg.topic test"}') cy.get(topicElId).find('.nrdb-ui-text-value').contains('topic from msg.topic') @@ -86,13 +87,14 @@ describe('Node-RED Dashboard 2.0 - Forms', () => { it('Delivers topic from flow.f1', () => { const formElId = '#nrdb-ui-widget-ddb5a30c677e5e0b' // wait for the input to be actionable - cy.get(formElId).find('[data-form="form-row-name2"]').find('input[type="text"]').should('not.be.disabled') + cy.get(formElId).find('[data-form="form-row-name2"] input[type="text"]').should('not.be.disabled') // enter a value into the text input field - cy.get(formElId).find('[data-form="form-row-name2"]').find('input[type="text"]').clear() - cy.get(formElId).find('[data-form="form-row-name2"]').find('input[type="text"]').should('have.value', '') - cy.get(formElId).find('[data-form="form-row-name2"]').find('input[type="text"]').type('flow.f1 test') + cy.get(formElId).find('[data-form="form-row-name2"] input[type="text"]').clear() + cy.get(formElId).find('[data-form="form-row-name2"] input[type="text"]').should('have.value', '') + cy.get(formElId).find('[data-form="form-row-name2"] input[type="text"]').should('not.be.disabled') + cy.get(formElId).find('[data-form="form-row-name2"] input[type="text"]').type('flow.f1 test') // submit the form - cy.clickAndWait(cy.get(formElId).find('[data-action="form-submit"]')) + cy.clickAndWait(cy.get(formElId).find('[data-action="form-submit"]'), 200) // check the output for the topic cy.get(payloadElId).find('.nrdb-ui-text-value').contains('{"name2":"flow.f1 test"}') cy.get(topicElId).find('.nrdb-ui-text-value').contains('topic from flow.f1') @@ -101,11 +103,11 @@ describe('Node-RED Dashboard 2.0 - Forms', () => { it('Delivers topic from global.g1', () => { const formElId = '#nrdb-ui-widget-cf0774a3c2e9edd4' // wait for the input to be actionable - cy.get(formElId).find('[data-form="form-row-name3"]').find('input[type="text"]').should('not.be.disabled') + cy.get(formElId).find('[data-form="form-row-name3"] input[type="text"]').should('not.be.disabled') // enter a value into the text input field - cy.get(formElId).find('[data-form="form-row-name3"]').find('input[type="text"]').type('global.g1 test') + cy.get(formElId).find('[data-form="form-row-name3"] input[type="text"]').type('global.g1 test') // submit the form - cy.clickAndWait(cy.get(formElId).find('[data-action="form-submit"]')) + cy.clickAndWait(cy.get(formElId).find('[data-action="form-submit"]'), 200) // check the output for the topic cy.get(payloadElId).find('.nrdb-ui-text-value').contains('{"name3":"global.g1 test"}') cy.get(topicElId).find('.nrdb-ui-text-value').contains('topic from global.g1') diff --git a/nodes/config/ui_base.js b/nodes/config/ui_base.js index 3c5461709..028125d68 100644 --- a/nodes/config/ui_base.js +++ b/nodes/config/ui_base.js @@ -142,7 +142,7 @@ module.exports = function (RED) { // - Redirects (301) URLs ending with a trailing slash to their non-trailing equivalent. // - Preserves the query string. // - Collapses any redundant slashes (e.g., "//" becomes "/"). - uiShared.app.use((req, res, next) => { + uiShared.app.use(config.path + '/', (req, res, next) => { // Skip if not a GET or HEAD request if (!(req.method === 'GET' || req.method === 'HEAD')) { next() @@ -153,7 +153,8 @@ module.exports = function (RED) { if (req.path.slice(-1) === '/' && req.path.length > 1) { const query = req.url.slice(req.path.length) // get the query string (if any) const safePath = req.path.slice(0, -1).replace(/\/+/g, '/') // remove trailing slash and make any double slashes single slashes - res.redirect(301, safePath + query) // redirect to the safe path + const redirectUrl = config.path + safePath + query // construct the new URL + res.redirect(301, redirectUrl) // redirect to the new URL } else { next() } diff --git a/ui/src/sw.js b/ui/src/sw.js index dff1fb452..2096874cd 100644 --- a/ui/src/sw.js +++ b/ui/src/sw.js @@ -10,14 +10,24 @@ precacheAndRoute(self.__WB_MANIFEST) cleanupOutdatedCaches() /** @type {RegExp[] | undefined} */ -let allowlist -// in dev mode, we disable precaching to avoid caching issues -if (import.meta.env.DEV) { allowlist = [/^\/$/] } +const denylist = [] -// to allow work offline +// in dev mode, do not precache anything +if (import.meta.env.DEV) { + // don't precache anything + console.log('Development mode, not pre-caching anything') + denylist.push(/.*/) +} else { + // don't precache anything where the urls pathname ends with a slash (including times when the url has a query string) + // this permits the request to be handled by the server which will do a redirect as required + const configPath = self.location.pathname.split('/')[1] + denylist.push(new RegExp(`/${configPath}/[^?]*/(\\?.*)*$`)) +} + +// to allow work offline for allowed routes only registerRoute(new NavigationRoute( createHandlerBoundToURL('index.html'), - { allowlist } + { denylist } )) self.skipWaiting()