Merge pull request #14925 from owncloud/ext-mountoptions-ui

Mount options GUI for external storage
This commit is contained in:
Robin McCorkell 2015-03-26 21:36:34 +00:00
commit c2909355bf
9 changed files with 366 additions and 28 deletions

View file

@ -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;
}

View file

@ -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}),
@ -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);
});
}
};

View file

@ -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;
}

View file

@ -10,6 +10,7 @@
<th><?php p($l->t('Configuration')); ?></th>
<?php if ($_['isAdminPage']) print_unescaped('<th>'.$l->t('Available for').'</th>'); ?>
<th>&nbsp;</th>
<th>&nbsp;</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"

View file

@ -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

View file

@ -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

View file

@ -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;
}

View file

@ -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);
});

View file

@ -360,7 +360,7 @@ abstract class Common implements \OC\Files\Storage\Storage {
if (!isset($this->watcher)) {
$this->watcher = new Watcher($storage);
$globalPolicy = \OC::$server->getConfig()->getSystemValue('filesystem_check_changes', Watcher::CHECK_ONCE);
$this->watcher->setPolicy($this->getMountOption('filesystem_check_changes', $globalPolicy));
$this->watcher->setPolicy((int)$this->getMountOption('filesystem_check_changes', $globalPolicy));
}
return $this->watcher;
}