* offers the ability to browse, upload and select remote files.
*
* @param {LuCI.form.Map|LuCI.form.JSONMap} form
- * The configuration form to which this section is added to. It is automatically passed
+ * The configuration form to which this section is added. It is automatically passed
* by [option()]{@link LuCI.form.AbstractSection#option} or
* [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
* option to the section.
*
* @param {LuCI.form.AbstractSection} section
- * The configuration section this option is added to. It is automatically passed
+ * The configuration section this option is added. It is automatically passed
* by [option()]{@link LuCI.form.AbstractSection#option} or
* [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
* option to the section.
this.super('__init__', args);
this.browser = false;
+ this.directory_create = false;
+ this.directory_select = false;
this.show_hidden = false;
this.enable_upload = true;
this.enable_remove = true;
/**
- * Open in a file browser mode instead of selecting for a file
+ * Render the widget in browser mode initially instead of a button
+ * to 'Select File...'.
*
* @name LuCI.form.FileUpload.prototype#browser
* @type boolean
* @default true
*/
+ /**
+ * Toggle remote directory create functionality.
+ *
+ * When set to `true`, the underlying widget provides a button which lets
+ * the user create directories. Note that this is merely
+ * a cosmetic feature: remote create permissions are controlled by the
+ * session ACL rules.
+ *
+ * The default of `false` means the directory create button is hidden.
+ *
+ * @name LuCI.form.FileUpload.prototype#directory_create
+ * @type boolean
+ * @default false
+ */
+
+ /**
+ * Toggle remote directory select functionality.
+ *
+ * When set to `true`, the underlying widget changes behaviour to select
+ * directories instead of files, in effect, becoming a directory
+ * picker.
+ *
+ * The default is `false`.
+ *
+ * @name LuCI.form.FileUpload.prototype#directory_select
+ * @type boolean
+ * @default false
+ */
+
/**
* Toggle remote file delete functionality.
*
name: this.cbid(section_id),
browser: this.browser,
show_hidden: this.show_hidden,
+ directory_create: this.directory_create,
+ directory_select: this.directory_select,
enable_upload: this.enable_upload,
enable_remove: this.enable_remove,
enable_download: this.enable_download,
}
});
+/**
+ * @class DirectoryPicker
+ * @memberof LuCI.form
+ * @augments LuCI.form.Value
+ * @hideconstructor
+ * @classdesc
+ *
+ * The `DirectoryPicker` element wraps a {@link LuCI.ui.FileUpload} widget and
+ * offers the ability to browse, create, delete and select remote directories.
+ *
+ * @param {LuCI.form.Map|LuCI.form.JSONMap} form
+ * The configuration form to which this section is added. It is automatically passed
+ * by [option()]{@link LuCI.form.AbstractSection#option} or
+ * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
+ * option to the section.
+ *
+ * @param {LuCI.form.AbstractSection} section
+ * The configuration section this option is added. It is automatically passed
+ * by [option()]{@link LuCI.form.AbstractSection#option} or
+ * [taboption()]{@link LuCI.form.AbstractSection#taboption} when adding the
+ * option to the section.
+ *
+ * @param {string} option
+ * The name of the UCI option to map.
+ *
+ * @param {string} [title]
+ * The title caption of the option element.
+ *
+ * @param {string} [description]
+ * The description text of the option element.
+ */
+const CBIDirectoryPicker = CBIValue.extend(/** @lends LuCI.form.DirectoryPicker.prototype */ {
+ __name__: 'CBI.DirectoryPicker',
+
+ __init__(...args) {
+ this.super('__init__', args);
+
+ this.browser = false;
+ this.directory_create = false;
+ this.enable_download = false;
+ this.enable_remove = false;
+ this.enable_upload = false;
+ this.root_directory = '/tmp';
+ this.show_hidden = true;
+ },
+
+
+ /**
+ * Render the widget in browser mode initially instead of a button
+ * to 'Select Directory...'.
+ *
+ * @name LuCI.form.DirectoryPicker.prototype#browser
+ * @type boolean
+ * @default false
+ */
+
+ /**
+ * Toggle remote directory create functionality.
+ *
+ * When set to `true`, the underlying widget provides a button which lets
+ * the user create directories. Note that this is merely
+ * a cosmetic feature: remote create permissions are controlled by the
+ * session ACL rules.
+ *
+ * The default of `false` means the directory create button is hidden.
+ *
+ * @name LuCI.form.DirectoryPicker.prototype#directory_create
+ * @type boolean
+ * @default false
+ */
+
+ /**
+ * Toggle download file functionality.
+ *
+ * @name LuCI.form.DirectoryPicker.prototype#enable_download
+ * @type boolean
+ * @default false
+ */
+
+ /**
+ * Toggle remote file delete functionality.
+ *
+ * When set to `true`, the underlying widget provides buttons which let
+ * the user delete files from remote directories. Note that this is merely
+ * a cosmetic feature: remote delete permissions are controlled by the
+ * session ACL rules.
+ *
+ * The default is `false`, means file removal buttons are not displayed.
+ *
+ * @name LuCI.form.DirectoryPicker.prototype#enable_remove
+ * @type boolean
+ * @default false
+ */
+
+ /**
+ * Toggle file upload functionality.
+ *
+ * When set to `true`, the underlying widget provides a button which lets
+ * the user select and upload local files to the remote system.
+ * Note that this is merely a cosmetic feature: remote upload access is
+ * controlled by the session ACL rules.
+ *
+ * The default of `false` means file upload functionality is disabled.
+ *
+ * @name LuCI.form.DirectoryPicker.prototype#enable_upload
+ * @type boolean
+ * @default false
+ */
+
+ /**
+ * Specify the root directory for file browsing.
+ *
+ * This property defines the topmost directory the file browser widget may
+ * navigate to. The UI will not allow browsing directories outside this
+ * prefix. Note that this is merely a cosmetic feature: remote file access
+ * and directory listing permissions are controlled by the session ACL
+ * rules.
+ *
+ * The default is `/tmp`.
+ *
+ * @name LuCI.form.DirectoryPicker.prototype#root_directory
+ * @type string
+ * @default /tmp
+ */
+
+ /**
+ * Toggle display of hidden files.
+ *
+ * Display hidden files when rendering the remote directory listing.
+ * Note that this is merely a cosmetic feature: hidden files are always
+ * included in received remote file listings.
+ *
+ * The default of `true` means hidden files are displayed.
+ *
+ * @name LuCI.form.DirectoryPicker.prototype#show_hidden
+ * @type boolean
+ * @default true
+ */
+
+ /** @private */
+ renderWidget(section_id, option_index, cfgvalue) {
+ const browserEl = new ui.FileUpload((cfgvalue != null) ? cfgvalue : this.default, {
+ id: this.cbid(section_id),
+ name: this.cbid(section_id),
+ browser: this.browser,
+ directory_create: this.directory_create,
+ directory_select: true,
+ enable_download: this.enable_download,
+ enable_remove: this.enable_remove,
+ enable_upload: this.enable_upload,
+ root_directory: this.root_directory,
+ show_hidden: this.show_hidden,
+ disabled: (this.readonly != null) ? this.readonly : this.map.readonly
+ });
+
+ return browserEl.render();
+ }
+});
+
/**
* @class SectionValue
* @memberof LuCI.form
Button: CBIButtonValue,
HiddenValue: CBIHiddenValue,
FileUpload: CBIFileUpload,
+ DirectoryPicker: CBIDirectoryPicker,
SectionValue: CBISectionValue
});
* remotely depends on the ACL setup for the current session. This option
* merely controls whether the file remove controls are rendered or not.
*
+ * @property {boolean} [directory_create=false]
+ * Specifies whether the widget allows the user to create directories.
+ *
+ * @property {boolean} [directory_select=false]
+ * Specifies whether the widget shall select directories only instead of files.
+ *
* @property {boolean} [enable_download=false]
* Specifies whether the widget allows the user to download files.
*
this.value = value;
this.options = Object.assign({
browser: false,
+ directory_create: false,
+ directory_select: false,
show_hidden: false,
enable_upload: true,
enable_remove: true,
const renderFileBrowser = L.resolveDefault(this.value != null ? fs.stat(this.value) : null).then(L.bind((stat) => {
let label;
- if (L.isObject(stat) && stat.type != 'directory')
+ if (L.isObject(stat))
this.stat = stat;
- if (this.stat != null)
+ if (this.stat != null && this.stat.type === 'directory')
+ label = [ this.iconForType(this.stat.type), ' %s'.format(this.truncatePath(this.stat.path)) ];
+ else if (this.stat != null && this.stat.type !== 'directory')
label = [ this.iconForType(this.stat.type), ' %s (%1000mB)'.format(this.truncatePath(this.stat.path), this.stat.size) ];
else if (this.value != null)
label = [ this.iconForType('file'), ' %s (%s)'.format(this.truncatePath(this.value), _('File not accessible')) ];
else
- label = [ _('Select file…') ];
+ label = [ this.options.directory_select ? _('Select directory…') : _('Select file…') ];
let btnOpenFileBrowser = E('button', {
'class': 'btn open-file-browser',
'click': UI.prototype.createHandlerFn(this, 'handleFileBrowser'),
if (cpath.length <= croot.length)
return [ croot ];
- const parts = cpath.substring(croot.length).split(/\//);
+ const parts = cpath.substring(croot.length).split(/\//).filter(p => p !== '');
parts.unshift(croot);
return parts;
},
+ /** @private */
+ handleCreateDirectory(path, ev) {
+ const container = E('div', { 'class': 'uci-dialog' });
+
+ const input = E('input', {
+ 'type': 'text',
+ 'placeholder': _('Directory name'),
+ 'style': 'margin-right: 0.5em'
+ });
+
+ const okBtn = E('button', {
+ 'type': 'button',
+ 'class': 'btn cbi-button',
+ 'click': async () => {
+ var directoryName = input.value.trim();
+ if (!directoryName) {
+ alert(_('Directory name cannot be empty.'));
+ return;
+ }
+
+ try {
+ // Assume current upload path (you may need to retrieve or set this yourself)
+ var basePath = path || '/tmp';
+ var fullPath = basePath + '/' + directoryName;
+
+ await fs.exec('mkdir', ['-p', fullPath]).then(L.bind((path, ev) => {
+ return this.handleSelect(path, null, ev);
+ }, this, path, ev));
+ } catch (err) {
+ UI.prototype.addTimeLimitedNotification(_('Error'), E('p', _('Failed to create directory: %s').format(err.message)), 5000, 'error');
+ } finally {
+ UI.prototype.hideModal();
+ }
+ }
+ }, _('OK'));
+
+ var cancelBtn = E('button', {
+ 'type': 'button',
+ 'class': 'btn cbi-button',
+ 'click': () => UI.prototype.hideModal(),
+ }, _('Cancel'));
+
+ container.appendChild(input);
+ container.appendChild(okBtn);
+ container.appendChild(cancelBtn);
+
+
+ UI.prototype.showModal(_('Create Directory'), [
+ container
+ ]);
+ },
+
/** @private */
handleUpload(path, list, ev) {
const form = ev.target.parentNode;
const hidden = this.node.lastElementChild;
if (path == hidden.value) {
- dom.content(button, _('Select file…'));
+ dom.content(button, this.options.directory_select ? _('Select directory…') : _('Select file…'));
hidden.value = '';
}
E('div', { 'class': 'name' }, [
this.iconForType(list[i].type),
' ',
+ (this.options.directory_select && list[i].type !== 'directory') ?
+ list[i].name :
E('a', {
'href': '#',
'style': selected ? 'font-weight:bold' : null,
mtime.getSeconds())
]),
E('div', [
+ (this.options.directory_select && list[i].type === 'directory') ? E('button', {
+ 'class': 'btn cbi-button',
+ 'click': UI.prototype.createHandlerFn(this, 'handleSelect',
+ entrypath, list[i].type === 'directory' ? list[i] : null)
+ }, [ _('Select') ]) : '',
selected ? E('button', {
'class': 'btn',
'click': UI.prototype.createHandlerFn(this, 'handleReset')
let cur = '';
for (let i = 0; i < dirs.length; i++) {
- cur += dirs[i];
+ cur = (i === 0 || cur === '/') ? cur + dirs[i] : cur + '/' + dirs[i];
dom.append(breadcrumb, [
i ? ' » ' : '',
E('a', {
rows,
E('div', { 'class': 'right' }, [
this.renderUpload(path, list),
+ (this.options.directory_create) ? E('a', {
+ 'href': '#',
+ 'class': 'btn cbi-button',
+ 'click': UI.prototype.createHandlerFn(this, 'handleCreateDirectory', path)
+ }, _('Create')) : '',
!this.options.browser ? E('a', {
'href': '#',
'class': 'btn',
const hidden = this.node.lastElementChild;
hidden.value = '';
- dom.content(button, _('Select file…'));
+ dom.content(button, this.options.directory_select ? _('Select directory…') : _('Select file…'));
this.handleCancel(ev);
},