Skip to content

Commit

Permalink
luci-base: add clone action for tables
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
systemcrash committed Nov 24, 2024
1 parent 2ce8529 commit 22cccf7
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 3 deletions.
43 changes: 40 additions & 3 deletions modules/luci-base/htdocs/luci-static/resources/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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'
}));
Expand All @@ -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'
}));
Expand All @@ -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', {
Expand Down Expand Up @@ -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);

Expand Down
47 changes: 47 additions & 0 deletions modules/luci-base/htdocs/luci-static/resources/uci.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down

0 comments on commit 22cccf7

Please sign in to comment.