diff options
author | Robin McCorkell <rmccorkell@karoshi.org.uk> | 2015-03-26 21:36:34 +0000 |
---|---|---|
committer | Robin McCorkell <rmccorkell@karoshi.org.uk> | 2015-03-26 21:36:34 +0000 |
commit | c2909355bf66c92e46c9fbea39497ba91dbe37a0 (patch) | |
tree | c832f4ba40ba1728ddd82bc5684ca6b4aa9eb3b6 /apps/files_external | |
parent | fc59a37ae7e28ad8ef7a0a383e87024ff942408d (diff) | |
parent | 58b4c2c0e53ff5ef6e261426ecb2b94d9791da71 (diff) | |
download | nextcloud-server-c2909355bf66c92e46c9fbea39497ba91dbe37a0.tar.gz nextcloud-server-c2909355bf66c92e46c9fbea39497ba91dbe37a0.zip |
Merge pull request #14925 from owncloud/ext-mountoptions-ui
Mount options GUI for external storage
Diffstat (limited to 'apps/files_external')
-rw-r--r-- | apps/files_external/css/settings.css | 13 | ||||
-rw-r--r-- | apps/files_external/js/settings.js | 179 | ||||
-rw-r--r-- | apps/files_external/lib/config.php | 17 | ||||
-rw-r--r-- | apps/files_external/templates/settings.php | 24 | ||||
-rw-r--r-- | apps/files_external/tests/js/settingsSpec.js | 88 | ||||
-rw-r--r-- | apps/files_external/tests/mountconfig.php | 51 |
6 files changed, 345 insertions, 27 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 = + '<div class="drop dropdown mountOptionsDropdown">' + + // FIXME: options are hard-coded for now + ' <div class="optionRow">' + + ' <label for="mountOptionsPreviews">{{t "files_external" "Enable previews"}}</label>' + + ' <input id="mountOptionsPreviews" name="previews" type="checkbox" value="true" checked="checked"/>' + + ' </div>' + + ' <div class="optionRow">' + + ' <label for="mountOptionsFilesystemCheck">{{t "files_external" "Check for changes"}}</label>' + + ' <select id="mountOptionsFilesystemCheck" name="filesystem_check_changes" data-type="int">' + + ' <option value="0">{{t "files_external" "Never"}}</option>' + + ' <option value="1" selected="selected">{{t "files_external" "Once every direct access"}}</option>' + + ' <option value="2">{{t "files_external" "Every time the filesystem is used"}}</option>' + + ' </select>' + + ' </div>' + + '</div>'; + /** * 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}), @@ -379,6 +398,110 @@ UserStorageConfig.prototype = _.extend({}, StorageConfig.prototype, }); /** + * @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 * * @classdesc Mount configuration list view @@ -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/lib/config.php b/apps/files_external/lib/config.php index 103d98cd1bf..40e9a75790f 100644 --- a/apps/files_external/lib/config.php +++ b/apps/files_external/lib/config.php @@ -156,8 +156,6 @@ class OC_Mount_Config { public static function getAbsoluteMountPoints($user) { $mountPoints = array(); - $datadir = \OC_Config::getValue("datadirectory", \OC::$SERVERROOT . "/data"); - $backends = self::getBackends(); // Load system mount points @@ -287,12 +285,21 @@ class OC_Mount_Config { /** * fill in the correct values for $user * - * @param string $user - * @param string $input + * @param string $user user value + * @param string|array $input * @return string */ private static function setUserVars($user, $input) { - return str_replace('$user', $user, $input); + if (is_array($input)) { + foreach ($input as &$value) { + if (is_string($value)) { + $value = str_replace('$user', $user, $value); + } + } + } else { + $input = str_replace('$user', $user, $input); + } + return $input; } 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 @@ <th><?php p($l->t('Configuration')); ?></th> <?php if ($_['isAdminPage']) print_unescaped('<th>'.$l->t('Available for').'</th>'); ?> <th> </th> + <th> </th> </tr> </thead> <tbody> @@ -78,14 +79,6 @@ <?php OCP\Util::addScript('files_external', $_['backends'][$mount['class']]['custom']); ?> <?php endif; ?> <?php endif; ?> - <?php if (isset($mount['mountOptions'])): ?> - <input type="hidden" class="mountOptions" value="<?php p(json_encode($mount['mountOptions'])) ?>" /> - <?php endif; ?> - <?php if ($_['isAdminPage']): ?> - <?php if (isset($mount['priority'])): ?> - <input type="hidden" class="priority" value="<?php p($mount['priority']) ?>" /> - <?php endif; ?> - <?php endif; ?> </td> <?php if ($_['isAdminPage']): ?> <td class="applicable" @@ -97,8 +90,21 @@ <input type="hidden" class="applicableUsers" style="width:20em;" value=""/> </td> <?php endif; ?> + <td class="mountOptionsToggle <?php if (!isset($mount['mountpoint'])) { p('hidden'); } ?>" + ><img + class="svg action" + title="<?php p($l->t('Advanced settings')); ?>" + alt="<?php p($l->t('Advanced settings')); ?>" + src="<?php print_unescaped(image_path('core', 'actions/settings.svg')); ?>" /> + <input type="hidden" class="mountOptions" value="<?php isset($mount['mountOptions']) ? p(json_encode($mount['mountOptions'])) : '' ?>" /> + <?php if ($_['isAdminPage']): ?> + <?php if (isset($mount['priority'])): ?> + <input type="hidden" class="priority" value="<?php p($mount['priority']) ?>" /> + <?php endif; ?> + <?php endif; ?> + </td> <td <?php if (isset($mount['mountpoint'])): ?>class="remove" - <?php else: ?>style="visibility:hidden;" + <?php else: ?>class="hidden" <?php endif ?>><img alt="<?php p($l->t('Delete')); ?>" title="<?php p($l->t('Delete')); ?>" class="svg action" diff --git a/apps/files_external/tests/js/settingsSpec.js b/apps/files_external/tests/js/settingsSpec.js index 5a3ee2cb5f1..f030965835a 100644 --- a/apps/files_external/tests/js/settingsSpec.js +++ b/apps/files_external/tests/js/settingsSpec.js @@ -43,6 +43,7 @@ describe('OCA.External.Settings tests', function() { '<td class="applicable">' + '<input type="hidden" class="applicableUsers">' + '</td>' + + '<td class="mountOptionsToggle"><input type="hidden" class="mountOptions"/><img class="svg action"/></td>' + '<td><img alt="Delete" title="Delete" class="svg action"/></td>' + '</tr>' + '</tbody>' + @@ -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/apps/files_external/tests/mountconfig.php b/apps/files_external/tests/mountconfig.php index a3b993f5018..b76ba0a39a6 100644 --- a/apps/files_external/tests/mountconfig.php +++ b/apps/files_external/tests/mountconfig.php @@ -123,7 +123,6 @@ class Test_Mount_Config extends \Test\TestCase { private $dataDir; private $userHome; private $oldAllowedBackends; - private $allBackends; const TEST_USER1 = 'user1'; const TEST_USER2 = 'user2'; @@ -213,6 +212,11 @@ class Test_Mount_Config extends \Test\TestCase { return json_decode(file_get_contents($configFile), true); } + private function writeGlobalConfig($config) { + $configFile = $this->dataDir . '/mount.json'; + file_put_contents($configFile, json_encode($config)); + } + /** * Reads the user config, for checking */ @@ -630,6 +634,51 @@ class Test_Mount_Config extends \Test\TestCase { $this->assertEquals($mountConfig, $savedMountConfig); } + public function testVariableSubstitution() { + $legacyBackendOptions = [ + 'user' => 'someuser', + 'password' => 'somepassword', + 'replacethis' => '$user', + ]; + $legacyBackendOptions = \OC_Mount_Config::encryptPasswords($legacyBackendOptions); + + $legacyConfig = [ + 'class' => '\OC\Files\Storage\SMB', + 'options' => $legacyBackendOptions, + 'mountOptions' => ['preview' => false, 'int' => 1], + ]; + // different mount options + $legacyConfig2 = [ + 'class' => '\OC\Files\Storage\SMB', + 'options' => $legacyBackendOptions, + 'mountOptions' => ['preview' => true, 'string' => 'abc'], + ]; + + $json = [ + 'user' => [ + self::TEST_USER1 => [ + '/$user/files/somemount' => $legacyConfig, + '/$user/files/anothermount' => $legacyConfig2, + ], + ], + ]; + + $this->writeGlobalConfig($json); + + // re-read config, password was read correctly + $config = OC_Mount_Config::getAbsoluteMountPoints(self::TEST_USER1); + + $config1 = $config['/' . self::TEST_USER1 . '/files/somemount']; + $config2 = $config['/' . self::TEST_USER1 . '/files/anothermount']; + + $this->assertSame(self::TEST_USER1, $config1['options']['replacethis']); + $this->assertSame(self::TEST_USER1, $config1['options']['replacethis']); + $this->assertSame(1, $config1['mountOptions']['int']); + $this->assertSame(true, $config2['mountOptions']['preview']); + $this->assertSame('abc', $config2['mountOptions']['string']); + } + + public function mountDataProvider() { return array( // Tests for visible mount points |