luci-base: add clone action for tables
authorPaul Donald <newtwen+github@gmail.com>
Sun, 24 Nov 2024 00:50:40 +0000 (01:50 +0100)
committerPaul Donald <newtwen+github@gmail.com>
Sun, 24 Nov 2024 14:55:24 +0000 (15:55 +0100)
This augments CBITableSection, affecting types which extend it, i.e.
CBIGridSection.

Setting a table 'cloneable' property to true reveals a column of clone
buttons who designate the current entry as a clone source. Clicking the
clone button duplicates the data of that section_id into a new entry,
while the new entry gets a new and unique SID. E.g.

s = m.section(form.GridSection, 'foo', _('Bar'));
...
s.cloneable = true;

Clone and add actions differ: clone will not open a dialogue. That is a
user exercise.

One may set the put_next flag to false to put the new clone last, or
true to put it next (after the clone source).

This uses a new uci action which fulfills the behaviour: clone

It is possible for the uci clone action to be used independently.

See also:
https://forum.openwrt.org/t/add-clone-button-to-luci-configurations-esp-in-firewall/196232

Signed-off-by: Paul Donald <newtwen+github@gmail.com>
modules/luci-base/htdocs/luci-static/resources/form.js
modules/luci-base/htdocs/luci-static/resources/uci.js

index c47b38d273e58dbf92047fa4e1fa64492f4aa7c9..53ea3c5bcd9176a15f2a97c98897a7262d7c4a06 100644 (file)
@@ -2431,6 +2431,16 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
         * @default false
         */
 
+       /**
+        * Set to `true`, a clone button is added to the button column, allowing
+        * the user to clone section instances mapped by the section form element.
+        * The default is `false`.
+        *
+        * @name LuCI.form.TypedSection.prototype#cloneable
+        * @type boolean
+        * @default false
+        */
+
        /**
         * Enables a per-section instance row `Edit` button which triggers a certain
         * action when clicked. If set to a string, the string value is used
@@ -2479,11 +2489,25 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
                throw 'Tabs are not supported by TableSection';
        },
 
+
+       /**
+        * Clone the section_id, putting the clone immediately after if put_next
+        * is true. Optionally supply a name for the new section_id.
+        */
+       /** @private */
+       handleClone: function(section_id, put_next, name) {
+               let config_name = this.uciconfig || this.map.config;
+
+               this.map.data.clone(config_name, this.sectiontype, section_id, put_next, name);
+               return this.map.save(null, true);
+       },
+
        /** @private */
        renderContents: function(cfgsections, nodes) {
                var section_id = null,
                    config_name = this.uciconfig || this.map.config,
                    max_cols = isNaN(this.max_cols) ? this.children.length : this.max_cols,
+                   cloneable = this.cloneable,
                    has_more = max_cols < this.children.length,
                    drag_sort = this.sortable && !('ontouchstart' in window),
                    touch_sort = this.sortable && ('ontouchstart' in window),
@@ -2601,7 +2625,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
                                        dom.content(trEl.lastElementChild, opt.title);
                        }
 
-                       if (this.sortable || this.extedit || this.addremove || has_more || has_action)
+                       if (this.sortable || this.extedit || this.addremove || has_more || has_action || this.cloneable)
                                trEl.appendChild(E('th', {
                                        'class': 'th cbi-section-table-cell cbi-section-actions'
                                }));
@@ -2628,7 +2652,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
                                                (typeof(opt.width) == 'number') ? opt.width+'px' : opt.width;
                        }
 
-                       if (this.sortable || this.extedit || this.addremove || has_more || has_action)
+                       if (this.sortable || this.extedit || this.addremove || has_more || has_action || this.cloneable)
                                trEl.appendChild(E('th', {
                                        'class': 'th cbi-section-table-cell cbi-section-actions'
                                }));
@@ -2643,7 +2667,7 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
        renderRowActions: function(section_id, more_label) {
                var config_name = this.uciconfig || this.map.config;
 
-               if (!this.sortable && !this.extedit && !this.addremove && !more_label)
+               if (!this.sortable && !this.extedit && !this.addremove && !more_label && !this.cloneable)
                        return E([]);
 
                var tdEl = E('td', {
@@ -2690,6 +2714,19 @@ var CBITableSection = CBITypedSection.extend(/** @lends LuCI.form.TableSection.p
                        );
                }
 
+               if (this.cloneable) {
+                       var btn_title = this.titleFn('clonebtntitle', section_id);
+
+                       dom.append(tdEl.lastElementChild,
+                               E('button', {
+                                       'title': btn_title || _('Clone') + '⿻',
+                                       'class': 'btn cbi-button cbi-button-neutral',
+                                       'click': ui.createHandlerFn(this, 'handleClone', section_id, true),
+                                       'disabled': this.map.readonly || null
+                               }, [ btn_title || _('Clone') + '⿻' ])
+                       );
+               }
+
                if (this.addremove) {
                        var btn_title = this.titleFn('removebtntitle', section_id);
 
index 76b274470b19dd6c9698e95b341b80e48b92be3a..e52c184e51f2dc9bb7dc6c661e066d573dfbc0c5 100644 (file)
@@ -299,6 +299,53 @@ return baseclass.extend(/** @lends LuCI.uci.prototype */ {
                return sid;
        },
 
+       /**
+        * Clones an existing section of the given type to the given configuration,
+        * optionally named according to the given name.
+        *
+        * @param {string} conf
+        * The name of the configuration into which to clone the section.
+        *
+        * @param {string} type
+        * The type of the section to clone.
+        *
+        * @param {string} srcsid
+        * The source section id to clone.
+        *
+        * @param {boolean} [put_next]
+        * Whether to put the cloned item next (true) or last (false: default).
+        *
+        * @param {string} [name]
+        * The name of the new cloned section. If the name is omitted, an anonymous
+        * section will be created instead.
+        *
+        * @returns {string}
+        * Returns the section ID of the newly cloned section which is equivalent
+        * to the given name for non-anonymous sections.
+        */
+       clone: function(conf, type, srcsid, put_next, name) {
+               let n = this.state.creates;
+               let sid = this.createSID(conf);
+               let v = this.state.values;
+               put_next = put_next || false;
+
+               if (!n[conf])
+                       n[conf] = { };
+
+               n[conf][sid] = {
+                       ...v[conf][srcsid],
+                       '.type': type,
+                       '.name': sid,
+                       '.create': name,
+                       '.anonymous': !name,
+                       '.index': 1000 + this.state.newidx++
+               };
+
+               if (put_next)
+                       this.move(conf, sid, srcsid, put_next);
+               return sid;
+       },
+
        /**
         * Removes the section with the given ID from the given configuration.
         *