From 266c7baf805c3489098444f03ba8afa28b381378 Mon Sep 17 00:00:00 2001 From: Joe Pavitt Date: Fri, 10 May 2024 14:53:01 +0100 Subject: [PATCH 1/3] UI Link - Add new config widget to allow links in navigation --- nodes/config/locales/en-US/ui_base.json | 1 + nodes/config/locales/en-US/ui_link.json | 17 +++ nodes/config/ui_base.html | 136 +++++++++++++++++------- nodes/config/ui_base.js | 4 +- nodes/config/ui_link.html | 108 +++++++++++++++++++ nodes/config/ui_link.js | 63 +++++++++++ nodes/config/ui_page.html | 4 +- package.json | 1 + ui/src/layouts/Baseline.vue | 3 +- 9 files changed, 295 insertions(+), 42 deletions(-) create mode 100644 nodes/config/locales/en-US/ui_link.json create mode 100644 nodes/config/ui_link.html create mode 100644 nodes/config/ui_link.js diff --git a/nodes/config/locales/en-US/ui_base.json b/nodes/config/locales/en-US/ui_base.json index b03ead95..a1591efd 100644 --- a/nodes/config/locales/en-US/ui_base.json +++ b/nodes/config/locales/en-US/ui_base.json @@ -28,6 +28,7 @@ "layout": { "pages": "Pages", "page": "Page", + "link": "Link", "group": "Group", "edit": "Edit", "focus": "Focus", diff --git a/nodes/config/locales/en-US/ui_link.json b/nodes/config/locales/en-US/ui_link.json new file mode 100644 index 00000000..80090f5f --- /dev/null +++ b/nodes/config/locales/en-US/ui_link.json @@ -0,0 +1,17 @@ +{ + "ui-link": { + "label": { + "linkName": "Link", + "ui": "UI", + "path": "Path", + "icon": "Icon", + "defaultState": "Default State", + "visibility": "Visibility", + "visible": "Visible", + "hidden": "Hidden", + "interactivity": "Interactivity", + "active": "Active", + "disabled": "Disabled" + } + } +} \ No newline at end of file diff --git a/nodes/config/ui_base.html b/nodes/config/ui_base.html index 70eb345b..742505e1 100644 --- a/nodes/config/ui_base.html +++ b/nodes/config/ui_base.html @@ -179,7 +179,7 @@ /** * @typedef {Object} DashboardItem - A widget/group/page/subflow item - * @property {String} itemType - The type of item (e.g. 'widget', 'group', 'page') + * @property {String} itemType - The type of item (e.g. 'widget', 'group', 'page', 'link') * @property {String} id - The unique id of the item * @property {String} name - The name of the item * @property {String} type - The type of the item (e.g. 'ui-button', 'ui-template', 'ui-group', 'ui-page') @@ -283,6 +283,7 @@ } if (hasProperty(node, 'group')) { item.group = node.group } if (hasProperty(node, 'page')) { item.page = node.page } + if (hasProperty(node, 'link')) { item.link = node.link } if (hasProperty(node, 'theme')) { item.theme = node.theme } if (hasProperty(node, 'env') && Array.isArray(node.env) && /subflow:.+/.test(node.type)) { const envOrder = node.env.find(e => e.key === 'DB2_SF_ORDER') @@ -292,6 +293,7 @@ } switch (node.type) { case 'ui-page': + case 'ui-link': case 'ui-group': case 'ui-theme': case 'ui-base': @@ -517,6 +519,22 @@ return pageNode } + function addDefaultLink (baseId) { + const link = RED.nodes.getType('ui-link') + const linkNode = { + _def: link, + id: RED.nodes.id(), + type: 'ui-link', + ...mapDefaults(link.defaults), + path: '/', + name: 'Link', + ui: baseId + } + + addConfigNode(linkNode) + return linkNode + } + function addDefaultGroup (pageId) { const group = RED.nodes.getType('ui-group') const groupNode = { @@ -672,7 +690,7 @@ * @param {DashboardItem} item - The page/group/widget that these actions are bound to */ function addRowActions (parent, item, list) { - const configNodes = ['ui-base', 'ui-page', 'ui-group', 'ui-theme'] + 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) @@ -710,7 +728,7 @@ */ function addRowStateOptions (parent, dashboardItem) { const item = dashboardItem.node - const nodes = ['ui-page', 'ui-group'] + const nodes = ['ui-page', 'ui-link', 'ui-group'] const btnGroup = $('
', { class: 'nrdb2-sb-list-header-state-options', id: item.id }).appendTo(parent) if (nodes.includes(item.type)) { const visibleIcon = (item.visible === 'false' || item.visible === false) ? 'fa-eye-slash' : 'fa-eye' @@ -1021,6 +1039,14 @@ .appendTo(buttonGroup) RED.popover.tooltip(buttonExpand, c_('layout.expand')) + // add link button + $(' ' + c_('layout.link') + '') + .click(function (evt) { + pagesOL.editableList('addItem', { type: 'ui-link' }) + evt.preventDefault() + }) + .appendTo(buttonGroup) + // add page button $(' ' + c_('layout.page') + '') .click(function (evt) { @@ -1033,6 +1059,7 @@ /** @type {DashboardItemLookup} */ const pages = {} + const links = {} /** @type {DashboardItemLookup} */ const groupsByPage = {} const unattachedGroups = [] @@ -1043,8 +1070,10 @@ RED.nodes.eachConfig(function (n) { if (n.type === 'ui-page' && !!n.ui) { pages[n.id] = toDashboardItem(n) - } - if (n.type === 'ui-group') { + } else if (n.type === 'ui-link') { + console.log('link', n) + links[n.id] = toDashboardItem(n) + } else if (n.type === 'ui-group') { const p = n.page if (!p) { unattachedGroups.push(toDashboardItem(n)) @@ -1116,35 +1145,61 @@ const pagesOL = $('
    ', { class: 'nrdb2-sb-pages-list' }).appendTo(divTabs).editableList({ sortable: '.nrdb2-sb-pages-list-header', addButton: false, - addItem: function (container, i, page) { - if (!page || !page.id) { - // this is a new page that's been added and we need to setup the basics - page = addDefaultPage() - RED.editor.editConfig('', page.type, page.id) - } - const groups = groupsByPage[page.id] || [] + addItem: function (container, i, item) { + if (item && item.type === 'ui-link') { + // want to create a new link + if (!item || !item.id) { + // create a default link + item = addDefaultLink() + RED.editor.editConfig('', item.type, item.id) + } + // add it to the list of pages/links + container.addClass('nrdb2-sb-pages-list-item') + const titleRow = $('
    ', { class: 'nrdb2-sb-list-header nrdb2-sb-pages-list-header' }).appendTo(container) - container.addClass('nrdb2-sb-pages-list-item') + // build title row + $('').appendTo(titleRow) + const linkIcon = 'fa-link' + $('', { class: 'nrdb2-sb-icon nrdb2-sb-tab-icon fa ' + linkIcon }).appendTo(titleRow) + $('', { class: 'nrdb2-sb-title' }).text(item.name || item.id).appendTo(titleRow) - const titleRow = $('
    ', { class: 'nrdb2-sb-list-header nrdb2-sb-pages-list-header' }).appendTo(container) - const groupsList = $('
    ', { class: 'nrdb2-sb-group-list-container' }).appendTo(container) + // link - actions + const actions = $('
    ', { class: 'nrdb2-sb-list-header-actions' }).appendTo(titleRow) + // add "Edit" and "Focus" buttons + addRowActions(actions, item) + // Add visibility/disabled options + addRowStateOptions(actions, item) + } else { + // is a page, with groups and widgets inside + if (!item || !item.id) { + // this is a new page that's been added and we need to setup the basics + item = addDefaultPage() + RED.editor.editConfig('', item.type, item.id) + } + const groups = groupsByPage[item.id] || [] - // build title row - $('').appendTo(titleRow) - const chevron = $('', { style: 'width:10px;' }).appendTo(titleRow) - const tabicon = 'fa-object-group' - $('', { class: 'nrdb2-sb-icon nrdb2-sb-tab-icon fa ' + tabicon }).appendTo(titleRow) - $('', { class: 'nrdb2-sb-title' }).text(page.name || page.id).appendTo(titleRow) - $('', { class: 'nrdb2-sb-info' }).text(`${groups.length} Groups`).appendTo(titleRow) + container.addClass('nrdb2-sb-pages-list-item') - // adds groups within this page - titleRow.click(titleToggle(page.id, groupsList, chevron)) - const groupsOL = addGroupOrderingList(page.id, groupsList, groups, widgetsByGroup) + const titleRow = $('
    ', { class: 'nrdb2-sb-list-header nrdb2-sb-pages-list-header' }).appendTo(container) + const groupsList = $('
    ', { class: 'nrdb2-sb-group-list-container' }).appendTo(container) - // page - actions - const actions = $('
    ', { class: 'nrdb2-sb-list-header-actions' }).appendTo(titleRow) - addRowActions(actions, page, groupsOL) - addRowStateOptions(actions, page) + // build title row + $('').appendTo(titleRow) + const chevron = $('', { style: 'width:10px;' }).appendTo(titleRow) + const tabicon = 'fa-object-group' + $('', { class: 'nrdb2-sb-icon nrdb2-sb-tab-icon fa ' + tabicon }).appendTo(titleRow) + $('', { class: 'nrdb2-sb-title' }).text(item.name || item.id).appendTo(titleRow) + $('', { class: 'nrdb2-sb-info' }).text(`${groups.length} Groups`).appendTo(titleRow) + + // adds groups within this page + titleRow.click(titleToggle(item.id, groupsList, chevron)) + const groupsOL = addGroupOrderingList(item.id, groupsList, groups, widgetsByGroup) + + // page - actions + const actions = $('
    ', { class: 'nrdb2-sb-list-header-actions' }).appendTo(titleRow) + addRowActions(actions, item, groupsOL) + addRowStateOptions(actions, item) + } }, sortItems: function (items) { // track any changes @@ -1159,15 +1214,22 @@ } }) - Object.values(pages).sort((a, b) => a.order - b.order).forEach(function (page) { - const groups = groupsByPage[page.id] || [] - if (RED._db2debug) { console.log('dashboard 2: ui_base.html: buildLayoutOrderEditor: adding groups', groups) } - if (page) { - pagesOL.editableList('addItem', page) - } - // groups.forEach(() => { + const items = { + ...pages, + ...links + } - // }) + console.log(pages, links, items) + + Object.values(items).sort((a, b) => a.order - b.order).forEach(function (item) { + let groups = [] + if (item.type === 'ui-page' && item.id) { + if (RED._db2debug) { console.log('dashboard 2: ui_base.html: buildLayoutOrderEditor: adding groups', groups) } + groups = groupsByPage[item.id] || [] + } + if (item) { + pagesOL.editableList('addItem', item) + } }) // add Unattached Groups to the bottom diff --git a/nodes/config/ui_base.js b/nodes/config/ui_base.js index 0129c40b..e32de057 100644 --- a/nodes/config/ui_base.js +++ b/nodes/config/ui_base.js @@ -231,7 +231,7 @@ module.exports = function (RED) { const pageNodes = [] const themes = [] RED.nodes.eachNode(n => { - if (n.type === 'ui-page') { + if (n.type === 'ui-page' || n.type === 'ui-link') { pageNodes.push(n) } else if (n.type === 'ui-base' && n.id !== node.id) { baseNodes.push(n) @@ -880,7 +880,7 @@ module.exports = function (RED) { // map pages by their ID if (page && !node.ui.pages.has(page?.id)) { - const { _user, type, ...p } = page + const { _users, ...p } = page node.ui.pages.set(page.id, p) } diff --git a/nodes/config/ui_link.html b/nodes/config/ui_link.html new file mode 100644 index 00000000..c2e30e29 --- /dev/null +++ b/nodes/config/ui_link.html @@ -0,0 +1,108 @@ + + + diff --git a/nodes/config/ui_link.js b/nodes/config/ui_link.js new file mode 100644 index 00000000..fb422665 --- /dev/null +++ b/nodes/config/ui_link.js @@ -0,0 +1,63 @@ +module.exports = function (RED) { + /** + * + * @param {*} config + */ + function UILinkNode (config) { + RED.nodes.createNode(this, config) + const node = this + + node.on('close', function (removed, done) { + node.deregister() // deregister self + done() + }) + + // handle bad typing in Node-RED + if (!('disabled' in config)) { + // ensure we have a value + config.disabled = false + } else { + // ensure we have a boolean + config.disabled = (config.disabled === 'true' || config.disabled === true) + } + if (!('visible' in config)) { + // ensure we have a value + config.visible = true + } else { + // ensure we have a boolean + config.visible = (config.visible === 'true' || config.visible === true) + } + + const ui = RED.nodes.getNode(config.ui) + + // register self + ui.register(config) + + /** + * Function for widgets to register themselves with this page + * Calls the parent UI Base "register" function and registers this page, + * along with the widget + * @param {*} widget + */ + node.register = function (group, widgetNode, widgetConfig, widgetEvents) { + const link = config + if (ui) { + ui.register(link, group, widgetNode, widgetConfig, widgetEvents) + } else { + node.error(`Error registering Widget - ${widgetNode.name || widgetNode.id}. No parent ui-base node found for ui-link node: ${(link.name || link.id)}`) + } + } + node.deregister = function (group, widgetNode) { + const link = config + if (ui) { + ui.deregister(link, group, widgetNode) + } + } + + // Return the UI Base Node this page lives in + node.getBase = function () { + return RED.nodes.getNode(config.ui) + } + } + RED.nodes.registerType('ui-link', UILinkNode) +} diff --git a/nodes/config/ui_page.html b/nodes/config/ui_page.html index d7e12b30..4780835e 100644 --- a/nodes/config/ui_page.html +++ b/nodes/config/ui_page.html @@ -60,12 +60,12 @@ }) // backwards compatibility - if (this.visible === undefined) { + if (this.visible === undefined || this.visible === 'true') { this.visible = true $('#node-config-input-visible').val('true') } // backwards compatibility - if (this.disabled === undefined) { + if (this.disabled === undefined || this.visible === 'false') { this.disabled = false $('#node-config-input-disabled').val('false') } diff --git a/package.json b/package.json index a7b41c75..ccc33058 100644 --- a/package.json +++ b/package.json @@ -112,6 +112,7 @@ "nodes": { "ui-base": "nodes/config/ui_base.js", "ui-page": "nodes/config/ui_page.js", + "ui-link": "nodes/config/ui_link.js", "ui-group": "nodes/config/ui_group.js", "ui-theme": "nodes/config/ui_theme.js", "ui-form": "nodes/widgets/ui_form.js", diff --git a/ui/src/layouts/Baseline.vue b/ui/src/layouts/Baseline.vue index 2c8829f6..c593592f 100644 --- a/ui/src/layouts/Baseline.vue +++ b/ui/src/layouts/Baseline.vue @@ -35,7 +35,8 @@ :disabled="page.disabled || undefined" :prepend-icon="`mdi-${page.icon?.replace(/^mdi-/, '') || 'home'}`" :title="getPageLabel(page)" - :to="{name: page.route.name}" link + :href="page.type === 'ui-link' ? page.path : null" + :to="page.type === 'ui-page' ? { name: page.route.name } : null" :data-nav="page.id" @click="closeNavigationDrawer()" /> From 87f5f22e5774dab42cd84d4882d0f64368412b85 Mon Sep 17 00:00:00 2001 From: Joe Pavitt Date: Fri, 10 May 2024 15:02:01 +0100 Subject: [PATCH 2/3] Docs - Add "ui-link" to the docs, and update debug view --- docs/.vitepress/config.js | 1 + docs/nodes/config/ui-link.md | 23 +++++++++++++++++++++++ ui/src/debug/Debug.vue | 2 +- 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 docs/nodes/config/ui-link.md diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js index a72938de..677dde71 100644 --- a/docs/.vitepress/config.js +++ b/docs/.vitepress/config.js @@ -118,6 +118,7 @@ export default ({ mode }) => { items: [ { text: 'ui-base', link: '/nodes/config/ui-base' }, { text: 'ui-page', link: '/nodes/config/ui-page' }, + { text: 'ui-link', link: '/nodes/config/ui-link' }, { text: 'ui-group', link: '/nodes/config/ui-group' }, { text: 'ui-theme', link: '/nodes/config/ui-theme' } ] diff --git a/docs/nodes/config/ui-link.md b/docs/nodes/config/ui-link.md new file mode 100644 index 00000000..b1b24c10 --- /dev/null +++ b/docs/nodes/config/ui-link.md @@ -0,0 +1,23 @@ +--- +description: Manage your dashboard pages with ease in Node-RED Dashboard 2.0 for a streamlined user experience. +props: + UI: The UI (ui-base) that this page will be added to. + Path: The URL to navigate to when a user selects this link + Icon: Which Material Designs Icon to use for the page. No need to include the mdi- prefix. + Default State:
    • Visibility - Defines the default visibility of this page in hte side navigation menu.
    • Interactivity - Controls whether the item is disabled/enabled in the side navigation menu.

    Both of these can be overridden by the user at runtime using a ui-control node.

    +--- + + + +# Config: UI Link `ui-link` + +If you want to link to external resources from your Dashboard, you can do so with the `ui-link` config node. This will render a link in the side navigation menu, just like your Dashboard [Pages](./ui-page.md), but will navigate directly to the URL you specify, even if out of the scope of Dashboard 2.0. + +## Properties + + + +## Adding Links + +To add a link to your Dashboard, you can use the Dashboard 2.0 side panel in the Node-RED editor. Click the `+ Link` button to add a new item to the list. You can then configure the link with the relevant properties. \ No newline at end of file diff --git a/ui/src/debug/Debug.vue b/ui/src/debug/Debug.vue index 4567d42c..fb4e7094 100644 --- a/ui/src/debug/Debug.vue +++ b/ui/src/debug/Debug.vue @@ -43,7 +43,7 @@