From 8867de8397a567e4ce6c6df9e59a4743ec5b1401 Mon Sep 17 00:00:00 2001 From: Joe Pavitt Date: Tue, 27 Aug 2024 16:33:16 +0100 Subject: [PATCH 001/109] Layout WYSIWYG - Drag & Drop Ordering for Groups --- nodes/config/locales/en-US/ui_page.json | 1 + nodes/config/ui_page.html | 1 + ui/src/api/node-red.js | 18 ++ ui/src/layouts/WYSIWYG.vue | 228 ++++++++++++++++++++++++ ui/src/layouts/index.mjs | 4 +- 5 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 ui/src/api/node-red.js create mode 100644 ui/src/layouts/WYSIWYG.vue diff --git a/nodes/config/locales/en-US/ui_page.json b/nodes/config/locales/en-US/ui_page.json index 3214ea881..aacd528c1 100644 --- a/nodes/config/locales/en-US/ui_page.json +++ b/nodes/config/locales/en-US/ui_page.json @@ -7,6 +7,7 @@ "icon": "Icon", "theme": "Theme", "layout": "Layout", + "wysiwyg": "Editor", "grid": "Grid", "fixed": "Fixed", "tabs": "Tabs", diff --git a/nodes/config/ui_page.html b/nodes/config/ui_page.html index 371eb9274..1f061f41d 100644 --- a/nodes/config/ui_page.html +++ b/nodes/config/ui_page.html @@ -51,6 +51,7 @@ types: [{ value: 'layout', options: [ + { value: 'wysiwyg', label: RED._('@flowfuse/node-red-dashboard/ui-page:ui-page.label.wysiwyg') }, { value: 'grid', label: RED._('@flowfuse/node-red-dashboard/ui-page:ui-page.label.grid') }, { value: 'flex', label: RED._('@flowfuse/node-red-dashboard/ui-page:ui-page.label.fixed') }, { value: 'tabs', label: RED._('@flowfuse/node-red-dashboard/ui-page:ui-page.label.tabs') }, diff --git a/ui/src/api/node-red.js b/ui/src/api/node-red.js new file mode 100644 index 000000000..6103e7bb3 --- /dev/null +++ b/ui/src/api/node-red.js @@ -0,0 +1,18 @@ +export default { + deploy: function () { + console.log('deploy changes') + // return cy.request({ + // method: 'POST', + // url: 'http://localhost:1881/flows', + // headers: { + // 'Content-type': 'application/json', + // 'Node-RED-API-Version': 'v2', + // 'Node-RED-Deployment-Type': 'full' + // }, + // body: { + // rev, + // flows + // } + // }) + } +} diff --git a/ui/src/layouts/WYSIWYG.vue b/ui/src/layouts/WYSIWYG.vue new file mode 100644 index 000000000..45ffa271d --- /dev/null +++ b/ui/src/layouts/WYSIWYG.vue @@ -0,0 +1,228 @@ + + + + + diff --git a/ui/src/layouts/index.mjs b/ui/src/layouts/index.mjs index 74052a14e..19cc76c4a 100644 --- a/ui/src/layouts/index.mjs +++ b/ui/src/layouts/index.mjs @@ -2,10 +2,12 @@ import Flex from './Flex.vue' import Grid from './Grid.vue' import Notebook from './Notebook.vue' import Tabs from './Tabs.vue' +import WYSIWYG from './WYSIWYG.vue' export default { flex: Flex, grid: Grid, tabs: Tabs, - notebook: Notebook + notebook: Notebook, + wysiwyg: WYSIWYG } From a83345628be2b4898f1ccc0ea9581189b68df72a Mon Sep 17 00:00:00 2001 From: Joe Pavitt Date: Thu, 19 Sep 2024 21:59:57 +0100 Subject: [PATCH 002/109] Working prototype of end-to-end API for deploy via Dashboard --- nodes/config/ui_base.js | 40 ++++++++++++++++++++++++++++++++++++++ package-lock.json | 18 ++++++----------- package.json | 1 + ui/src/api/node-red.js | 27 +++++++++++++------------ ui/src/layouts/WYSIWYG.vue | 16 +++++++++++---- ui/src/store/ui.mjs | 6 ++++++ 6 files changed, 78 insertions(+), 30 deletions(-) diff --git a/nodes/config/ui_base.js b/nodes/config/ui_base.js index b12a048a7..e18911cc6 100644 --- a/nodes/config/ui_base.js +++ b/nodes/config/ui_base.js @@ -1,5 +1,7 @@ const path = require('path') +const axios = require('axios') + const v = require('../../package.json').version const datastore = require('../store/data.js') const statestore = require('../store/state.js') @@ -1069,4 +1071,42 @@ module.exports = function (RED) { } RED.nodes.registerType('ui-base', UIBaseNode) + + RED.httpAdmin.patch('/dashboard/:dashboardId', RED.auth.needsPermission('flows.write'), async function (req, res) { + const host = RED.settings.uiHost + const port = RED.settings.uiPort + const httpAdminRoot = RED.settings.httpAdminRoot + const url = 'http://' + (`${host}:${port}/${httpAdminRoot}flows`).replace('//', '/') + console.log('url', url) + // get request body + const changes = req.body + console.log(changes) + // get the active Node-RED flows json via the GET /flows API + return axios.request({ + method: 'GET', + url + }).then(response => { + const flows = response.data + // console.log('API Flows', flows) + // check groups for updates + if (changes.groups) { + for (const group of changes.groups) { + const existingGroup = flows.find(n => n.id === group.id) + if (existingGroup) { + existingGroup.order = group.order + } + } + } + return flows + }).then(flows => { + // update the flows with the new group order + return axios.request({ + method: 'POST', + url, + data: flows + }) + }).then(response => { + return res.status(200).json(response.data) + }) + }) } diff --git a/package-lock.json b/package-lock.json index 2f308cb7a..e97d7965a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "Apache-2.0", "dependencies": { "acorn": "^8.11.2", + "axios": "^1.7.7", "chartjs-adapter-luxon": "^1.3.1", "core-js": "^3.32.0", "d3": "^7.8.5", @@ -4696,8 +4697,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/at-least-node": { "version": "1.0.0", @@ -4739,10 +4739,9 @@ "dev": true }, "node_modules/axios": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", - "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", - "dev": true, + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -4753,7 +4752,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -4766,8 +4764,7 @@ "node_modules/axios/node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, "node_modules/b4a": { "version": "1.6.6", @@ -5782,7 +5779,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -6878,7 +6874,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -8379,7 +8374,6 @@ "version": "1.15.6", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "dev": true, "funding": [ { "type": "individual", diff --git a/package.json b/package.json index bf7b80447..709cc38ad 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ }, "dependencies": { "acorn": "^8.11.2", + "axios": "^1.7.7", "chartjs-adapter-luxon": "^1.3.1", "core-js": "^3.32.0", "d3": "^7.8.5", diff --git a/ui/src/api/node-red.js b/ui/src/api/node-red.js index 6103e7bb3..a61378628 100644 --- a/ui/src/api/node-red.js +++ b/ui/src/api/node-red.js @@ -1,18 +1,17 @@ +import axios from 'axios' + export default { - deploy: function () { + deployChanges: function (dashboardId, changes) { console.log('deploy changes') - // return cy.request({ - // method: 'POST', - // url: 'http://localhost:1881/flows', - // headers: { - // 'Content-type': 'application/json', - // 'Node-RED-API-Version': 'v2', - // 'Node-RED-Deployment-Type': 'full' - // }, - // body: { - // rev, - // flows - // } - // }) + return axios.request({ + method: 'PATCH', + url: '/editor/dashboard/' + dashboardId, + headers: { + 'Content-type': 'application/json', + 'Node-RED-API-Version': 'v2', + 'Node-RED-Deployment-Type': 'full' + }, + data: changes + }) } } diff --git a/ui/src/layouts/WYSIWYG.vue b/ui/src/layouts/WYSIWYG.vue index 45ffa271d..90492cc34 100644 --- a/ui/src/layouts/WYSIWYG.vue +++ b/ui/src/layouts/WYSIWYG.vue @@ -67,7 +67,7 @@ export default { computed: { ...mapState('ui', ['pages']), ...mapState('data', ['properties']), - ...mapGetters('ui', ['groupsByPage', 'widgetsByGroup', 'widgetsByPage']), + ...mapGetters('ui', ['id', 'groupsByPage', 'widgetsByGroup', 'widgetsByPage']), orderedGroups: function () { // get groups on this page const groups = this.groupsByPage(this.$route.meta.id) @@ -111,7 +111,9 @@ export default { }, save () { // API call to NR to trigger a deploy - NodeREDAPI.deploy() + NodeREDAPI.deployChanges(this.id, { + groups: this.groups + }) }, discard () { // reload groups from store @@ -177,7 +179,7 @@ export default { } - diff --git a/ui/src/layouts/WYSIWYG.vue b/ui/src/layouts/WYSIWYG.vue index 90492cc34..aa55ca17c 100644 --- a/ui/src/layouts/WYSIWYG.vue +++ b/ui/src/layouts/WYSIWYG.vue @@ -1,6 +1,15 @@ @@ -71,6 +86,9 @@ export default { groups: [], dragging: { index: -1 + }, + init: { + groups: [] } } }, @@ -98,11 +116,17 @@ export default { }, page: function () { return this.pages[this.$route.meta.id] + }, + hasChanges () { + return JSON.stringify(this.groups) !== JSON.stringify(this.init.groups) } }, mounted () { // get groups for this page this.groups = this.loadGroupsFromStore() + + // clone to track changes + this.init.groups = JSON.parse(JSON.stringify(this.groups)) }, methods: { loadGroupsFromStore () { @@ -123,11 +147,19 @@ export default { // API call to NR to trigger a deploy NodeREDAPI.deployChanges(this.id, { groups: this.groups + }).then(() => { + // update saved state + this.init.groups = JSON.parse(JSON.stringify(this.groups)) + }).catch((error) => { + console.error('Error saving changes', error) }) }, discard () { // reload groups from store - this.groups = this.loadGroupsFromStore() + this.groups = JSON.parse(JSON.stringify(this.init.groups)) + }, + cancel () { + console.log('cancel editing placeholder') }, onDragStart (event, index) { this.dragging.index = index @@ -212,7 +244,7 @@ export default { .nrdb-ui-editor-tray { background-color: white; border: 1px solid #ccc; - box-shadow: 0px 0px 5px black; + box-shadow: 0px 0px 5px #00000021; padding: 12px; border-radius: 4px; display: flex; From 78d7c6a72b15129180636fa22b427d8dd2f7df20 Mon Sep 17 00:00:00 2001 From: Joe Pavitt Date: Tue, 8 Oct 2024 12:00:03 +0100 Subject: [PATCH 005/109] Componentise the WYSWIYG functinality --- ui/src/layouts/WYSIWYG.vue | 39 ++------------------------ ui/src/layouts/wysiwyg/index.js | 44 ++++++++++++++++++++++++++++++ ui/src/layouts/wysiwyg/sortable.js | 39 ++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 36 deletions(-) create mode 100644 ui/src/layouts/wysiwyg/index.js create mode 100644 ui/src/layouts/wysiwyg/sortable.js diff --git a/ui/src/layouts/WYSIWYG.vue b/ui/src/layouts/WYSIWYG.vue index 6ba82c082..34aae96af 100644 --- a/ui/src/layouts/WYSIWYG.vue +++ b/ui/src/layouts/WYSIWYG.vue @@ -61,8 +61,6 @@ - {{ groups }} - {{ init.groups }} @@ -72,6 +70,8 @@ import NodeREDAPI from '../api/node-red' import BaselineLayout from './Baseline.vue' import WidgetGroup from './Group.vue' +import WYSIWYG from './wysiwyg' + // eslint-disable-next-line import/order, sort-imports import { mapState, mapGetters } from 'vuex' @@ -81,6 +81,7 @@ export default { BaselineLayout, WidgetGroup }, + mixins: [WYSIWYG], data () { return { groups: [], @@ -161,36 +162,6 @@ export default { cancel () { console.log('cancel editing placeholder') }, - onDragStart (event, index) { - this.dragging.index = index - event.dataTransfer.effectAllowed = 'move' - }, - onDragOver (event, index) { - if (this.dragging.index >= 0) { - event.preventDefault() - event.dataTransfer.dropEffect = 'move' - this.moveGroup(this.dragging.index, index) - } - }, - onDrop (event, index) { - event.preventDefault() - if (this.dragging.index >= 0) { - this.moveGroup(this.dragging.index, index) - this.dragging.index = -1 - } - }, - onDragEnd (event, index) { - this.dragging.index = -1 - }, - moveGroup (fromIndex, toIndex) { - const movedItem = this.groups.splice(fromIndex, 1)[0] - this.groups.splice(toIndex, 0, movedItem) - // update .order property of all groups - this.groups.forEach((group, index) => { - group.order = index + 1 - }) - this.dragging.index = toIndex - }, getWidgetClass (widget) { const classes = [] // ensure each widget has a class for its type @@ -221,10 +192,6 @@ export default { classes.push('dragging') } return classes.join(' ') - }, - onGroupResize (opts) { - this.groups[opts.index].width = opts.width - // this.groups[opts.index].height = opts.height } } } diff --git a/ui/src/layouts/wysiwyg/index.js b/ui/src/layouts/wysiwyg/index.js new file mode 100644 index 000000000..1a66f4e45 --- /dev/null +++ b/ui/src/layouts/wysiwyg/index.js @@ -0,0 +1,44 @@ +export default { + data () { + return { + dragging: { + index: -1 + } + } + }, + methods: { + onDragStart (event, index) { + this.dragging.index = index + event.dataTransfer.effectAllowed = 'move' + }, + onDragOver (event, index) { + if (this.dragging.index >= 0) { + event.preventDefault() + event.dataTransfer.dropEffect = 'move' + this.moveGroup(this.dragging.index, index) + } + }, + onDrop (event, index) { + event.preventDefault() + if (this.dragging.index >= 0) { + this.moveGroup(this.dragging.index, index) + this.dragging.index = -1 + } + }, + onDragEnd (event, index) { + this.dragging.index = -1 + }, + moveGroup (fromIndex, toIndex) { + const movedItem = this.groups.splice(fromIndex, 1)[0] + this.groups.splice(toIndex, 0, movedItem) + // update .order property of all groups + this.groups.forEach((group, index) => { + group.order = index + 1 + }) + this.dragging.index = toIndex + }, + onGroupResize (opts) { + this.groups[opts.index].width = opts.width + } + } +} diff --git a/ui/src/layouts/wysiwyg/sortable.js b/ui/src/layouts/wysiwyg/sortable.js new file mode 100644 index 000000000..96053b454 --- /dev/null +++ b/ui/src/layouts/wysiwyg/sortable.js @@ -0,0 +1,39 @@ +const sortable = { + data () { + + }, + created: (el, binding) => { + console.log(binding.instance) + function onDragStart (event, index) { + this.dragging.index = index + event.dataTransfer.effectAllowed = 'move' + } + + function onDragOver (event, index) { + if (this.dragging.index >= 0) { + event.preventDefault() + event.dataTransfer.dropEffect = 'move' + this.moveGroup(this.dragging.index, index) + } + } + + function onDrop (event, index) { + event.preventDefault() + if (this.dragging.index >= 0) { + this.moveGroup(this.dragging.index, index) + this.dragging.index = -1 + } + } + + function onDragEnd (event, index) { + this.dragging.index = -1 + } + + el.addEventListener('dragstart', onDragStart) + el.addEventListener('dragover', onDragOver) + el.addEventListener('drop', onDrop) + el.addEventListener('dragend', onDragEnd) + } +} + +export default sortable From daafd72fe676311f542813017d2b630fc17fce2a Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 14 Oct 2024 10:26:24 +0100 Subject: [PATCH 006/109] support enabling edit mode for a page from sidebar --- nodes/config/ui_base.js | 44 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/nodes/config/ui_base.js b/nodes/config/ui_base.js index f9cb5996e..846cb7ca2 100644 --- a/nodes/config/ui_base.js +++ b/nodes/config/ui_base.js @@ -418,6 +418,7 @@ module.exports = function (RED) { // pass the connected UI the UI config socket.emit('ui-config', node.id, { + meta: node.ui.meta, dashboards: Object.fromEntries(node.ui.dashboards), heads: Object.fromEntries(node.ui.heads), pages: Object.fromEntries(node.ui.pages), @@ -758,6 +759,15 @@ module.exports = function (RED) { */ // store ui config to be sent to UI node.ui = { + meta: { + wysiwyg: { + enabled: false, + timestamp: null, + dashboard: null, + page: null, + editKey: null + } + }, heads: new Map(), dashboards: new Map(), pages: new Map(), @@ -1089,7 +1099,8 @@ module.exports = function (RED) { RED.nodes.registerType('ui-base', UIBaseNode) - RED.httpAdmin.patch('/dashboard/:dashboardId', RED.auth.needsPermission('flows.write'), async function (req, res) { + // PATCH: /dashboard/api/v1/:dashboardId/flows - deploy curated/controlled updates to the flows + RED.httpAdmin.patch('/dashboard/api/v1/:dashboardId/flows', RED.auth.needsPermission('flows.write'), async function (req, res) { const host = RED.settings.uiHost const port = RED.settings.uiPort const httpAdminRoot = RED.settings.httpAdminRoot @@ -1126,5 +1137,36 @@ module.exports = function (RED) { }).then(response => { return res.status(200).json(response.data) }) + console.error(error) + const status = error.response?.status || 500 + return res.status(status).json({ error: error.message }) + }) + }) + + // PATCH: /dashboard/api/v1/:dashboardId/edit/:pageId - start editing a page + RED.httpAdmin.patch('/dashboard/api/v1/:dashboardId/edit/:pageId', RED.auth.needsPermission('flows.write'), async function (req, res) { + /** @type {UIBaseNode} */ + const baseNode = RED.nodes.getNode(req.params.dashboardId) + if (!baseNode) { + return res.status(404).json({ error: 'Dashboard not found' }) + } + const pageNode = baseNode.ui.pages.get(req.params.pageId) + if (!pageNode) { + return res.status(404).json({ error: 'Page not found' }) + } + const editConfig = { + timestamp: Date.now(), + path: pageNode.path || '', + dashboard: baseNode.id, + page: pageNode.id, + editKey: Math.random().toString(36).substring(2) + } + baseNode.ui.meta.wysiwyg.enabled = true + baseNode.ui.meta.wysiwyg.timestamp = editConfig.timestamp + baseNode.ui.meta.wysiwyg.editKey = editConfig.editKey + baseNode.ui.meta.wysiwyg.dashboard = baseNode.id + baseNode.ui.meta.wysiwyg.page = pageNode.id + + return res.status(200).json(editConfig) }) } From fda6848473f9f9fbf5484bbabad24dd679458c9a Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 14 Oct 2024 10:28:53 +0100 Subject: [PATCH 007/109] on-hover buttons for enabling edit mode on supported pages --- nodes/config/ui_base.html | 79 ++++++++++++++++++++++++++++++++++----- 1 file changed, 70 insertions(+), 9 deletions(-) diff --git a/nodes/config/ui_base.html b/nodes/config/ui_base.html index aa3953925..d239578b5 100644 --- a/nodes/config/ui_base.html +++ b/nodes/config/ui_base.html @@ -288,6 +288,7 @@ // #endregion (function () { + const supportedEditableLayouts = ['grid', 'flex'] const sidebarContainer = '
' const sidebarContentTemplate = $('
').appendTo(sidebarContainer) const sidebar = $(sidebarContentTemplate) @@ -824,14 +825,16 @@ const configNodes = ['ui-base', 'ui-page', 'ui-link', 'ui-group', 'ui-theme'] const btnGroup = $('
', { class: 'nrdb2-sb-list-header-button-group', id: item.id }).appendTo(parent) if (!configNodes.includes(item.type)) { - const focusButton = $(' ' + c_('layout.focus') + '').appendTo(btnGroup) + const focusButton = $(``).appendTo(btnGroup) focusButton.on('click', function (evt) { RED.view.reveal(item.id) evt.stopPropagation() evt.preventDefault() }) } - const editButton = $(' ' + c_('layout.edit') + '').appendTo(btnGroup) + + // button to edit node via node-red editor panel + const editButton = $(``).appendTo(btnGroup) editButton.on('click', function (evt) { if (configNodes.includes(item.type)) { RED.editor.editConfig('', item.type, item.id) @@ -841,14 +844,72 @@ evt.stopPropagation() evt.preventDefault() }) - if (item.type === 'ui-page') { + + if (item.type === 'ui-page' && supportedEditableLayouts.includes(item.node.layout)) { + // button to edit group in wysiwyg layout editor + const layoutButton = $(``).appendTo(btnGroup) + layoutButton.on('click', async function (evt) { + evt.preventDefault() + evt.stopPropagation() + + // call to httpadmin endpoint requesting layout editor mode for this group + const windowUrl = new URL(window.location.href) + const pageId = item.id + const baseId = item.node.ui + const base = RED.nodes.node(item.node.ui) + let basePath = base.path || '/dashboard' + if (basePath.endsWith('/')) { + basePath = basePath.slice(0, -1) + } + if (!basePath.startsWith('/')) { + basePath = `/${basePath}` + } + const authTokens = RED.settings.get('auth-tokens') || {} + const headers = {} + if (authTokens.access_token) { + headers.Authorization = `${authTokens.token_type || 'Bearer'} ${authTokens.access_token}` + } + + // promisify the ajax call so we can await it & return false after opening the popup + const ajax = () => { + return new Promise((resolve, reject) => { + $.ajax({ + url: `${base.path}/api/v1/${baseId}/edit/${pageId}`, // e.g. /dashboard/api/v1/123/edit/456 + type: 'PATCH', + headers, + data: { + mode: 'edit', + dashboard: baseId, + page: pageId + }, + success: function (data) { + // open the dashboard on that page in a _blank window + const _url = new URL(`${basePath}/${data.path}`.replace(/\/\//g, '/'), windowUrl.origin) + _url.searchParams.set('edit-key', data.editKey) + resolve(_url) + }, + error: function (err) { + reject(err) + } + }) + }) + } + try { + const url = await ajax() + const target = `ff-dashboard-${baseId}` // try to reuse the same window per base + window.open(url, target) + } catch (err) { + console.error('layout mode error', err) + RED.notify(base._('common.notification.error', { message: 'Unable to begin layout editor' }), 'error') + } + return false // return false to click event to prevent default + }) // add the "+ group" button - $(' ' + c_('layout.group') + '') - .click(function (evt) { - list.editableList('addItem') - evt.preventDefault() - }) - .appendTo(btnGroup) + const groupEditButton = $(``).appendTo(btnGroup) + groupEditButton.on('click', function (evt) { + list.editableList('addItem') + evt.preventDefault() + }) } } From 88d0de9e2db2b69699b1badf746997b889efe6e4 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 14 Oct 2024 10:29:19 +0100 Subject: [PATCH 008/109] consistent target (per base) --- nodes/config/ui_base.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nodes/config/ui_base.html b/nodes/config/ui_base.html index d239578b5..a306a7086 100644 --- a/nodes/config/ui_base.html +++ b/nodes/config/ui_base.html @@ -453,8 +453,8 @@ editSettingsButton.on('click', function () { RED.editor.editConfig('', 'ui-base', id) }) - - const openDashboardButton = $(`` + + const target = `nr-dashboard-${id}` // try to reuse the same window per base + const openDashboardButton = $(`` + c_('label.openDashboard') + ' ') label.appendTo(header) From 35bbd8874ba4cfa4c3a4de97b0ca826dfad79626 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 14 Oct 2024 10:30:06 +0100 Subject: [PATCH 009/109] update deploy endpoint to v2 and add error handling --- nodes/config/ui_base.js | 106 ++++++++++++++++++++++++++++++++++------ 1 file changed, 91 insertions(+), 15 deletions(-) diff --git a/nodes/config/ui_base.js b/nodes/config/ui_base.js index 846cb7ca2..21933fa97 100644 --- a/nodes/config/ui_base.js +++ b/nodes/config/ui_base.js @@ -1107,36 +1107,112 @@ module.exports = function (RED) { const url = 'http://' + (`${host}:${port}/${httpAdminRoot}flows`).replace('//', '/') console.log('url', url) // get request body - const changes = req.body - console.log(changes) - // get the active Node-RED flows json via the GET /flows API + const dashboardId = req.params.dashboardId + const pageId = req.body.page + const changes = req.body.changes || {} + const editKey = req.body.key + const groups = changes.groups || [] + console.log(changes, editKey, dashboardId) + const baseNode = RED.nodes.getNode(dashboardId) + + // validity checks + if (groups.length === 0) { + // this could be a 200 but since the group data might be missing due to + // a bug or regression, we'll return a 400 and let the user know + // there were no changes provided. + return res.status(400).json({ error: 'No changes to deploy' }) + } + if (!baseNode) { + return res.status(404).json({ error: 'Dashboard not found' }) + } + if (!baseNode.ui.meta.wysiwyg.enabled) { + return res.status(403).json({ error: 'Unauthorized' }) + } + if (editKey !== baseNode.ui.meta.wysiwyg.editKey) { + return res.status(403).json({ error: 'Unauthorized' }) + } + if (pageId !== baseNode.ui.meta.wysiwyg.page) { + return res.status(403).json({ error: 'Unauthorized' }) + } + for (const modified of groups) { + if (modified.page !== baseNode.ui.meta.wysiwyg.page) { + return res.status(400).json({ error: 'Invalid page id' }) + } + } + + // Prepare headers for the requests + const getHeaders = { + 'Node-RED-API-Version': 'v2', + Accept: 'application/json' + } + const postHeaders = { + 'Node-RED-Deployment-Type': 'nodes', // only update the nodes (don't restart ALL nodes! Only those that have changed) + 'Node-RED-API-Version': 'v2', + 'Content-Type': 'application/json' + } + // apply headers from the incoming request + if (req.headers.cookie) { + getHeaders.cookie = req.headers.cookie + postHeaders.cookie = req.headers.cookie + } + if (req.headers.authorization) { + getHeaders.authorization = req.headers.authorization + postHeaders.authorization = req.headers.authorization + } + if (req.headers.referer) { + getHeaders.referer = req.headers.referer + postHeaders.referer = req.headers.referer + } + + const applyIfDifferent = (node, nodeNew, propName) => { + const origValue = node[propName] + const newValue = nodeNew[propName] + if (origValue !== newValue) { + node[propName] = newValue + return true + } + return false + } + let rev = null return axios.request({ method: 'GET', + headers: getHeaders, url }).then(response => { - const flows = response.data - // console.log('API Flows', flows) - // check groups for updates - if (changes.groups) { - for (const group of changes.groups) { - const existingGroup = flows.find(n => n.id === group.id) - if (existingGroup) { - existingGroup.width = group.width - existingGroup.order = group.order - } + const flows = response.data?.flows || [] + rev = response.data?.rev + const changeResult = [] + for (const modified of groups) { + const current = flows.find(n => n.id === modified.id) + if (!current) { + // group not found in current flows! integrity of data suspect! Has flows changed on the server? + return res.status(400).json({ error: 'Group not found', code: 'GROUP_NOT_FOUND' }) + } + if (modified.page !== current.page) { + // integrity of data suspect! Has flow changed on the server? + return res.status(400).json({ error: 'Invalid page id', code: 'INVALID_PAGE_ID' }) } + changeResult.push(applyIfDifferent(current, modified, 'width')) + changeResult.push(applyIfDifferent(current, modified, 'order')) + } + if (changeResult.length === 0 || !changeResult.includes(true)) { + return res.status(200).json({ message: 'No changes were' }) } return flows }).then(flows => { // update the flows with the new group order return axios.request({ method: 'POST', + headers: postHeaders, url, - data: flows + data: { + flows, + rev + } }) }).then(response => { return res.status(200).json(response.data) - }) + }).catch(error => { console.error(error) const status = error.response?.status || 500 return res.status(status).json({ error: error.message }) From 6258104bdd1403e5d20284451755fbe12634b16d Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 14 Oct 2024 10:30:37 +0100 Subject: [PATCH 010/109] add i18n messages fro edit buttons --- nodes/config/locales/en-US/ui_base.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nodes/config/locales/en-US/ui_base.json b/nodes/config/locales/en-US/ui_base.json index c9870ea12..614b615a4 100644 --- a/nodes/config/locales/en-US/ui_base.json +++ b/nodes/config/locales/en-US/ui_base.json @@ -37,7 +37,9 @@ "page": "Page", "link": "Link", "group": "Group", + "addGroup": "Add Group", "edit": "Edit", + "layoutEditor": "Layout Editor", "focus": "Focus", "collapse": "Collapse", "expand": "Expand", From ec476750216552694f3533b1f3ac094caf4b2779 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 14 Oct 2024 10:33:10 +0100 Subject: [PATCH 011/109] put edit controls in own component --- ui/src/layouts/wysiwyg/EditControls.vue | 63 +++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 ui/src/layouts/wysiwyg/EditControls.vue diff --git a/ui/src/layouts/wysiwyg/EditControls.vue b/ui/src/layouts/wysiwyg/EditControls.vue new file mode 100644 index 000000000..47062cc6d --- /dev/null +++ b/ui/src/layouts/wysiwyg/EditControls.vue @@ -0,0 +1,63 @@ + + + + + From 25797d051fd2ce15083970ceef6dd3623985ce79 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 14 Oct 2024 10:34:07 +0100 Subject: [PATCH 012/109] ensure query param edit-key is carried over upon forced refresh --- ui/src/main.mjs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/ui/src/main.mjs b/ui/src/main.mjs index 5ea62b7fa..6fc0468cf 100644 --- a/ui/src/main.mjs +++ b/ui/src/main.mjs @@ -74,14 +74,19 @@ function forcePageReload (err) { console.log('redirecting to:', window.location.origin + '/dashboard') // Reloading dashboard without using cache by appending a cache-busting string to fully reload page to allow redirecting to auth - window.location.replace(window.location.origin + '/dashboard' + '?' + 'reloadTime=' + Date.now().toString() + Math.random()) // Seems to work on Edge and Chrome on Windows, Chromium and Firefox on Linux, and also on Chrome Android (and also as PWA App) + const url = new URL(window.location.origin + '/dashboard') + url.searchParams.set('reloadTime', Date.now().toString() + Math.random()) + if (host.searchParams.has('edit-key')) { + url.searchParams.set('edit-key', host.searchParams.get('edit-key')) + } + window.location.replace(url) } /* * Configure SocketIO Client to Interact with Node-RED */ -// if our scoket disconnects, we should inform the user when it reconnects +// if our socket disconnects, we should inform the user when it reconnects // GET our SocketIO Config from Node-RED & any other bits plugins have added to the _setup endpoint fetch('_setup') @@ -93,10 +98,15 @@ fetch('_setup') case !response.ok: console.error('Failed to fetch setup data:', response) return - case host.origin !== new URL(response.url).origin: + case host.origin !== new URL(response.url).origin: { console.log('Following redirect:', response.url) - window.location.replace(response.url) + const url = new URL(response.url) + if (host.searchParams.has('edit-key')) { + url.searchParams.set('edit-key', host.searchParams.get('edit-key')) + } + window.location.replace(url) return + } default: break } From b040f625d9138fefadfcb7f92c4c1f127ef2abfb Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 14 Oct 2024 10:38:14 +0100 Subject: [PATCH 013/109] Add edit tracking for edit buffer and state --- ui/src/EditTracking.js | 57 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 ui/src/EditTracking.js diff --git a/ui/src/EditTracking.js b/ui/src/EditTracking.js new file mode 100644 index 000000000..59485eb83 --- /dev/null +++ b/ui/src/EditTracking.js @@ -0,0 +1,57 @@ +import { computed, reactive } from 'vue' + +// A simple non-vuex state store for edit tracking +const state = reactive({ + editKey: '', + editPage: '', + editMode: false, + isTrackingEdits: false, + originalGroups: [] +}) + +// Methods + +/** + * Initialise the edit tracking state + * @param {String} editKey - The edit key provided by the server + * @param {String} editPage - The page id to edit (provided by the server) + */ +function initialise (editKey, editPage) { + state.editKey = editKey + state.editPage = editPage + state.editMode = !!editKey && !!editPage +} + +/** + * Start tracking edits + */ +function startEditTracking (groups) { + state.isTrackingEdits = true + updateEditTracking(groups) +} + +/** + * Stop tracking edits, clear editKey/editPage & exit edit mode + */ +function exitEditMode () { + state.editKey = '' + state.editPage = '' + state.initialised = false + state.originalGroups = [] +} + +/** + * Update the original groups with the current groups + */ +function updateEditTracking (groups) { + state.originalGroups = JSON.parse(JSON.stringify(groups)) +} + +// RO computed props +const editKey = computed(() => state.editKey) +const editPage = computed(() => state.editPage) +const editMode = computed(() => !!state.editKey && !!state.editPage) +const originalGroups = computed(() => state.originalGroups) +const isTrackingEdits = computed(() => state.isTrackingEdits) + +export { editMode, editKey, editPage, originalGroups, isTrackingEdits, initialise, exitEditMode, startEditTracking, updateEditTracking } From 3e967b5f2d25bcde862caf87110018b9dcb0e759 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 14 Oct 2024 10:39:22 +0100 Subject: [PATCH 014/109] Init edit mode when matching query and config are present --- ui/src/App.vue | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/ui/src/App.vue b/ui/src/App.vue index b8afbb0d9..041a5418e 100644 --- a/ui/src/App.vue +++ b/ui/src/App.vue @@ -20,6 +20,7 @@ + + From 7ad09387b8ddf31112dc4f46889613e90a5dc028 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 14 Oct 2024 10:43:36 +0100 Subject: [PATCH 018/109] grab/move/resize pointers --- ui/src/layouts/Group.vue | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ui/src/layouts/Group.vue b/ui/src/layouts/Group.vue index f24050142..2792a4f2d 100644 --- a/ui/src/layouts/Group.vue +++ b/ui/src/layouts/Group.vue @@ -155,7 +155,9 @@ export default { top: 0; left: 0; --handler-size: 12px; + cursor: grab; &.active { + cursor: grabbing !important; background-color: #ff00001f; border: 1px dashed red; } @@ -167,7 +169,6 @@ export default { background-color: white; border: 1px solid black; border-radius: 6px; - cursor: pointer; } .nrdb-resizable--top-right { top: calc(-1 * var(--handler-size) / 2); @@ -186,6 +187,9 @@ export default { cursor: ew-resize; background-color: #eee; } + &:active { + cursor: ew-resize !important; + } } .nrdb-resizable--right:after { From 5737de60fa221d45b9c6fc3329a182b46cdb2cd5 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 14 Oct 2024 10:44:49 +0100 Subject: [PATCH 019/109] typo --- ui/src/layouts/Group.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/layouts/Group.vue b/ui/src/layouts/Group.vue index 2792a4f2d..5e4df710d 100644 --- a/ui/src/layouts/Group.vue +++ b/ui/src/layouts/Group.vue @@ -60,7 +60,7 @@ export default { columns: 0, rows: 0, width: 0, - heigh: 0 + height: 0 }, current: { columns: 0, From c2060eb1dd1a03f0b5fc50a826d7d664e42b57ba Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 14 Oct 2024 10:45:13 +0100 Subject: [PATCH 020/109] fix jump to size 1 when releasing mouse --- ui/src/layouts/Group.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/layouts/Group.vue b/ui/src/layouts/Group.vue index 5e4df710d..ea2c89def 100644 --- a/ui/src/layouts/Group.vue +++ b/ui/src/layouts/Group.vue @@ -63,8 +63,8 @@ export default { height: 0 }, current: { - columns: 0, - rows: 0, + columns: null, + rows: null, width: null } } From 6c29dca788ed8c9495c717a9af4307cbb7fc7709 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 14 Oct 2024 10:45:39 +0100 Subject: [PATCH 021/109] cast columns to integer --- ui/src/layouts/Group.vue | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ui/src/layouts/Group.vue b/ui/src/layouts/Group.vue index ea2c89def..e9452f6ea 100644 --- a/ui/src/layouts/Group.vue +++ b/ui/src/layouts/Group.vue @@ -72,8 +72,7 @@ export default { }, computed: { columns () { - const cols = this.dragging.current.columns > 0 ? this.dragging.current.columns : this.group.width - return cols + return this.dragging.current.columns > 0 ? this.dragging.current.columns : +this.group.width } }, methods: { From 6ad445b0f3214b12447db51cac98b9c4748f1284 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 14 Oct 2024 10:46:36 +0100 Subject: [PATCH 022/109] cast to integer with default of 1 --- ui/src/layouts/Group.vue | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/ui/src/layouts/Group.vue b/ui/src/layouts/Group.vue index e9452f6ea..3b73956e4 100644 --- a/ui/src/layouts/Group.vue +++ b/ui/src/layouts/Group.vue @@ -90,15 +90,8 @@ export default { }, onHandleDragStart (event, vertical, horizontal) { this.dragging.active = true - this.dragging.init.columns = parseFloat(this.group.width) - this.dragging.init.rows = parseFloat(this.group.height) - // event.preventDefault() - event.stopPropagation() - // don't show image preview - const EMPTY_IMAGE = this.$refs['blank-img'] - event.dataTransfer.setDragImage(EMPTY_IMAGE, 0, 0) - - this.dragging.init.x = event.x + this.dragging.init.columns = +this.group.width || 1 + this.dragging.init.rows = +this.group.height || 1 this.dragging.init.width = this.$refs['resize-view'].clientWidth return false }, From ee305dbbba2fab0e7199d4ca3cf236db30fcbf27 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 14 Oct 2024 10:47:28 +0100 Subject: [PATCH 023/109] code reorder --- ui/src/layouts/Group.vue | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ui/src/layouts/Group.vue b/ui/src/layouts/Group.vue index 3b73956e4..c2776b4a3 100644 --- a/ui/src/layouts/Group.vue +++ b/ui/src/layouts/Group.vue @@ -93,6 +93,11 @@ export default { this.dragging.init.columns = +this.group.width || 1 this.dragging.init.rows = +this.group.height || 1 this.dragging.init.width = this.$refs['resize-view'].clientWidth + this.dragging.init.x = event.x + const EMPTY_IMAGE = this.$refs['blank-img'] // don't show image preview + event.dataTransfer.setDragImage(EMPTY_IMAGE, 0, 0) + // prevent default drag behavior + event.stopPropagation() return false }, onHandleDrag (event, vertical, horizontal) { From 2c161e38ebefbc8b4f6ea3eb13adc70d43447f09 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 14 Oct 2024 10:48:10 +0100 Subject: [PATCH 024/109] dont attempt resize if not dragging --- ui/src/layouts/Group.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/src/layouts/Group.vue b/ui/src/layouts/Group.vue index c2776b4a3..028dcdc7d 100644 --- a/ui/src/layouts/Group.vue +++ b/ui/src/layouts/Group.vue @@ -101,6 +101,7 @@ export default { return false }, onHandleDrag (event, vertical, horizontal) { + if (this.dragging.active === false) { return } if (event.x > 0 && event.y > 0) { // odd event fired on mouse up with x/y = 0 const stepX = this.$el.clientWidth / this.group.width From ba54734944ea0c44c0a6cdf9441efff088d856f6 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 14 Oct 2024 10:49:12 +0100 Subject: [PATCH 025/109] prevent invalid values causing a resize --- ui/src/layouts/Group.vue | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/ui/src/layouts/Group.vue b/ui/src/layouts/Group.vue index 028dcdc7d..feab5a20a 100644 --- a/ui/src/layouts/Group.vue +++ b/ui/src/layouts/Group.vue @@ -103,29 +103,15 @@ export default { onHandleDrag (event, vertical, horizontal) { if (this.dragging.active === false) { return } if (event.x > 0 && event.y > 0) { - // odd event fired on mouse up with x/y = 0 - const stepX = this.$el.clientWidth / this.group.width - // const stepY = 50 - + const stepX = this.$el.clientWidth / +this.group.width const dx = event.x - this.dragging.init.x - // what change does this reflect in the grid? const dw = dx < 0 ? Math.ceil(dx / stepX) : Math.floor(dx / stepX) - - // const dh = Math.floor(event.offsetY / stepY) - this.dragging.current.width = this.dragging.init.width + dx - - if (dw !== 0) { - const width = this.dragging.init.columns + dw - // const height = this.dragging.init.rows + dh - - if (width !== this.group.width) { - this.dragging.current.columns = width - this.$emit('resize', { - index: this.index, - width - }) - } + const width = Math.max(this.dragging.init.columns + dw, 1) + if (width !== +this.group.width) { + // console.log('drag handle drag: width', width, 'emitting resize') + this.dragging.current.columns = width + this.$emit('resize', { index: this.index, width }) } } }, From 6f3a45b5cf636908ff710dc40055d8d9f47295a8 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 14 Oct 2024 10:49:37 +0100 Subject: [PATCH 026/109] add missing reset --- ui/src/layouts/Group.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/src/layouts/Group.vue b/ui/src/layouts/Group.vue index feab5a20a..6e1eec2d0 100644 --- a/ui/src/layouts/Group.vue +++ b/ui/src/layouts/Group.vue @@ -125,6 +125,7 @@ export default { resetDragState () { this.dragging.current.width = null this.dragging.current.columns = null + this.dragging.current.rows = null } } From 1b8140fbd83fd4a1939375f10cfd4d45321b12d3 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 14 Oct 2024 10:50:06 +0100 Subject: [PATCH 027/109] dont resize if not active --- ui/src/layouts/Group.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/layouts/Group.vue b/ui/src/layouts/Group.vue index 6e1eec2d0..859ebe7de 100644 --- a/ui/src/layouts/Group.vue +++ b/ui/src/layouts/Group.vue @@ -115,7 +115,7 @@ export default { } } }, - onHandleDragEnd () { + if (this.dragging.active === false) { return } this.dragging.active = false const width = Math.max(this.dragging.current.columns, 1) const height = Math.max(this.dragging.current.rows, 1) From cc1a33298224e400834b5a5772f79e66c0957c77 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 14 Oct 2024 10:50:39 +0100 Subject: [PATCH 028/109] only apply resize if something changed --- ui/src/layouts/Group.vue | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/ui/src/layouts/Group.vue b/ui/src/layouts/Group.vue index 859ebe7de..12fae7483 100644 --- a/ui/src/layouts/Group.vue +++ b/ui/src/layouts/Group.vue @@ -115,12 +115,21 @@ export default { } } }, + onHandleDragEnd (event) { if (this.dragging.active === false) { return } this.dragging.active = false - const width = Math.max(this.dragging.current.columns, 1) - const height = Math.max(this.dragging.current.rows, 1) + if (this.dragging.current.columns === null && this.dragging.current.rows === null) { + // console.log('drag handle end reset due to null columns or rows') + this.resetDragState() + return + } + const columns = Math.max(this.dragging.current.columns, 1) + const rows = Math.max(this.dragging.current.rows, 1) + const changed = this.dragging.init.columns !== columns || this.dragging.init.rows !== rows this.resetDragState() - this.$emit('resize', { index: this.index, width, height }) + if (changed) { + this.$emit('resize', { index: this.index, width: columns, height: rows }) + } }, resetDragState () { this.dragging.current.width = null From 08af2d51c8b526ab54e73b46ce299365a923b61b Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 14 Oct 2024 10:52:12 +0100 Subject: [PATCH 029/109] move implementation from temp wysiwyg to supported page types --- ui/src/layouts/Flex.vue | 110 ++++++++++++-- ui/src/layouts/Grid.vue | 116 +++++++++++++-- ui/src/layouts/WYSIWYG.vue | 254 -------------------------------- ui/src/layouts/index.mjs | 4 +- ui/src/layouts/wysiwyg/index.js | 98 +++++++++++- ui/src/stylesheets/common.css | 5 + 6 files changed, 297 insertions(+), 290 deletions(-) delete mode 100644 ui/src/layouts/WYSIWYG.vue diff --git a/ui/src/layouts/Flex.vue b/ui/src/layouts/Flex.vue index 6ef255057..3edeacf18 100644 --- a/ui/src/layouts/Flex.vue +++ b/ui/src/layouts/Flex.vue @@ -1,24 +1,30 @@ - - diff --git a/ui/src/layouts/index.mjs b/ui/src/layouts/index.mjs index 19cc76c4a..74052a14e 100644 --- a/ui/src/layouts/index.mjs +++ b/ui/src/layouts/index.mjs @@ -2,12 +2,10 @@ import Flex from './Flex.vue' import Grid from './Grid.vue' import Notebook from './Notebook.vue' import Tabs from './Tabs.vue' -import WYSIWYG from './WYSIWYG.vue' export default { flex: Flex, grid: Grid, tabs: Tabs, - notebook: Notebook, - wysiwyg: WYSIWYG + notebook: Notebook } diff --git a/ui/src/layouts/wysiwyg/index.js b/ui/src/layouts/wysiwyg/index.js index 1a66f4e45..3b627fbc7 100644 --- a/ui/src/layouts/wysiwyg/index.js +++ b/ui/src/layouts/wysiwyg/index.js @@ -1,20 +1,90 @@ +import { editKey, editMode, editPage, exitEditMode, isTrackingEdits, originalGroups, startEditTracking, updateEditTracking } from '../../EditTracking.js' +import NodeRedApi from '../../api/node-red' + export default { data () { return { + pageGroups: [], dragging: { index: -1 } } }, + computed: { + dirty () { + if (!this.editMode || !isTrackingEdits.value) { + return false + } + return JSON.stringify(this.pageGroups) !== JSON.stringify(originalGroups.value) + }, + editMode: function () { + return editMode.value && editPage.value === this.$route.meta.id + } + }, methods: { + initializeEditTracking () { + if (this.editMode && !isTrackingEdits.value) { + startEditTracking(this.pageGroups) + } + }, + acceptChanges () { + updateEditTracking(this.pageGroups) + }, + exitEditMode () { + const url = new URL(window.location.href) + url.searchParams.delete('edit-key') + const query = { ...this.$route.query } + delete query['edit-key'] + this.$router.replace({ query }) + window.history.replaceState({}, document.title, url) + exitEditMode() // EditTracking method + }, + revertEdits () { + this.pageGroups = JSON.parse(JSON.stringify(originalGroups.value)) + }, + deployChanges ({ dashboard, page, groups }) { + return NodeRedApi.deployChanges({ dashboard, page, groups, key: editKey.value }) + }, + /** + * Event handler for dragstart event + * @param {DragEvent} event - The drag event + * @param {Number} index - The index of the group + */ onDragStart (event, index) { this.dragging.index = index event.dataTransfer.effectAllowed = 'move' }, + /** + * Event handler for dragover event + * @param {DragEvent} event - The drag event + * @param {Number} index - The index of the group + */ onDragOver (event, index) { + // ensure the mouse is over a different group + if (this.dragging.index === index || this.dragging.index < 0) { + return + } + // ensure the mouse is within the bounds of the source group size + // to avoid flip-flop when the target group is larger than the source group + const sourceGroup = this.pageGroups[this.dragging.index] + const sourceId = `nrdb-ui-group-${sourceGroup.id}` + const sourceEl = document.getElementById(sourceId) + const sourceBounds = sourceEl.getBoundingClientRect() + const targetGroup = this.pageGroups[index] + const targetId = `nrdb-ui-group-${targetGroup.id}` + const targetEl = document.getElementById(targetId) + const targetBounds = targetEl.getBoundingClientRect() + const hitBoundX1 = targetBounds.left + const hitBoundX2 = targetBounds.left + sourceBounds.width + const hitBoundY1 = targetBounds.top + const hitBoundY2 = targetBounds.top + sourceBounds.height + if (event.clientX < hitBoundX1 || event.clientX > hitBoundX2 || event.clientY < hitBoundY1 || event.clientY > hitBoundY2) { + return + } + if (this.dragging.index >= 0) { event.preventDefault() - event.dataTransfer.dropEffect = 'move' + event.dataTransfer.dropEffect = 'drop' this.moveGroup(this.dragging.index, index) } }, @@ -29,16 +99,34 @@ export default { this.dragging.index = -1 }, moveGroup (fromIndex, toIndex) { - const movedItem = this.groups.splice(fromIndex, 1)[0] - this.groups.splice(toIndex, 0, movedItem) + const movedItem = this.pageGroups.splice(fromIndex, 1)[0] + this.pageGroups.splice(toIndex, 0, movedItem) // update .order property of all groups - this.groups.forEach((group, index) => { + this.pageGroups.forEach((group, index) => { group.order = index + 1 }) this.dragging.index = toIndex }, onGroupResize (opts) { - this.groups[opts.index].width = opts.width + // ensure opts.width is a number and is greater than 0 + if (typeof opts.width !== 'number' || opts.width < 1) { + return + } + this.pageGroups[opts.index].width = opts.width + }, + isDragging (group) { + const dragging = this.pageGroups[this.dragging.index] + if (dragging?.id === group?.id) { + return true + } + return false + }, + getElementBounds (id) { + const sourceGroup = this.pageGroups[this.dragging.index] + const sourceId = `nrdb-ui-group-${sourceGroup.id}` + const sourceEl = document.getElementById(sourceId) + const sourceBounds = sourceEl.getBoundingClientRect() + return sourceBounds } } } diff --git a/ui/src/stylesheets/common.css b/ui/src/stylesheets/common.css index e7b5f0439..23ec9a864 100644 --- a/ui/src/stylesheets/common.css +++ b/ui/src/stylesheets/common.css @@ -484,4 +484,9 @@ NB! supresses the gridarea for messages, but those are not in use*/ .v-btn.v-btn--density-comfortable { height: auto; min-height: var(--widget-row-height); +} + +/* WYSIWYG styling */ +.nrdb-ui-group.dragging { + border-style: dashed; } \ No newline at end of file From 45e96005b17dba762b18633c511931ba3908adbb Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 14 Oct 2024 14:21:19 +0100 Subject: [PATCH 030/109] remove dead code --- ui/src/layouts/wysiwyg/sortable.js | 39 ------------------------------ 1 file changed, 39 deletions(-) delete mode 100644 ui/src/layouts/wysiwyg/sortable.js diff --git a/ui/src/layouts/wysiwyg/sortable.js b/ui/src/layouts/wysiwyg/sortable.js deleted file mode 100644 index 96053b454..000000000 --- a/ui/src/layouts/wysiwyg/sortable.js +++ /dev/null @@ -1,39 +0,0 @@ -const sortable = { - data () { - - }, - created: (el, binding) => { - console.log(binding.instance) - function onDragStart (event, index) { - this.dragging.index = index - event.dataTransfer.effectAllowed = 'move' - } - - function onDragOver (event, index) { - if (this.dragging.index >= 0) { - event.preventDefault() - event.dataTransfer.dropEffect = 'move' - this.moveGroup(this.dragging.index, index) - } - } - - function onDrop (event, index) { - event.preventDefault() - if (this.dragging.index >= 0) { - this.moveGroup(this.dragging.index, index) - this.dragging.index = -1 - } - } - - function onDragEnd (event, index) { - this.dragging.index = -1 - } - - el.addEventListener('dragstart', onDragStart) - el.addEventListener('dragover', onDragOver) - el.addEventListener('drop', onDrop) - el.addEventListener('dragend', onDragEnd) - } -} - -export default sortable From 3f5cdcc7cb5a5d57bfa3e83eaeaca974e069cc4e Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 14 Oct 2024 18:24:18 +0100 Subject: [PATCH 031/109] Add pill indicator and draw indicator when in edit mode --- ui/src/layouts/Baseline.vue | 39 +++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/ui/src/layouts/Baseline.vue b/ui/src/layouts/Baseline.vue index 354016f50..2fc124336 100644 --- a/ui/src/layouts/Baseline.vue +++ b/ui/src/layouts/Baseline.vue @@ -7,6 +7,9 @@
@@ -37,7 +40,11 @@ :to="page.type === 'ui-page' ? { name: page.route.name } : null" :data-nav="page.id" @click="closeNavigationDrawer()" - /> + > + + @@ -65,6 +72,8 @@ + + From 97b74d8c822dc6ae21d36f4469bc374d5d8d9bc8 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 14 Oct 2024 18:49:34 +0100 Subject: [PATCH 032/109] lint fixes --- nodes/widgets/ui_dropdown.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nodes/widgets/ui_dropdown.html b/nodes/widgets/ui_dropdown.html index f40cc2ed2..75c662d47 100644 --- a/nodes/widgets/ui_dropdown.html +++ b/nodes/widgets/ui_dropdown.html @@ -54,13 +54,13 @@ } if (this.chips === undefined) { $('#node-input-chips').prop('checked', false) - } + } if (this.clearable === undefined) { $('#node-input-clearable').prop('checked', false) - } + } if (this.typeIsComboBox === undefined) { $('#node-input-typeIsComboBox').prop('checked', true) - } + } // if this groups parent is a subflow template, set the node-config-input-width and node-config-input-height up // as typedInputs and hide the elementSizer (as it doesn't make sense for subflow templates) if (RED.nodes.subflow(this.z)) { From ba0cb2635bbe86bb5c6dbbc2740aaf3a9baa3dcd Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 14 Oct 2024 19:21:03 +0100 Subject: [PATCH 033/109] allow widget width to be larger than group width --- nodes/widgets/ui_button.html | 2 +- nodes/widgets/ui_button_group.html | 2 +- nodes/widgets/ui_chart.html | 2 +- nodes/widgets/ui_dropdown.html | 2 +- nodes/widgets/ui_file_input.html | 2 +- nodes/widgets/ui_form.html | 2 +- nodes/widgets/ui_gauge.html | 2 +- nodes/widgets/ui_number_input.html | 2 +- nodes/widgets/ui_radio_group.html | 2 +- nodes/widgets/ui_slider.html | 2 +- nodes/widgets/ui_switch.html | 2 +- nodes/widgets/ui_table.html | 2 +- nodes/widgets/ui_template.html | 6 +++--- nodes/widgets/ui_text.html | 2 +- nodes/widgets/ui_text_input.html | 2 +- 15 files changed, 17 insertions(+), 17 deletions(-) diff --git a/nodes/widgets/ui_button.html b/nodes/widgets/ui_button.html index e965f70af..925e3d4c9 100644 --- a/nodes/widgets/ui_button.html +++ b/nodes/widgets/ui_button.html @@ -17,7 +17,7 @@ const width = v || 0 const currentGroup = $('#node-input-group').val() || this.group const groupNode = RED.nodes.node(currentGroup) - const valid = !groupNode || +width <= +groupNode.width + const valid = !groupNode || +width >= 0 $('#node-input-size').toggleClass('input-error', !valid) return valid } diff --git a/nodes/widgets/ui_button_group.html b/nodes/widgets/ui_button_group.html index 1254a1284..94ca11fae 100644 --- a/nodes/widgets/ui_button_group.html +++ b/nodes/widgets/ui_button_group.html @@ -15,7 +15,7 @@ const width = v || 0 const currentGroup = $('#node-input-group').val() || this.group const groupNode = RED.nodes.node(currentGroup) - const valid = !groupNode || +width <= +groupNode.width + const valid = !groupNode || +width >= 0 $('#node-input-size').toggleClass('input-error', !valid) return valid } diff --git a/nodes/widgets/ui_chart.html b/nodes/widgets/ui_chart.html index 8512ded08..d5b745a3a 100644 --- a/nodes/widgets/ui_chart.html +++ b/nodes/widgets/ui_chart.html @@ -134,7 +134,7 @@ const width = v || 0 const currentGroup = $('#node-input-group').val() || this.group const groupNode = RED.nodes.node(currentGroup) - const valid = !groupNode || +width <= +groupNode.width + const valid = !groupNode || +width >= 0 $('#node-input-size').toggleClass('input-error', !valid) return valid } diff --git a/nodes/widgets/ui_dropdown.html b/nodes/widgets/ui_dropdown.html index 75c662d47..5c06df3fd 100644 --- a/nodes/widgets/ui_dropdown.html +++ b/nodes/widgets/ui_dropdown.html @@ -18,7 +18,7 @@ const width = v || 0 const currentGroup = $('#node-input-group').val() || this.group const groupNode = RED.nodes.node(currentGroup) - const valid = !groupNode || +width <= +groupNode.width + const valid = !groupNode || +width >= 0 $('#node-input-size').toggleClass('input-error', !valid) return valid } diff --git a/nodes/widgets/ui_file_input.html b/nodes/widgets/ui_file_input.html index 8fe5763be..4130b98ae 100644 --- a/nodes/widgets/ui_file_input.html +++ b/nodes/widgets/ui_file_input.html @@ -16,7 +16,7 @@ const width = v || 0 const currentGroup = $('#node-input-group').val() || this.group const groupNode = RED.nodes.node(currentGroup) - const valid = !groupNode || +width <= +groupNode.width + const valid = !groupNode || +width >= 0 $('#node-input-size').toggleClass('input-error', !valid) return valid } diff --git a/nodes/widgets/ui_form.html b/nodes/widgets/ui_form.html index 441211da9..b2bfa318a 100644 --- a/nodes/widgets/ui_form.html +++ b/nodes/widgets/ui_form.html @@ -21,7 +21,7 @@ const width = v || 0 const currentGroup = $('#node-input-group').val() || this.group const groupNode = RED.nodes.node(currentGroup) - const valid = !groupNode || +width <= +groupNode.width + const valid = !groupNode || +width >= 0 $('#node-input-size').toggleClass('input-error', !valid) return valid } diff --git a/nodes/widgets/ui_gauge.html b/nodes/widgets/ui_gauge.html index b1d680672..85c49a635 100644 --- a/nodes/widgets/ui_gauge.html +++ b/nodes/widgets/ui_gauge.html @@ -129,7 +129,7 @@ const width = v || 0 const currentGroup = $('#node-input-group').val() || this.group const groupNode = RED.nodes.node(currentGroup) - const valid = !groupNode || +width <= +groupNode.width + const valid = !groupNode || +width >= 0 $('#node-input-size').toggleClass('input-error', !valid) return valid } diff --git a/nodes/widgets/ui_number_input.html b/nodes/widgets/ui_number_input.html index b7a4bc8b5..7ff748ab8 100644 --- a/nodes/widgets/ui_number_input.html +++ b/nodes/widgets/ui_number_input.html @@ -17,7 +17,7 @@ const width = v || 0 const currentGroup = $('#node-input-group').val() || this.group const groupNode = RED.nodes.node(currentGroup) - const valid = !groupNode || +width <= +groupNode.width + const valid = !groupNode || +width >= 0 $('#node-input-size').toggleClass('input-error', !valid) return valid } diff --git a/nodes/widgets/ui_radio_group.html b/nodes/widgets/ui_radio_group.html index 97a40ab36..fab4fa481 100644 --- a/nodes/widgets/ui_radio_group.html +++ b/nodes/widgets/ui_radio_group.html @@ -17,7 +17,7 @@ const width = v || 0 const currentGroup = $('#node-input-group').val() || this.group const groupNode = RED.nodes.node(currentGroup) - const valid = !groupNode || +width <= +groupNode.width + const valid = !groupNode || +width >= 0 $('#node-input-size').toggleClass('input-error', !valid) return valid } diff --git a/nodes/widgets/ui_slider.html b/nodes/widgets/ui_slider.html index 6658e9a25..db5440b17 100644 --- a/nodes/widgets/ui_slider.html +++ b/nodes/widgets/ui_slider.html @@ -18,7 +18,7 @@ const width = v || 0 const currentGroup = $('#node-input-group').val() || this.group const groupNode = RED.nodes.node(currentGroup) - const valid = !groupNode || +width <= +groupNode.width + const valid = !groupNode || +width >= 0 $('#node-input-size').toggleClass('input-error', !valid) return valid } diff --git a/nodes/widgets/ui_switch.html b/nodes/widgets/ui_switch.html index ecd9c120f..e447e8839 100644 --- a/nodes/widgets/ui_switch.html +++ b/nodes/widgets/ui_switch.html @@ -18,7 +18,7 @@ const width = v || 0 const currentGroup = $('#node-input-group').val() || this.group const groupNode = RED.nodes.node(currentGroup) - const valid = !groupNode || +width <= +groupNode.width + const valid = !groupNode || +width >= 0 $('#node-input-size').toggleClass('input-error', !valid) return valid } diff --git a/nodes/widgets/ui_table.html b/nodes/widgets/ui_table.html index 2bc40eab0..90a2858ab 100644 --- a/nodes/widgets/ui_table.html +++ b/nodes/widgets/ui_table.html @@ -72,7 +72,7 @@ const width = v || 0 const currentGroup = $('#node-input-group').val() || this.group const groupNode = RED.nodes.node(currentGroup) - const valid = !groupNode || +width <= +groupNode.width + const valid = !groupNode || +width >= 0 $('#node-input-size').toggleClass('input-error', !valid) return valid } diff --git a/nodes/widgets/ui_template.html b/nodes/widgets/ui_template.html index 058e15659..7953e274d 100644 --- a/nodes/widgets/ui_template.html +++ b/nodes/widgets/ui_template.html @@ -135,15 +135,15 @@ width: { value: 0, validate: function (v) { - let valid = true if (this.templateScope !== 'global') { const width = v || 0 const currentGroup = $('#node-input-group').val() || this.group const groupNode = RED.nodes.node(currentGroup) - valid = !groupNode || +width <= +groupNode.width + const valid = !groupNode || +width >= 0 $('#node-input-size').toggleClass('input-error', !valid) + return valid } - return valid + return true } }, height: { value: 0 }, diff --git a/nodes/widgets/ui_text.html b/nodes/widgets/ui_text.html index 55a8f2fbb..8e05630ce 100644 --- a/nodes/widgets/ui_text.html +++ b/nodes/widgets/ui_text.html @@ -83,7 +83,7 @@ const width = v || 0 const currentGroup = $('#node-input-group').val() || this.group const groupNode = RED.nodes.node(currentGroup) - const valid = !groupNode || +width <= +groupNode.width + const valid = !groupNode || +width >= 0 $('#node-input-size').toggleClass('input-error', !valid) return valid } diff --git a/nodes/widgets/ui_text_input.html b/nodes/widgets/ui_text_input.html index c010be80b..5029f5044 100644 --- a/nodes/widgets/ui_text_input.html +++ b/nodes/widgets/ui_text_input.html @@ -17,7 +17,7 @@ const width = v || 0 const currentGroup = $('#node-input-group').val() || this.group const groupNode = RED.nodes.node(currentGroup) - const valid = !groupNode || +width <= +groupNode.width + const valid = !groupNode || +width >= 0 $('#node-input-size').toggleClass('input-error', !valid) return valid } From ce4f5d64c7adfa7e9f50abf0cef4178442177b93 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Tue, 15 Oct 2024 10:52:05 +0100 Subject: [PATCH 034/109] revert url query param clean up code (fix e2e tests) --- ui/src/App.vue | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ui/src/App.vue b/ui/src/App.vue index 041a5418e..6b3aee749 100644 --- a/ui/src/App.vue +++ b/ui/src/App.vue @@ -191,10 +191,7 @@ export default { // if this is the first time we load the Dashboard, the router hasn't registered the current route properly, // so best we just navigate to the existing URL to let router catch up - // ensure we remove the edit key from the URL - const url = new URL(window.location.href) - url.searchParams.delete('edit-key') - this.$router.push(url) + this.$router.push(this.$route.fullPath) // loop over the widgets defined in Node-RED, // map their respective Vue component for rendering on a page From 529be18957e9ffcfcd340c03f9a1b7ff4fe4906c Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Tue, 15 Oct 2024 17:25:24 +0100 Subject: [PATCH 035/109] fix resize and drag pointers and placement --- ui/src/layouts/Flex.vue | 13 ++++-- ui/src/layouts/Grid.vue | 13 ++++-- ui/src/layouts/Group.vue | 83 +++++++++++++++++++++------------ ui/src/layouts/wysiwyg/index.js | 77 +++++++++++++++++------------- ui/src/stylesheets/common.css | 16 ++++++- 5 files changed, 128 insertions(+), 74 deletions(-) diff --git a/ui/src/layouts/Flex.vue b/ui/src/layouts/Flex.vue index 47dffab79..770e1c49a 100644 --- a/ui/src/layouts/Flex.vue +++ b/ui/src/layouts/Flex.vue @@ -11,9 +11,11 @@ :style="{'width': ((rowHeight * 2 * g.width) + 'px')}" :draggable="editMode" @dragstart="onDragStart($event, $index)" - @dragend="onDragEnd($event, $index)" - @dragover="onDragOver($event, $index)" - @drop="onDrop($event, $index)" + @dragend="onDragEnd($event, $index, g)" + @dragover="onDragOver($event, $index, g)" + @drop="onDrop($event, $index, g)" + @dragleave="onDragLeave($event, $index, g)" + @dragenter.prevent > From ab881dd99fa506e3040f6cb9903c6e5812e6b401 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Fri, 1 Nov 2024 23:15:01 +0000 Subject: [PATCH 068/109] improve i18n --- nodes/config/locales/en-US/ui_base.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodes/config/locales/en-US/ui_base.json b/nodes/config/locales/en-US/ui_base.json index 5a4acf83a..11279bc31 100644 --- a/nodes/config/locales/en-US/ui_base.json +++ b/nodes/config/locales/en-US/ui_base.json @@ -45,7 +45,7 @@ "collapse": "Collapse", "expand": "Expand", "delete": "Delete", - "spacer": "Spacer", + "spacer": "Add Spacer", "unattachedGroups": "Unattached Groups" }, "themes": { From eca44ed5a7f81cf5da6a3d2054f1fd2879079188 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Fri, 1 Nov 2024 23:15:23 +0000 Subject: [PATCH 069/109] init tooltip popover --- nodes/widgets/ui_spacer.html | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/nodes/widgets/ui_spacer.html b/nodes/widgets/ui_spacer.html index 8692ab2f4..c4b875497 100644 --- a/nodes/widgets/ui_spacer.html +++ b/nodes/widgets/ui_spacer.html @@ -48,6 +48,13 @@ group: '#node-input-group' }) } + // use jQuery UI tooltip to convert the plain old title attribute to a nice tooltip + $('.ui-node-popover-title').tooltip({ + show: { + effect: 'slideDown', + delay: 150 + } + }) }, oneditsave: function () { this.width = $('#node-input-width').val() From eff6fd36922c791249ce3d01f98429b53f9fa883 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Fri, 1 Nov 2024 23:15:45 +0000 Subject: [PATCH 070/109] remove sizing error now layout editor is merged --- nodes/widgets/ui_spacer.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodes/widgets/ui_spacer.html b/nodes/widgets/ui_spacer.html index c4b875497..50b969cd2 100644 --- a/nodes/widgets/ui_spacer.html +++ b/nodes/widgets/ui_spacer.html @@ -14,7 +14,7 @@ const width = v || 0 const currentGroup = $('#node-input-group').val() || this.group const groupNode = RED.nodes.node(currentGroup) - const valid = !groupNode || +width <= +groupNode.width + const valid = !groupNode || +width >= 0 $('#node-input-size').toggleClass('input-error', !valid) return valid } From 64c1a540e5ce0d5ae3766a6d641c65c5ad3dd9d7 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Fri, 1 Nov 2024 23:16:10 +0000 Subject: [PATCH 071/109] add missing quotes --- nodes/config/ui_base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodes/config/ui_base.html b/nodes/config/ui_base.html index dc5202d83..d7b79a3e6 100644 --- a/nodes/config/ui_base.html +++ b/nodes/config/ui_base.html @@ -1086,7 +1086,7 @@ } // button to edit node via node-red editor panel - const editButton = $(``).appendTo(btnGroup) + const editButton = $(``).appendTo(btnGroup) editButton.on('click', function (evt) { if (configNodes.includes(item.type)) { RED.editor.editConfig('', item.type, item.id) From fcd9968c8bcc9ade92d4e926f82d7e51e44945b2 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Fri, 1 Nov 2024 23:16:22 +0000 Subject: [PATCH 072/109] remove unused param --- nodes/config/ui_base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodes/config/ui_base.html b/nodes/config/ui_base.html index d7b79a3e6..7d879802d 100644 --- a/nodes/config/ui_base.html +++ b/nodes/config/ui_base.html @@ -1073,7 +1073,7 @@ * @param {DashboardItem} item - The page/group/widget that these actions are bound to * @param {Object} list - The editableList instance associated with this item */ - function addRowActions (parent, item, list, other) { + function addRowActions (parent, item, list) { const configNodes = ['ui-base', 'ui-page', 'ui-link', 'ui-group', 'ui-theme'] const btnGroup = $('
', { class: 'nrdb2-sb-list-header-button-group', id: item.id }).appendTo(parent) if (!configNodes.includes(item.type) && item.type !== 'ui-spacer') { From 84f9f0f7518dad1543f2f8e45f86b704c16b104a Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Fri, 1 Nov 2024 23:16:54 +0000 Subject: [PATCH 073/109] use newer node create/add pattern --- nodes/config/ui_base.html | 70 +++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/nodes/config/ui_base.html b/nodes/config/ui_base.html index 7d879802d..fe3e7d236 100644 --- a/nodes/config/ui_base.html +++ b/nodes/config/ui_base.html @@ -744,6 +744,25 @@ } return linkNode } + + function createSpacerNode ({ name, order, group } = {}) { + if (RED._db2debug) { console.log('dashboard 2: ui_base.html: createSpacerNode', name, group) } + const spacer = RED.nodes.getType('ui-spacer') + const spacerNode = { + _def: spacer, + id: RED.nodes.id(), + type: 'ui-spacer', + ...mapDefaults(spacer.defaults), + users: [], + name: name ?? 'spacer', + group: group || null, + order: order ?? 0, + hasUsers: false, + dirty: true + } + return spacerNode + } + function createPageNode ({ name, order, layout = 'grid' } = {}) { if (RED._db2debug) { console.log('dashboard 2: ui_base.html: createPageNode', name, layout) } const page = RED.nodes.getType('ui-page') @@ -1160,39 +1179,24 @@ list.editableList('addItem') evt.preventDefault() }) - // if this is a group & it is not an unattached group, add the "_ spacer" button - if (item.type === 'ui-group' && !!item.page) { - // add the "+ spacer" button - $(' ' + c_('layout.spacer') + '') - .click(function (evt) { - // TODO: refactor this into createSpacer when #1401 is merged', parent, list - const spacer = RED.nodes.getType('ui-spacer') - const spacerNode = { - _def: spacer, - id: RED.nodes.id(), - type: 'ui-spacer', - ...mapDefaults(spacer.defaults), - users: [], - name: 'spacer', - order: list.editableList('items').length + 1, - group: item.id - } - spacerNode.dirty = true - const n = RED.nodes.add(spacerNode) - RED.editor.validateNode(spacerNode) - RED.history.push({ - t: 'add', - nodes: [spacerNode.id], - dirty: RED.nodes.dirty() - }) - RED.nodes.dirty(true) - const widget = toDashboardItem(spacerNode) - list.editableList('addItem', widget) - RED.editor.edit(n) - evt.preventDefault() - }) - .appendTo(btnGroup) - } + } + + // if this is a group & it is not an unattached group, add the "_ spacer" button + if (item.type === 'ui-group' && !!item.page) { + // add the "+ spacer" button + $(``) + .on(function (evt) { + const spacerNode = createSpacerNode({ name: 'spacer', group: item.id, order: list.editableList('items').length + 1 }) + const history = [] + addNode(spacerNode, history) + assignGroup(spacerNode, item.node, history) + recordEvents(history) + evt.preventDefault() + const widget = toDashboardItem(spacerNode) + list.editableList('addItem', widget) + RED.editor.edit(widget.node) + }) + .appendTo(btnGroup) } } From 4ff3aae3777b0fe5733f40677171cb054300f71b Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Fri, 1 Nov 2024 23:37:15 +0000 Subject: [PATCH 074/109] fix add spacer button --- nodes/config/ui_base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodes/config/ui_base.html b/nodes/config/ui_base.html index fe3e7d236..fbb039a05 100644 --- a/nodes/config/ui_base.html +++ b/nodes/config/ui_base.html @@ -1185,7 +1185,7 @@ if (item.type === 'ui-group' && !!item.page) { // add the "+ spacer" button $(``) - .on(function (evt) { + .on('click', function (evt) { const spacerNode = createSpacerNode({ name: 'spacer', group: item.id, order: list.editableList('items').length + 1 }) const history = [] addNode(spacerNode, history) From 2eef950bc70fbbef14d908de46e10a2d607a6d7b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 10:20:43 +0000 Subject: [PATCH 075/109] Bump flowfuse/github-actions-workflows from 0.36.0 to 0.37.0 Bumps [flowfuse/github-actions-workflows](https://github.com/flowfuse/github-actions-workflows) from 0.36.0 to 0.37.0. - [Commits](https://github.com/flowfuse/github-actions-workflows/compare/v0.36.0...v0.37.0) --- updated-dependencies: - dependency-name: flowfuse/github-actions-workflows dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/build-and-test.yml | 2 +- .github/workflows/publish-private.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index c5503650f..50f7e8e25 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -3,7 +3,7 @@ on: pull_request: jobs: build: - uses: 'flowfuse/github-actions-workflows/.github/workflows/build_node_package.yml@v0.36.0' + uses: 'flowfuse/github-actions-workflows/.github/workflows/build_node_package.yml@v0.37.0' with: node: '[ {"version": "18", "tests": true, "lint": true}, diff --git a/.github/workflows/publish-private.yml b/.github/workflows/publish-private.yml index ded82489e..11702cc93 100644 --- a/.github/workflows/publish-private.yml +++ b/.github/workflows/publish-private.yml @@ -6,7 +6,7 @@ on: jobs: build: - uses: 'flowfuse/github-actions-workflows/.github/workflows/build_node_package.yml@v0.36.0' + uses: 'flowfuse/github-actions-workflows/.github/workflows/build_node_package.yml@v0.37.0' with: node: '[ {"version": "18", "tests": true, "lint": true}, @@ -16,7 +16,7 @@ jobs: needs: build if: | github.ref == 'refs/heads/main' - uses: 'flowfuse/github-actions-workflows/.github/workflows/publish_node_package.yml@v0.36.0' + uses: 'flowfuse/github-actions-workflows/.github/workflows/publish_node_package.yml@v0.37.0' with: package_name: node-red-dashboard publish_package: true From b7a0341ec2df0d639eb46c3c9e6495a521fa6f78 Mon Sep 17 00:00:00 2001 From: Joe Pavitt Date: Mon, 4 Nov 2024 11:17:24 +0000 Subject: [PATCH 076/109] WYSIWYG: Tidy up styling/positioning of the Edit Controls --- ui/src/components/ConfirmDialog.vue | 4 +-- ui/src/layouts/Baseline.vue | 8 +----- ui/src/layouts/wysiwyg/EditControls.vue | 34 ++++++++++++++++++------- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/ui/src/components/ConfirmDialog.vue b/ui/src/components/ConfirmDialog.vue index 25f43d798..04a788219 100644 --- a/ui/src/components/ConfirmDialog.vue +++ b/ui/src/components/ConfirmDialog.vue @@ -2,8 +2,8 @@ - {{ cancelButton }} - {{ okButton }} + {{ cancelButton }} + {{ okButton }} diff --git a/ui/src/layouts/Baseline.vue b/ui/src/layouts/Baseline.vue index 220089737..280dbb6cb 100644 --- a/ui/src/layouts/Baseline.vue +++ b/ui/src/layouts/Baseline.vue @@ -6,9 +6,7 @@
@@ -193,10 +191,6 @@ export default { return this.orderedPages.find(p => p.id === editPage.value) } return null - }, - showEditingIconInAppBar: function () { - // show the edit icon in the app bar if the page is in edit mode and the drawer is closed - return !!(this.currentEditPage && (this.drawer === false || this.rail === true)) } }, watch: { diff --git a/ui/src/layouts/wysiwyg/EditControls.vue b/ui/src/layouts/wysiwyg/EditControls.vue index aa71f1eb3..21c2868db 100644 --- a/ui/src/layouts/wysiwyg/EditControls.vue +++ b/ui/src/layouts/wysiwyg/EditControls.vue @@ -1,10 +1,11 @@ @@ -39,20 +40,26 @@ export default { From 0088af4e38c48084d4a7d1dccdff69457b94326a Mon Sep 17 00:00:00 2001 From: Joe Pavitt Date: Mon, 4 Nov 2024 11:41:41 +0000 Subject: [PATCH 077/109] ~WYSIWYG: Fix edit controls taking full width fo the page --- ui/src/layouts/Grid.vue | 5 ----- ui/src/layouts/wysiwyg/EditControls.vue | 4 ++++ 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/ui/src/layouts/Grid.vue b/ui/src/layouts/Grid.vue index ca23763ac..9e3efc1da 100644 --- a/ui/src/layouts/Grid.vue +++ b/ui/src/layouts/Grid.vue @@ -239,11 +239,6 @@ export default { gap: var(--group-gap); } -.nrdb-layout--grid > div { - width: 100%; - /* max-width: 100%; */ -} - .v-card { width: 100%; } diff --git a/ui/src/layouts/wysiwyg/EditControls.vue b/ui/src/layouts/wysiwyg/EditControls.vue index 21c2868db..b3e9c014b 100644 --- a/ui/src/layouts/wysiwyg/EditControls.vue +++ b/ui/src/layouts/wysiwyg/EditControls.vue @@ -49,6 +49,10 @@ export default { flex-direction: column; align-items: center; --shadow: 0px 0px 5px #000000de; + width: fit-content; + left: 50%; + right: 50%; + transform: translate(-50%); } .nrdb-ui-editor-tray { background-color: white; From df7609f952c89133b9e3485fcec9dbe81bf19c93 Mon Sep 17 00:00:00 2001 From: Joe Pavitt Date: Mon, 4 Nov 2024 11:47:28 +0000 Subject: [PATCH 078/109] Remov unnecessary left declaration --- ui/src/layouts/wysiwyg/EditControls.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/src/layouts/wysiwyg/EditControls.vue b/ui/src/layouts/wysiwyg/EditControls.vue index b3e9c014b..3dba92489 100644 --- a/ui/src/layouts/wysiwyg/EditControls.vue +++ b/ui/src/layouts/wysiwyg/EditControls.vue @@ -41,7 +41,6 @@ export default { .nrdb-ui-editor-tray-container { position: fixed; top: 0; - left: 0; z-index: 1000; width: 100%; display: flex; From 01cfafe8478736c6460d49bf7bf6563caf642ba0 Mon Sep 17 00:00:00 2001 From: Joe Pavitt Date: Mon, 4 Nov 2024 11:48:01 +0000 Subject: [PATCH 079/109] Remove unnecessary width declaration --- ui/src/layouts/wysiwyg/EditControls.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/src/layouts/wysiwyg/EditControls.vue b/ui/src/layouts/wysiwyg/EditControls.vue index 3dba92489..12d32b718 100644 --- a/ui/src/layouts/wysiwyg/EditControls.vue +++ b/ui/src/layouts/wysiwyg/EditControls.vue @@ -42,7 +42,6 @@ export default { position: fixed; top: 0; z-index: 1000; - width: 100%; display: flex; justify-content: center; flex-direction: column; From 663a479452927555f6b8982e95f4beb010fa64d6 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 4 Nov 2024 11:54:36 +0000 Subject: [PATCH 080/109] remove Editor as a layout choice --- nodes/config/ui_page.html | 1 - nodes/widgets/ui_control.js | 14 ++++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/nodes/config/ui_page.html b/nodes/config/ui_page.html index b4bdff3a9..8d8fbbfb7 100644 --- a/nodes/config/ui_page.html +++ b/nodes/config/ui_page.html @@ -97,7 +97,6 @@ types: [{ value: 'layout', options: [ - { value: 'wysiwyg', label: RED._('@flowfuse/node-red-dashboard/ui-page:ui-page.label.wysiwyg') }, { value: 'grid', label: RED._('@flowfuse/node-red-dashboard/ui-page:ui-page.label.grid') }, { value: 'flex', label: RED._('@flowfuse/node-red-dashboard/ui-page:ui-page.label.fixed') }, { value: 'tabs', label: RED._('@flowfuse/node-red-dashboard/ui-page:ui-page.label.tabs') }, diff --git a/nodes/widgets/ui_control.js b/nodes/widgets/ui_control.js index a62d08df4..1f068b80a 100644 --- a/nodes/widgets/ui_control.js +++ b/nodes/widgets/ui_control.js @@ -177,13 +177,15 @@ module.exports = function (RED) { // this message was sent by this particular node if (evt === 'change') { const wNode = RED.nodes.getNode(node.id) - let msg = { - payload: 'change', - tab: payload.page, // index of tab - name: payload.name // page name + if (wNode && payload && wNode.send) { + let msg = { + payload: 'change', + tab: payload.page, // index of tab + name: payload.name // page name + } + msg = addConnectionCredentials(RED, msg, conn, ui) + wNode.send(msg) } - msg = addConnectionCredentials(RED, msg, conn, ui) - wNode.send(msg) } } } From 0a0970a7378958f4f7c9e8dc105ddd0fa0d8753c Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 4 Nov 2024 12:45:30 +0000 Subject: [PATCH 081/109] remove temp dev i18n --- nodes/config/locales/en-US/ui_page.json | 1 - 1 file changed, 1 deletion(-) diff --git a/nodes/config/locales/en-US/ui_page.json b/nodes/config/locales/en-US/ui_page.json index 0465ae0bb..fed5b28f8 100644 --- a/nodes/config/locales/en-US/ui_page.json +++ b/nodes/config/locales/en-US/ui_page.json @@ -7,7 +7,6 @@ "icon": "Icon", "theme": "Theme", "layout": "Layout", - "wysiwyg": "Editor", "grid": "Grid", "fixed": "Fixed", "tabs": "Tabs", From fc3bde91febb050d079f81f6f8f222658929ceb1 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 4 Nov 2024 13:32:59 +0000 Subject: [PATCH 082/109] fix edit revert --- ui/src/layouts/Flex.vue | 3 ++- ui/src/layouts/Grid.vue | 3 ++- ui/src/layouts/wysiwyg/index.js | 16 +++++++++++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/ui/src/layouts/Flex.vue b/ui/src/layouts/Flex.vue index 0b299f3bd..2d4b4e954 100644 --- a/ui/src/layouts/Flex.vue +++ b/ui/src/layouts/Flex.vue @@ -109,8 +109,8 @@ export default { } }, mounted () { - this.pageGroups = this.getPageGroups() if (this.editMode) { // mixin property + this.pageGroups = this.getPageGroups() this.initializeEditTracking() // Mixin method } }, @@ -197,6 +197,7 @@ export default { }, discardEdits () { this.revertEdits() // Mixin method + this.pageGroups = this.getPageGroups() }, async leaveEditMode () { let leave = true diff --git a/ui/src/layouts/Grid.vue b/ui/src/layouts/Grid.vue index 9e3efc1da..e6afc56ad 100644 --- a/ui/src/layouts/Grid.vue +++ b/ui/src/layouts/Grid.vue @@ -109,8 +109,8 @@ export default { } }, mounted () { - this.pageGroups = this.getPageGroups() if (this.editMode) { // mixin property + this.pageGroups = this.getPageGroups() this.initializeEditTracking() // Mixin method } }, @@ -197,6 +197,7 @@ export default { }, discardEdits () { this.revertEdits() // Mixin method + this.pageGroups = this.getPageGroups() }, async leaveEditMode () { let leave = true diff --git a/ui/src/layouts/wysiwyg/index.js b/ui/src/layouts/wysiwyg/index.js index 0485aabf3..ace1421f3 100644 --- a/ui/src/layouts/wysiwyg/index.js +++ b/ui/src/layouts/wysiwyg/index.js @@ -41,7 +41,21 @@ export default { exitEditMode() // EditTracking method }, revertEdits () { - this.pageGroups = JSON.parse(JSON.stringify(originalGroups.value)) + const originals = originalGroups.value || [] + // scan through each group and revert changes + const propertiesOfInterest = ['width', 'height', 'order'] + originals.forEach((originalGroup, index) => { + const pageGroup = this.pageGroups?.find(group => group.id === originalGroup.id) + if (!pageGroup) { + console.warn('Group not found in pageGroups - as we do not currently support adding/removing groups, this should not happen!') + return + } + propertiesOfInterest.forEach(property => { + if (originalGroup[property] !== pageGroup[property]) { + pageGroup[property] = originalGroup[property] + } + }) + }) }, deployChanges ({ dashboard, page, groups }) { return NodeRedApi.deployChanges({ dashboard, page, groups, key: editKey.value, editorPath: editorPath.value }) From de76e2ea605808b39ad865371d260aa87e36990e Mon Sep 17 00:00:00 2001 From: Joe Pavitt Date: Mon, 4 Nov 2024 15:19:44 +0000 Subject: [PATCH 083/109] Fix column sizing issue for widgets --- ui/src/layouts/Group.vue | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ui/src/layouts/Group.vue b/ui/src/layouts/Group.vue index a7972c9a4..d0aca8a0a 100644 --- a/ui/src/layouts/Group.vue +++ b/ui/src/layouts/Group.vue @@ -7,7 +7,7 @@ class="nrdb-ui-widget" :class="getWidgetClass(w)" style="display: grid" - :style="`grid-template-columns: minmax(0, 1fr); grid-template-rows: repeat(${w.props.height}, minmax(var(--widget-row-height), auto)); grid-row-end: span ${w.props.height}; grid-column-end: span min(${ w.props.width || columns }, var(--layout-columns))`" + :style="`grid-template-columns: minmax(0, 1fr); grid-template-rows: repeat(${w.props.height}, minmax(var(--widget-row-height), auto)); grid-row-end: span ${w.props.height}; grid-column-end: span min(${ getWidgetWidth(w.props.width) }, var(--layout-columns))`" >
@@ -70,6 +70,13 @@ export default { classes.push(widget.state.class) } return classes.join(' ') + }, + getWidgetWidth (width) { + if (width) { + return Math.min(width, this.columns) + } else { + return this.columns + } } } } From 29c52a9f0650c8b938b115e58732f43b7899b339 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 4 Nov 2024 15:47:57 +0000 Subject: [PATCH 084/109] persist name --- nodes/widgets/ui_spacer.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nodes/widgets/ui_spacer.html b/nodes/widgets/ui_spacer.html index 50b969cd2..dd9d4f02a 100644 --- a/nodes/widgets/ui_spacer.html +++ b/nodes/widgets/ui_spacer.html @@ -27,7 +27,7 @@ hasUsers: false, icon: 'font-awesome/fa-arrows-h', paletteLabel: 'spacer', - label: function () { return this.name + ' ' + this.width + 'x' + this.height }, + label: function () { return `${this.name || 'Spacer'} ${this.width} x ${this.height}` }, labelStyle: function () { return this.name ? 'node_label_italic' : '' }, oneditprepare: function () { // if this groups parent is a subflow template, set the node-config-input-width and node-config-input-height up @@ -57,6 +57,7 @@ }) }, oneditsave: function () { + this.name = $('#node-input-name').val() this.width = $('#node-input-width').val() this.height = $('#node-input-height').val() this.className = $('#node-input-className').val() From b2d7e2adff60a1655eecb2e3f8ad995d7e108284 Mon Sep 17 00:00:00 2001 From: Joe Pavitt Date: Mon, 4 Nov 2024 16:34:07 +0000 Subject: [PATCH 085/109] Add edit-mode styling for the spacers --- ui/src/layouts/Baseline.vue | 5 ++++- ui/src/widgets/ui-spacer/UISpacer.vue | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/ui/src/layouts/Baseline.vue b/ui/src/layouts/Baseline.vue index 280dbb6cb..6d0a17879 100644 --- a/ui/src/layouts/Baseline.vue +++ b/ui/src/layouts/Baseline.vue @@ -15,7 +15,7 @@ - + p.id === editPage.value) } return null + }, + editMode: function () { + return editMode.value } }, watch: { diff --git a/ui/src/widgets/ui-spacer/UISpacer.vue b/ui/src/widgets/ui-spacer/UISpacer.vue index db016029a..38ee36920 100644 --- a/ui/src/widgets/ui-spacer/UISpacer.vue +++ b/ui/src/widgets/ui-spacer/UISpacer.vue @@ -17,3 +17,21 @@ export default { } } + + From 07f379789456e179d5984f523249eef0290a7739 Mon Sep 17 00:00:00 2001 From: Joe Pavitt Date: Mon, 4 Nov 2024 16:34:21 +0000 Subject: [PATCH 086/109] Remove odd that was adding an extra row to every widget --- ui/src/layouts/Group.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/src/layouts/Group.vue b/ui/src/layouts/Group.vue index a7972c9a4..238d9d0e8 100644 --- a/ui/src/layouts/Group.vue +++ b/ui/src/layouts/Group.vue @@ -25,7 +25,6 @@ @dragenter.prevent />
-
From 9bdabe32943a1f3cc562038b024c672c2e499156 Mon Sep 17 00:00:00 2001 From: Joe Pavitt Date: Mon, 4 Nov 2024 16:43:32 +0000 Subject: [PATCH 087/109] Include spacer dimensions in the holding text --- ui/src/widgets/ui-spacer/UISpacer.vue | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ui/src/widgets/ui-spacer/UISpacer.vue b/ui/src/widgets/ui-spacer/UISpacer.vue index 38ee36920..53903c480 100644 --- a/ui/src/widgets/ui-spacer/UISpacer.vue +++ b/ui/src/widgets/ui-spacer/UISpacer.vue @@ -12,6 +12,11 @@ export default { props: { type: Object, default: () => ({}) }, state: { type: Object, default: () => ({}) } }, + computed: { + placeholderText () { + return `"spacer (${this.props.width}x${this.props.height})"` + } + }, created () { this.$socket.emit('widget-load', this.id) } @@ -26,7 +31,7 @@ export default { } .nrdb-edit-mode .nrdb-spacer:before { - content: "spacer"; + content: v-bind(placeholderText); font-style: italic; display: flex; justify-content: center; From d5e6613c67ff5eaae67e0beb13a70a8c3c90c423 Mon Sep 17 00:00:00 2001 From: Stephen McLaughlin <44235289+Steve-Mcl@users.noreply.github.com> Date: Mon, 4 Nov 2024 16:56:25 +0000 Subject: [PATCH 088/109] Update nodes/widgets/ui_spacer.html --- nodes/widgets/ui_spacer.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodes/widgets/ui_spacer.html b/nodes/widgets/ui_spacer.html index dd9d4f02a..22c73f5ac 100644 --- a/nodes/widgets/ui_spacer.html +++ b/nodes/widgets/ui_spacer.html @@ -27,7 +27,7 @@ hasUsers: false, icon: 'font-awesome/fa-arrows-h', paletteLabel: 'spacer', - label: function () { return `${this.name || 'Spacer'} ${this.width} x ${this.height}` }, + label: function () { return `${this.name || 'spacer'} (${this.width}x${this.height})` }, labelStyle: function () { return this.name ? 'node_label_italic' : '' }, oneditprepare: function () { // if this groups parent is a subflow template, set the node-config-input-width and node-config-input-height up From 9d3f123e051d0aecefe48a946da9415f91620bfd Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 4 Nov 2024 21:03:35 +0000 Subject: [PATCH 089/109] Bump for release 1.19.1 --- package-lock.json | 4 ++-- package.json | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 21d616a09..15a70f9c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@flowfuse/node-red-dashboard", - "version": "1.18.1", + "version": "1.19.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@flowfuse/node-red-dashboard", - "version": "1.18.1", + "version": "1.19.0", "license": "Apache-2.0", "dependencies": { "acorn": "^8.11.2", diff --git a/package.json b/package.json index 58dfb40e0..0eff9d197 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@flowfuse/node-red-dashboard", - "version": "1.18.1", + "version": "1.19.0", "description": "Dashboard 2.0 - A collection of Node-RED nodes that provide functionality to build your own UI applications (inc. forms, buttons, charts).", "keywords": [ "node-red" @@ -20,6 +20,10 @@ "name": "Pez Cuckow", "url": "https://github.com/pezmc" }, + { + "name": "Steve McLaughlin", + "url": "https://github.com/Steve-Mcl" + }, { "name": "FlowFuse", "url": "https://flowfuse.com" From d61d028dbdd3f50e0a916f7a1dcde475857416d5 Mon Sep 17 00:00:00 2001 From: cgjgh <160297365+cgjgh@users.noreply.github.com> Date: Mon, 4 Nov 2024 15:45:53 -0600 Subject: [PATCH 090/109] Fix notification docs for msg.show --- docs/nodes/widgets/ui-notification.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/nodes/widgets/ui-notification.md b/docs/nodes/widgets/ui-notification.md index db03921a1..b37002458 100644 --- a/docs/nodes/widgets/ui-notification.md +++ b/docs/nodes/widgets/ui-notification.md @@ -43,12 +43,13 @@ dynamic: Accept raw html: payload: msg.ui_update.raw structure: ["Boolean"] - Show: - payload: msg.ui_update.show - structure: ["Boolean"] Show countdown bar: payload: msg.ui_update.showCountdown structure: ["Boolean"] +controls: + show: + example: true | false + description: Allow control over whether or not the notification is visible. --- From 44d11bed33f5a43e8dd05285a7823edbf7358e9c Mon Sep 17 00:00:00 2001 From: cgjgh <160297365+cgjgh@users.noreply.github.com> Date: Mon, 4 Nov 2024 21:11:11 -0600 Subject: [PATCH 105/109] Allow specifying target for opening external URLs in new tab --- ui/src/widgets/ui-control/UIControl.vue | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/ui/src/widgets/ui-control/UIControl.vue b/ui/src/widgets/ui-control/UIControl.vue index b435c7622..d2b03f090 100644 --- a/ui/src/widgets/ui-control/UIControl.vue +++ b/ui/src/widgets/ui-control/UIControl.vue @@ -216,8 +216,13 @@ export default { } if ('url' in payload) { - // we are setting the url - window.location.href = payload.url + if ('target' in payload) { + // Open the link in a new browser window or tab + window.open(payload.url, payload.target) + } else { + // Open the link in the same window + window.location.href = payload.url + } } }) }, From b2f1f0a05187986a9f2b909181cacd2f07846408 Mon Sep 17 00:00:00 2001 From: cgjgh <160297365+cgjgh@users.noreply.github.com> Date: Mon, 4 Nov 2024 21:12:35 -0600 Subject: [PATCH 106/109] Update ui-control docs for specifying target --- docs/nodes/widgets/ui-control.md | 9 +++++++++ nodes/widgets/locales/en-US/ui_control.html | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/docs/nodes/widgets/ui-control.md b/docs/nodes/widgets/ui-control.md index 0e6e28efd..f832ade7f 100644 --- a/docs/nodes/widgets/ui-control.md +++ b/docs/nodes/widgets/ui-control.md @@ -94,6 +94,15 @@ If you want to trigger navigation to an external resource or website, you can do msg.payload = { url: 'https://nodered.org' } +``` + + You can also specify a `target` property to open the website in a new browser window or tab. + +```js +msg.payload = { + url: 'https://nodered.org', + target: '_blank' +} ``` ### Show/Hide diff --git a/nodes/widgets/locales/en-US/ui_control.html b/nodes/widgets/locales/en-US/ui_control.html index 13d50cc8b..fe435bf2e 100644 --- a/nodes/widgets/locales/en-US/ui_control.html +++ b/nodes/widgets/locales/en-US/ui_control.html @@ -48,6 +48,11 @@

External URL

It is possible to trigger navigation to an external site, away from your Dashboard by passing in a URL string.

msg.payload = {
     url: 'https://url.goes.here'
+}
+

You can also specify a target to open the link in a new browser window or tab.

+
msg.payload = {
+    url: 'https://url.goes.here',
+    target: '_blank'
 }

Events List

When any browser client connects or loses connection, changes tab, or expands or collapses a group this node will emit a msg containing:

From 59311ab780e52768c743492b95733cce42ef0db8 Mon Sep 17 00:00:00 2001 From: cgjgh <160297365+cgjgh@users.noreply.github.com> Date: Mon, 4 Nov 2024 21:13:10 -0600 Subject: [PATCH 107/109] Fix ui-control placeholder name in config --- nodes/widgets/ui_control.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nodes/widgets/ui_control.html b/nodes/widgets/ui_control.html index 99763559b..8e4105249 100644 --- a/nodes/widgets/ui_control.html +++ b/nodes/widgets/ui_control.html @@ -36,8 +36,8 @@