From 22cccf7b042e537b1e1b99fdae8bf18fa646b997 Mon Sep 17 00:00:00 2001 From: Paul Donald Date: Sun, 24 Nov 2024 01:50:40 +0100 Subject: [PATCH] luci-base: add clone action for tables 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 --- .../htdocs/luci-static/resources/form.js | 43 +++++++++++++++-- .../htdocs/luci-static/resources/uci.js | 47 +++++++++++++++++++ 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/modules/luci-base/htdocs/luci-static/resources/form.js b/modules/luci-base/htdocs/luci-static/resources/form.js index c47b38d273..53ea3c5bcd 100644 --- a/modules/luci-base/htdocs/luci-static/resources/form.js +++ b/modules/luci-base/htdocs/luci-static/resources/form.js @@ -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); diff --git a/modules/luci-base/htdocs/luci-static/resources/uci.js b/modules/luci-base/htdocs/luci-static/resources/uci.js index 76b274470b..e52c184e51 100644 --- a/modules/luci-base/htdocs/luci-static/resources/uci.js +++ b/modules/luci-base/htdocs/luci-static/resources/uci.js @@ -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. * -- 2.30.2