From ca6b715b31bbfea7724dd8659e2a1e61098a31a5 Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Mon, 16 Mar 2015 14:07:53 +0100 Subject: [PATCH] Added ext storage mount options GUI Added option to disable autoscan of external storages Mount option file system scan is now an int Move priority field to avoid undefined field in storage options All input elements inside the storage options block get parsed into storage options. Moving the priority field outside prevents it to appear in the storage config, as expected. It is still parsed by special code. --- apps/files_external/css/settings.css | 13 +- apps/files_external/js/settings.js | 179 ++++++++++++++++++- apps/files_external/templates/settings.php | 24 ++- apps/files_external/tests/js/settingsSpec.js | 88 ++++++++- core/css/apps.css | 16 ++ core/js/l10n.js | 4 + 6 files changed, 303 insertions(+), 21 deletions(-) diff --git a/apps/files_external/css/settings.css b/apps/files_external/css/settings.css index 93689f78c52..fb7789a537d 100644 --- a/apps/files_external/css/settings.css +++ b/apps/files_external/css/settings.css @@ -6,10 +6,11 @@ td.status > span { } td.mountPoint, td.backend { width:160px; } -td.remove>img { visibility:hidden; padding-top:7px; } -tr:hover>td.remove>img { visibility:visible; cursor:pointer; } +#externalStorage td>img.action { visibility:hidden; padding-top:7px; } +#externalStorage tr:hover>td>img.action { visibility:visible; cursor:pointer; } #addMountPoint>td { border:none; } #addMountPoint>td.applicable { visibility:hidden; } +#addMountPoint>td.hidden { visibility:hidden; } #selectBackend { margin-left: -10px; @@ -67,3 +68,11 @@ tr:hover>td.remove>img { visibility:visible; cursor:pointer; } top: 6px; position: relative; } + +#externalStorage .mountOptionsToggle .dropdown { + width: auto; +} + +#externalStorage .mountOptionsDropdown { + margin-right: 40px; +} diff --git a/apps/files_external/js/settings.js b/apps/files_external/js/settings.js index 254bab30c56..ae3aef4ef35 100644 --- a/apps/files_external/js/settings.js +++ b/apps/files_external/js/settings.js @@ -7,8 +7,27 @@ * See the COPYING-README file. * */ + (function(){ +// TODO: move to a separate file +var MOUNT_OPTIONS_DROPDOWN_TEMPLATE = + ''; + /** * Returns the selection of applicable users in the given configuration row * @@ -219,7 +238,8 @@ StorageConfig.prototype = { $.ajax({ type: method, url: url, - data: this.getData(), + contentType: 'application/json', + data: JSON.stringify(this.getData()), success: function(result) { self.id = result.id; if (_.isFunction(options.success)) { @@ -285,7 +305,6 @@ StorageConfig.prototype = { } return; } - var self = this; $.ajax({ type: 'DELETE', url: OC.generateUrl(this._url + '/{id}', {id: this.id}), @@ -378,6 +397,110 @@ UserStorageConfig.prototype = _.extend({}, StorageConfig.prototype, _url: 'apps/files_external/userstorages' }); +/** + * @class OCA.External.Settings.MountOptionsDropdown + * + * @classdesc Dropdown for mount options + * + * @param {Object} $container container DOM object + */ +var MountOptionsDropdown = function() { +}; +/** + * @memberof OCA.External.Settings + */ +MountOptionsDropdown.prototype = { + /** + * Dropdown element + * + * @var Object + */ + $el: null, + + /** + * Show dropdown + * + * @param {Object} $container container + * @param {Object} mountOptions mount options + */ + show: function($container, mountOptions) { + if (MountOptionsDropdown._last) { + MountOptionsDropdown._last.hide(); + } + + var template = MountOptionsDropdown._template; + if (!template) { + template = Handlebars.compile(MOUNT_OPTIONS_DROPDOWN_TEMPLATE); + MountOptionsDropdown._template = template; + } + + var $el = $(template()); + this.$el = $el; + $el.addClass('hidden'); + + this.setOptions(mountOptions); + + this.$el.appendTo($container); + MountOptionsDropdown._last = this; + + this.$el.trigger('show'); + }, + + hide: function() { + if (this.$el) { + this.$el.trigger('hide'); + this.$el.remove(); + this.$el = null; + MountOptionsDropdown._last = null; + } + }, + + /** + * Returns the mount options from the dropdown controls + * + * @return {Object} options mount options + */ + getOptions: function() { + var options = {}; + + this.$el.find('input, select').each(function() { + var $this = $(this); + var key = $this.attr('name'); + var value = null; + if ($this.attr('type') === 'checkbox') { + value = $this.prop('checked'); + } else { + value = $this.val(); + } + if ($this.attr('data-type') === 'int') { + value = parseInt(value, 10); + } + options[key] = value; + }); + return options; + }, + + /** + * Sets the mount options to the dropdown controls + * + * @param {Object} options mount options + */ + setOptions: function(options) { + var $el = this.$el; + _.each(options, function(value, key) { + var $optionEl = $el.find('input, select').filterAttr('name', key); + if ($optionEl.attr('type') === 'checkbox') { + if (_.isString(value)) { + value = (value === 'true'); + } + $optionEl.prop('checked', !!value); + } else { + $optionEl.val(value); + } + }); + } +}; + /** * @class OCA.External.Settings.MountConfigListView * @@ -503,12 +626,20 @@ MountConfigListView.prototype = { self.deleteStorageConfig($(this).closest('tr')); }); + this.$el.on('click', 'td.mountOptionsToggle>img', function() { + self._showMountOptionsDropdown($(this).closest('tr')); + }); + this.$el.on('change', '.selectBackend', _.bind(this._onSelectBackend, this)); }, _onChange: function(event) { var self = this; var $target = $(event.target); + if ($target.closest('.dropdown').length) { + // ignore dropdown events + return; + } highlightInput($target); var $tr = $target.closest('tr'); @@ -569,6 +700,7 @@ MountConfigListView.prototype = { } }); $tr.find('td').last().attr('class', 'remove'); + $tr.find('td.mountOptionsToggle').removeClass('hidden'); $tr.find('td').last().removeAttr('style'); $tr.removeAttr('id'); $target.remove(); @@ -643,7 +775,7 @@ MountConfigListView.prototype = { storage.applicableUsers = users; storage.applicableGroups = groups; - storage.priority = $tr.find('input.priority').val(); + storage.priority = parseInt($tr.find('input.priority').val() || '100', 10); } var mountOptions = $tr.find('input.mountOptions').val(); @@ -786,6 +918,47 @@ MountConfigListView.prototype = { } } return defaultMountPoint + append; + }, + + /** + * Toggles the mount options dropdown + * + * @param {Object} $tr configuration row + */ + _showMountOptionsDropdown: function($tr) { + if (this._preventNextDropdown) { + // prevented because the click was on the toggle + this._preventNextDropdown = false; + return; + } + var self = this; + var storage = this.getStorageConfig($tr); + var $toggle = $tr.find('.mountOptionsToggle'); + var dropDown = new MountOptionsDropdown(); + dropDown.show($toggle, storage.mountOptions || []); + + $('body').on('mouseup.mountOptionsDropdown', function(event) { + var $target = $(event.target); + if ($toggle.has($target).length) { + // why is it always so hard to make dropdowns behave ? + // this prevents the click on the toggle to cause + // the dropdown to reopen itself + // (preventDefault doesn't work here because the click + // event is already in the queue and cannot be cancelled) + self._preventNextDropdown = true; + } + if ($target.closest('.dropdown').length) { + return; + } + dropDown.hide(); + }); + + dropDown.$el.on('hide', function() { + var mountOptions = dropDown.getOptions(); + $('body').off('mouseup.mountOptionsDropdown'); + $tr.find('input.mountOptions').val(JSON.stringify(mountOptions)); + self.saveStorageConfig($tr); + }); } }; diff --git a/apps/files_external/templates/settings.php b/apps/files_external/templates/settings.php index 4866d177634..967c59debe6 100644 --- a/apps/files_external/templates/settings.php +++ b/apps/files_external/templates/settings.php @@ -10,6 +10,7 @@ t('Configuration')); ?> '.$l->t('Available for').''); ?>   +   @@ -78,14 +79,6 @@ - - - - - - - - + <?php p($l->t('Advanced settings')); ?> + + + + + + + class="remove" - style="visibility:hidden;" + class="hidden" ><?php p($l->t('Delete')); ?>' + '' + '' + + '' + 'Delete' + '' + '' + @@ -116,30 +117,57 @@ describe('OCA.External.Settings tests', function() { // TODO: test suggested mount point logic }); describe('saving storages', function() { - it('saves storage after editing config', function() { - var $tr = view.$el.find('tr:first'); - selectBackend('\\OC\\TestBackend'); + var $tr; + beforeEach(function() { + $tr = view.$el.find('tr:first'); + selectBackend('\\OC\\TestBackend'); + }); + it('saves storage after editing config', function() { var $field1 = $tr.find('input[data-parameter=field1]'); expect($field1.length).toEqual(1); $field1.val('test'); $field1.trigger(new $.Event('keyup', {keyCode: 97})); + var $mountOptionsField = $tr.find('input.mountOptions'); + expect($mountOptionsField.length).toEqual(1); + $mountOptionsField.val(JSON.stringify({previews:true})); + clock.tick(4000); expect(fakeServer.requests.length).toEqual(1); var request = fakeServer.requests[0]; expect(request.url).toEqual(OC.webroot + '/index.php/apps/files_external/globalstorages'); - expect(OC.parseQueryString(request.requestBody)).toEqual({ + expect(JSON.parse(request.requestBody)).toEqual({ backendClass: '\\OC\\TestBackend', - 'backendOptions[field1]': 'test', - 'backendOptions[field2]': '', + backendOptions: { + 'field1': 'test', + 'field2': '' + }, mountPoint: 'TestBackend', - priority: '11' + priority: 11, + applicableUsers: [], + applicableGroups: [], + mountOptions: { + 'previews': true + } }); // TODO: respond and check data-id }); + it('saves storage after closing mount options dropdown', function() { + $tr.find('.mountOptionsToggle img').click(); + $tr.find('[name=previews]').trigger(new $.Event('keyup', {keyCode: 97})); + $tr.find('input[data-parameter=field1]').val('test'); + + // does not save inside the dropdown + expect(fakeServer.requests.length).toEqual(0); + + $('body').mouseup(); + + // but after closing the dropdown + expect(fakeServer.requests.length).toEqual(1); + }); // TODO: tests with "applicableUsers" and "applicableGroups" // TODO: test with non-optional config parameters // TODO: test with missing mount point value @@ -157,6 +185,52 @@ describe('OCA.External.Settings tests', function() { describe('recheck storages', function() { // TODO }); + describe('mount options dropdown', function() { + var $tr; + var $td; + + beforeEach(function() { + $tr = view.$el.find('tr:first'); + $td = $tr.find('.mountOptionsToggle'); + selectBackend('\\OC\\TestBackend'); + }); + + it('shows dropdown when clicking on toggle button, hides when clicking outside', function() { + $td.find('img').click(); + + expect($td.find('.dropdown').length).toEqual(1); + + $('body').mouseup(); + + expect($td.find('.dropdown').length).toEqual(0); + }); + + it('reads config from mountOptions field', function() { + $tr.find('input.mountOptions').val(JSON.stringify({previews:false})); + + $td.find('img').click(); + expect($td.find('.dropdown [name=previews]').prop('checked')).toEqual(false); + $('body').mouseup(); + + $tr.find('input.mountOptions').val(JSON.stringify({previews:true})); + $td.find('img').click(); + expect($td.find('.dropdown [name=previews]').prop('checked')).toEqual(true); + }); + + it('writes config into mountOptions field', function() { + $td.find('img').click(); + // defaults to true + var $field = $td.find('.dropdown [name=previews]'); + expect($field.prop('checked')).toEqual(true); + $td.find('.dropdown [name=filesystem_check_changes]').val(2); + $('body').mouseup(); + + expect(JSON.parse($tr.find('input.mountOptions').val())).toEqual({ + previews: true, + filesystem_check_changes: 2 + }); + }); + }); }); describe('applicable user list', function() { // TODO: test select2 retrieval logic diff --git a/core/css/apps.css b/core/css/apps.css index c2816decc5d..2bac961d36f 100644 --- a/core/css/apps.css +++ b/core/css/apps.css @@ -528,3 +528,19 @@ em { -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; opacity: .5; } + +/* generic dropdown style */ +.dropdown { + background:#eee; + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + box-shadow:0 1px 1px #777; + display:block; + margin-right: 0; + position:absolute; + right:0; + width:420px; + z-index:500; + padding:16px; +} + diff --git a/core/js/l10n.js b/core/js/l10n.js index 60ffa949191..fb93d7b789e 100644 --- a/core/js/l10n.js +++ b/core/js/l10n.js @@ -226,3 +226,7 @@ window.t = _.bind(OC.L10N.translate, OC.L10N); */ window.n = _.bind(OC.L10N.translatePlural, OC.L10N); +Handlebars.registerHelper('t', function(app, text) { + return OC.L10N.translate(app, text); +}); + -- 2.39.5