Node config screen - Show details of a table row

Hi folks,

In my newest node config screen, a table is displayed. Each row in that table can have a different number of properties/columns.

So I show the common properties/columns in the table, but I need to be able to show the other extra properties somewhere (so the user can edit them):

image

In other words: when the user selects a row in the table, I need to display somehow the extra properties of that row. But when I show them below the table (as in the screenshot), there is no visual indication that the properties belong to the selected row :pleading_face:

Does anybody have a suggestion how I could implement this, or other nodes that do something similar. I assume some kind of popup is not an option?

Thanks !!
Bart

When you create the admin ui for a node, you get access to jQuery and jQuery UI in addition to the Node-RED specific ui elements like editableList.

As you are using editableList, I would probably start by trying a 2-line layout with the extra fields appearing in a second line - really all one row of the editableList but since the list row simply contains HTML, you can split the input form over 2 lines.

Morning @TotallyInformation, indeed some nodes solve it like that. But that isn't really my cup of tea. I have added column headers. Because I find it very confusing having to fill in multiple input fields, without column headers guiding me... When I add a second line, the headers don't make sense anymore...

Maybe make each row a seperate table?

Hey @zenofmud,
Perhaps I might give that idea a chance. Found treetables, datasets, ... but they are all quite mind blowing for a hobby :thinking: Think this jsfiddle example could be a good place to start for my adventure. Although I'm wondering if I could replace my table perhaps by this JSON editor or this JSON editor: would be nice if I could just create a JSON scheme, and let the editor do all the heavy work. I 'think' I just have to pass a json string and a json scheme, then the editor generates a screen and gives me a json output at the end (which I could just send to my server-side Node-RED flow). Not sure if it works that way. Anybody has good or bad experiences with this?

Sorry, meant to say this yesterday.

You know that you get a header with the editableList? All you need to do is set sensible widths using CSS. Then you can have the best of both worlds.

There are certainly tools to do that but I don't know of any for jQuery - not that I use jQuery much so that isn't to say they don't exist.

The default framework for uibuilder v2 uses bootstrap-vue which has such a thing built in and which I'm using for my home dashboard but that doesn't really help you for the admin ui.

@TotallyInformation. No, unfortunately I didn't knew that. Until now :wink:... Do you perhaps know a node that I can use as an example?

https://nodered.org/docs/api/ui/editableList/#header

Hmm, let me think - do I know anyone using editableList? Maybe I do ... wait one ...

Here is a simple section taken from the uibuilder v2 admin panel:

    <!-- ---- Hideable NPM Section ---- -->
    <div id="npm-props">
        <h3>Manage Front-End Libraries</h3>
        <p>
            Install, remove or update npm packages that provide front-end libraries such as
            VueJS, jQuery, MoonJS, etc.
        </p>
        <p>
            You can search for packages on the 
            <a href="https://www.npmjs.com/" target="_blank">official npm site</a> 
            or on <a href="https://npms.io/" target="_blank">npms.io</a>.
            Take the name and paste it below then use one of the buttons.
        </p>
        <!-- Package List -->
        <div class="form-row">
            <ol id="node-input-packageList"></ol>
        </div>
    </div>

And here is some code to go with it:

RED.nodes.registerType('uibuilder', {
    ....
    /** AddItem function for package list
     * @param {JQuery<HTMLElement>} element the jQuery DOM element to which any row content should be added
     * @param {number} index the index of the row
     * @param {string|*} data data object for the row. {} if add button pressed, else data passed to addItem method
     */
    function addPackageRow(element,index,data) {
        var hRow = ''

        if (Object.entries(data).length === 0) {
            // Add button was pressed so we have no packageName, create an input form instead
            hRow='<input type="text" id="packageList-input-' + index + '"> <button id="packageList-button-' + index + '">Install</button>'
        } else {
            // addItem method was called with a packageName passed
            hRow = data
        }
        // Build the output row
        var myRow = $('<div id="packageList-row-' + index + '" class="packageList-row-data">'+hRow+'</div>').appendTo(element)

        // Create a button click listener for the install button for this row
        $('#packageList-button-' + index).click(function(){
            //console.log('.packageList-row-data button::click', $('#packageList-input-' + index).val() )

            // show activity spinner
            $('i.spinner').show()
            
            const packageName = '' + $('#packageList-input-' + index).val()

            if ( packageName.length !== 0 ) {
                // Call the npm installPackage API (it updates the package list)
                $.get( 'uibnpmmanage?cmd=install&package=' + packageName, function(data){
                    //console.log('.packageList-row-data get::uibnpm', data )

                    if ( data.success === true) {
                        console.log('PACKAGE INSTALLED')

                        // Replace the input field with the normal package name display
                        myRow.html(packageName)
                    } else {
                        console.log('ERROR ON INSTALLATION ' )
                        console.dir( data.result )
                    }

                    // Hide the progress spinner
                    $('i.spinner').hide()

                }).fail(function(jqXHR, textStatus, errorThrown) {
                    console.error( '[uibuilder:addPackageRow:get] Error ' + textStatus, errorThrown )

                    $('i.spinner').hide()
                    return 'addPackageRow failed'
                    // TODO otherwise highlight input
                })
            } // else Do nothing

        }) // -- end of button click -- //

    } // --- End of addPackageRow() ---- //

    /** RemoveItem function for package list */
    function removePackageRow(packageName) {
        console.log('PACKAGE NAME: ', packageName)

        // If package name is an empty object - user removed an add row so ignore
        if ( (packageName === '') || (typeof packageName !== 'string') ) {
            return false
        }

        // show activity spinner
        $('i.spinner').show()

        // Call the npm installPackage API (it updates the package list)
        $.get( 'uibnpmmanage?cmd=remove&package=' + packageName, function(data){
            console.log('removePackageRow get::uibnpm', data )

            if ( data.success === true) console.log('PACKAGE REMOVED')
            else {
                console.log('ERROR ON REMOVAL ', data.result )
                // Put the entry back again
                $('#node-input-packageList').editableList('addItem',packageName)
            }

            $('i.spinner').hide()

        }).fail(function(jqXHR, textStatus, errorThrown) {
            console.error( '[uibuilder:removePackageRow:get] Error ' + textStatus, errorThrown )
            
            // Put the entry back again
            $('#node-input-packageList').editableList('addItem',packageName)

            $('i.spinner').hide()
            return 'removePackageRow failed'
            // TODO otherwise highlight input
        })
        
    } // ---- End of removePackageRow ---- //

    /** Get full package list via API and show in admin ui
     * param {string} url 
     * param {boolean} rebuild - Rebuild the vendorPaths list
     */
    function packageList() {
        $.getJSON('uibvendorpackages', function(vendorPaths) {
            console.log('uibuilder:packageList:uibvendorpackages', vendorPaths)

            $('#node-input-packageList').editableList('empty');

            const pkgList = Object.keys(vendorPaths)
            pkgList.forEach(function(packageName,index){
                if ( packageName !== 'socket.io' )
                    $('#node-input-packageList').editableList('addItem',packageName)
            })
        })
    } // --- End of packageList --- //

    ....
        oneditprepare: function () {
            ....

            //#region ---- npm ---- //
            // NB: Assuming that the edit section is closed
            // Show the npm section, hide the main & adv sections
            $('#show-npm-props').click(function(e) {
                e.preventDefault() // don't trigger normal click event
                $('#main-props').hide()
                $('#adv-props').hide()
                $('#show-adv-props').html('<i class="fa fa-caret-right"></i> Advanced Settings')
                $('#npm-props').show()

                // TODO Improve feedback
                //#region Setup the package list
                $('#node-input-packageList').editableList({
                    addItem: addPackageRow, // function
                    removeItem: removePackageRow, // function(data){},
                    resizeItem: function(row,index) {},
                    header: $('<div>').append('<h4 style="display: inline-grid">Installed Packages</h4>'),
                    height: 'auto',
                    addButton: true,
                    removable: true,
                    scrollOnAdd: true,
                    sortable: false,
                })

                /** Initialise default values for package list
                 * NOTE: This is build dynamically each time the edit panel is opened
                 *       we are not saving this since external changes would result in
                 *       users having being prompted to deploy even when they've made
                 *       no changes themselves to a node instance.
                 */
                packageList()

                // spinner
                $('.red-ui-editableList-addButton').after(' <i class="spinner"></i>')
                $('i.spinner').hide()
                //#endregion --- package list ---- //

            })
            // Hide the npm section, show the main section
            $('#npm-close').click(function(e) {
                e.preventDefault() // don't trigger normal click event

                $('#main-props').show()
                $('#npm-props').hide()
            })
            //#endregion ---- npm ---- //

           ...
       },

       ....
}

Sorry, that's a bit of a dump, hopefully you can see how it is done.

While I'm not using columns as such, those are just some html and some CSS to control the width.

You can easily break up rows either wrapping elements with <div> or a simple <br> if you don't want paragraph spacing.

1 Like

@TotallyInformation, @knolleary,
Thanks for the pointer guys!
Must admit I was wrong about the editableList: seems that the editableList is really my cup of tea!
Once you get familiar with it, it is a very powerful and rather easy to use UI component.
With very few code, I managed to create just what I wanted:

cam_viewer

For those who ever need something similar, you can find my code in the node-red-contrib-ui-camera-viewer.
Bart

1 Like

Hi @BartButenaers, minor point, but I'd suggest using fa-angle-right and fa-angle-down as the collapsed/expanded icons on the left so you are consistent with the other places in the UI that we have collapsible sections.

1 Like

Consider it done:

cam_viewer_arrow

2 Likes

By the way, how to people record these gifs? I really should do some for uibuilder.

On a mac I use RecordIt

Thanks. There is a Windows version of RecordIt too. Another popular choice seems to be ScreenToGif.

https://www.screentogif.com/

http://recordit.co/

ScreenToGif seems to have more options/control but RecordIt seems the simplest to use. Inevitably for me, I'll probably choose the former. :slight_smile:

Actually, ScreenToGif has another advantage - it doesn't need to be installed, they have a standalone version.

Hey @TotallyInformation,
I use GifCam on a Windows portable ...

ShareX

https://goo.gl/search/ShareX
ShareX,