diff --git a/src/bower.json b/src/bower.json index 0d0e89f5..1928e289 100644 --- a/src/bower.json +++ b/src/bower.json @@ -5,7 +5,7 @@ "private": true, "dependencies": { "bootstrap": "3.3.6", - "font-awesome": "4.5.0", + "fork-awesome": "1.1.7", "handlebars-helper-intl": "1.1.2", "handlebars": "4.0.11", "sammy": "0.7.6", diff --git a/src/css/style.less b/src/css/style.less index d5565166..21b27fa8 100644 --- a/src/css/style.less +++ b/src/css/style.less @@ -25,7 +25,7 @@ /* * FontAwesome */ -@import "../bower_components/font-awesome/less/font-awesome.less"; +@import "../bower_components/fork-awesome/less/fork-awesome.less"; // Fixes @@ -101,6 +101,10 @@ button { color: transparent; } +.label { + border-radius: 1px; +} + /* * The top heading of the doc * @@ -698,6 +702,62 @@ input[type='radio'].nice-radio { } +/** Groups View **/ +#view-groups { + .panel-heading a { + text-decoration: none; + &.group-delete { + float:right; + color:lighten(@label-danger-bg, 20%); + :hover { + color:@label-danger-bg; + } + } + } + .panel-body { + h3 { + margin-top:0; + } + button.dropdown-toggle { + line-height: 15.666px; + top: -1.666px; + } + .dropdown-menu { + max-height: 200px; + overflow-y: auto; + } + .label-removable { + // The following match properties from regular btn's + display:inline-block; + font-size:14px; + color:#333; + background-color:#f8f8f8; + border: #ccc 1px solid; + font-weight: normal; + margin-bottom:0; + position: relative; + top: -1.666px; + height: 29.666px; + vertical-align: middle; + padding: 6px 12px; + + margin-right:7px; // Spacing between labels + + > a { + margin-left:6px; + padding-left:6px; + border-left: #ccc 1px solid; + color:lighten(@label-info-bg,20); + + text-decoration: none; + } + > a:hover { + color:@label-info-bg; + } + } + } +} + /** Flash messages **/ #flashMessage { max-height: 120px; diff --git a/src/gulpfile.js b/src/gulpfile.js index 5875faf9..e5f183e8 100644 --- a/src/gulpfile.js +++ b/src/gulpfile.js @@ -71,7 +71,7 @@ gulp.task('js-lint', function() { // Fonts gulp.task('fonts', function() { return gulp.src([ - 'bower_components/font-awesome/fonts/*', + 'bower_components/fork-awesome/fonts/*', 'bower_components/source-code-pro/EOT/*.eot', 'bower_components/source-code-pro/OTF/*.otf', 'bower_components/source-code-pro/TTF/*.ttf', diff --git a/src/js/yunohost/controllers/apps.js b/src/js/yunohost/controllers/apps.js index 26630ddc..3bff1be4 100644 --- a/src/js/yunohost/controllers/apps.js +++ b/src/js/yunohost/controllers/apps.js @@ -202,20 +202,23 @@ // Get app information app.get('#/apps/:app', function (c) { c.api('/apps/'+c.params['app']+'?raw', function(data) { // http://api.yunohost.org/#!/app/app_info_get_9 - // Presentation - data.settings.allowed_users = (data.settings.allowed_users) ? data.settings.allowed_users.replace(',', ', ')+"." : y18n.t('everyone_has_access'); + c.api('/users/permissions', function(data_permissions) { - // Multilingual description - data.description = (typeof data.manifest.description[y18n.locale] !== 'undefined') ? - data.manifest.description[y18n.locale] : - data.manifest.description['en'] - ; + // Permissions + data.permissions = data_permissions.permissions[c.params['app']+".main"]["allowed"]; - // Multi Instance settings - data.manifest.multi_instance = data.manifest.multi_instance ? y18n.t('yes') : y18n.t('no'); - data.install_time = new Date(data.settings.install_time * 1000); + // Multilingual description + data.description = (typeof data.manifest.description[y18n.locale] !== 'undefined') ? + data.manifest.description[y18n.locale] : + data.manifest.description['en'] + ; - c.view('app/app_info', data); + // Multi Instance settings + data.manifest.multi_instance = data.manifest.multi_instance ? y18n.t('yes') : y18n.t('no'); + data.install_time = new Date(data.settings.install_time * 1000); + + c.view('app/app_info', data); + }); }); }); @@ -619,150 +622,6 @@ ); }); - // Manage app access - app.get('#/apps/:app/access', function (c) { - c.api('/apps/'+c.params['app']+'?raw', function(data) { // http://api.yunohost.org/#!/app/app_info_get_9 - c.api('/users', function(dataUsers) { - - // allowed_users as array - if (typeof data.settings.allowed_users !== 'undefined') { - if (data.settings.allowed_users.length === 0) { - // Force empty array, means no user has access - data.settings.allowed_users = []; - } - else { - data.settings.allowed_users = data.settings.allowed_users.split(','); - } - } else { - data.settings.allowed_users = []; // Force array - // if 'allowed_users' is undefined, everyone has access - // that means that undefined is different from empty array - data.settings.allow_everyone = true; - } - - // Available users - data.users = []; - $.each(dataUsers.users, function(username, user){ - // Do not list allowed_users in select list - if ( data.settings.allowed_users.indexOf(username) === -1 ) { - data.users.push({ - value: username, - label: user.fullname+' ('+user.mail+')' - }); - } else { - // Complete allowed_users data - data.settings.allowed_users[data.settings.allowed_users.indexOf(username)] = { - username: username, - fullname: user.fullname, - mail: user.mail, - }; - } - }); - - c.view('app/app_access', data); - }); - }); - }); - - // Remove all access - app.get('#/apps/:app/access/remove', function (c) { - c.confirm( - y18n.t('applications'), - y18n.t('confirm_access_remove_all', [c.params['app']]), - function() { - var params = { - apps: c.params['app'], - users: [] - }; - c.api('/access?'+c.serialize(params), function(data) { // http://api.yunohost.org/#!/app/app_removeaccess_delete_12 - store.clear('slide'); - c.redirect('#/apps/'+ c.params['app']+ '/access'); - }, 'DELETE', params); - }, - function() { - store.clear('slide'); - c.redirect('#/apps/'+ c.params['app']+ '/access'); - } - ); - }); - - // Remove access to a specific user - app.get('#/apps/:app/access/remove/:user', function (c) { - c.confirm( - y18n.t('applications'), - y18n.t('confirm_access_remove_user', [c.params['app'], c.params['user']]), - function() { - var params = { - apps: c.params['app'], - users: c.params['user'] - }; - c.api('/access?'+c.serialize(params), function(data) { // http://api.yunohost.org/#!/app/app_removeaccess_delete_12 - store.clear('slide'); - c.redirect('#/apps/'+ c.params['app']+ '/access'); - }, 'DELETE', params); // passing 'params' here is useless because jQuery doesn't handle ajax datas for DELETE requests. Passing parameters through uri. - }, - function() { - store.clear('slide'); - c.redirect('#/apps/'+ c.params['app']+ '/access'); - } - ); - }); - - // Grant all access - app.get('#/apps/:app/access/add', function (c) { - c.confirm( - y18n.t('applications'), - y18n.t('confirm_access_add', [c.params['app']]), - function() { - var params = { - apps: c.params['app'], - users: null - }; - c.api('/access', function() { // http://api.yunohost.org/#!/app/app_addaccess_put_13 - store.clear('slide'); - c.redirect('#/apps/'+ c.params['app'] +'/access'); - }, 'PUT', params); - }, - function() { - store.clear('slide'); - c.redirect('#/apps/'+ c.params['app']+ '/access'); - } - ); - }); - - // Grant access for a specific user - app.post('#/apps/:app/access/add', function (c) { - var params = { - users: c.params['user'], - apps: c.params['app'] - }; - c.api('/access', function() { // http://api.yunohost.org/#!/app/app_addaccess_put_13 - store.clear('slide'); - c.redirect('#/apps/'+ c.params['app'] +'/access'); - }, 'PUT', params); - }); - - // Clear access (reset) - app.get('#/apps/:app/access/clear', function (c) { - c.confirm( - y18n.t('applications'), - y18n.t('confirm_access_clear', [c.params['app']]), - function() { - var params = { - apps: c.params['app'] - }; - c.api('/access', function() { // - store.clear('slide'); - c.redirect('#/apps/'+ c.params['app'] +'/access'); - }, 'POST', params); - }, - function() { - store.clear('slide'); - c.redirect('#/apps/'+ c.params['app']+ '/access'); - } - ); - }); - // Make app default app.get('#/apps/:app/default', function (c) { c.confirm( diff --git a/src/js/yunohost/controllers/users.js b/src/js/yunohost/controllers/users.js index 92e235ad..655ff4c5 100644 --- a/src/js/yunohost/controllers/users.js +++ b/src/js/yunohost/controllers/users.js @@ -5,6 +5,196 @@ var PASSWORD_MIN_LENGTH = 4; + // A small utility to convert a string to title case + // e.g. "hAvE a NicE dAy" --> "Have A Nice Day" + // Savagely stolen from https://stackoverflow.com/a/196991 + function toTitleCase(str) { + return str.replace( + /\w\S*/g, + function(txt) { + return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); + } + ); + } + + /** + * Groups and permissions + * + */ + + /** + * Update group or permissions + * + * @model data organize in the same way than /users/groups?full&include_primary_groups + * @params.operation "add"|"remove" + * @params.type "members"|"permissions" + * @param.item Name of the user or the permission to add or remove + * @param.group Name of the group affected + * + * This function is built to be apply with params generated by the use of + * HTML dataset attributes (e.g. link in the partial inline view "label" in group_list.ms) + * + * @return void + **/ + function updateGroup(model, params) { + var type = params.type; + var operation = params.operation; + var item = params.item; + var groupname = params.group; + var group = data.groups[groupname]; + var to = (operation == 'add')?group[type]:group[type + 'Inv']; + var from = (operation == 'add')?group[type+'Inv']:group[type]; + // Do nothing, if array of destination already contains the item + if (from.indexOf(item) === -1) return; + + // Hack to disable pacman loader if any + if ($('div.loader').length === 0) { + $('#main').append('
'); + } + $('div.loader').css('display', 'none'); + + // Update group + var params = {}; var url; + if (type == 'members') { + url = '/users/groups/' + groupname; + params[operation] = [item]; + } + else { + url = '/users/permissions/' + item; + params[operation] = [groupname]; + } + c.api(url, function(data_update) { + to.push(item); + from.splice(from.indexOf(item), 1); + updateView(data); + }, 'PUT', params); + } + + /** + * Update the view with the new model + * + * @model data organize in the same way than /users/groups?full&include_primary_groups + * + * @return void + **/ + function updateView(model) { + // Sort in aphanumerical order to improve user experience + for (var group in model.groups) { + model.groups[group].permissions.sort(); + model.groups[group].permissionsInv.sort(); + model.groups[group].members.sort(); + model.groups[group].membersInv.sort(); + } + + // Manual render, we don't use c.view to avoid scrollTop and other + // uneeded behaviour + var rendered = c.render('views/user/group_list.ms', model); + rendered.swap(function () { + // Add click event to get a nice "reactive" interface + jQuery(".group-update").on('click', function (e) { + updateGroup(model, jQuery(this)[0].dataset); + return false; + }); + jQuery(".group-add-user").on('click', function (e) { + data.groups[$(this)[0].dataset.user].display = true; + updateView(data); + return false; + }); + }); + } + + + app.get('#/groups', function (c) { + c.api('/users/groups?full&include_primary_groups', function(data_groups) { + c.api('/users', function(data_users) { + c.api('/users/permissions?short', function(data_permissions) { + //var perms = data_permissions.permissions; + var specific_perms = {}; + var all_perms = data_permissions.permissions; + var users = Object.keys(data_users.users); + + // Enrich groups data with primary group indicator and inversed items list + for (var group in data_groups.groups) { + data_groups.groups[group].primary = users.indexOf(group) !== -1; + data_groups.groups[group].permissionsInv = all_perms.filter(function(item) { + return data_groups.groups[group].permissions.indexOf(item) === -1; + }).filter(function(item) { + return group != "visitors" || (item != "mail.main" && item != "xmpp.main"); // Remove 'email' and 'xmpp' in visitors's permission choice list + }); + data_groups.groups[group].membersInv = users.filter(function(item) { + return data_groups.groups[group].members.indexOf(item) === -1; + }); + } + + // Declare all_users and visitors has special + data_groups.groups['all_users'].special = true; + data_groups.groups['visitors'].special = true; + + // Data given to the view with 2 functions to convert technical + // permission id to display names + data = { + 'groups':data_groups.groups, + 'displayPermission': function (text) { + // Display a permission correctly for a human + text = text.replace('.main', ''); + if (text.indexOf('.') > -1) + text = text.replace('.', ' (') + ')'; + + if (text == "mail") + text = "E-mail"; + else if (text == "xmpp") + text = "XMPP"; + else + text = toTitleCase(text); + + return text; + }, + 'displayUser': function (text) { + return text; + }, + }; + updateView(data); + }); + }); + }); + }); + + // Create a new group + app.get('#/groups/create', function (c) { + c.view('user/group_create', {}); + }); + + app.post('#/groups/create', function (c) { + c.params['groupname'] = c.params['groupname'].replace(' ', '_').toLowerCase(); + c.api('/users/groups', function(data) { + c.redirect('#/groups'); + }, 'POST', c.params.toHash()); + }); + + app.get('#/groups/:group/delete', function (c) { + + var params = {}; + + // make confirm content + var confirmModalContent = $('{{t 'everyone_has_access'}}
- {{else}} -{{t 'no_allowed_users'}}
- {{/if}} -{{t 'app_access_addall_desc' settings.label}}
- - {{t 'app_access_addall_btn'}} - -{{t 'app_access_removeall_desc' settings.label}}
- - {{t 'app_access_removeall_btn'}} - -{{t 'app_access_clearall_desc' settings.label}}
- - {{t 'app_access_clearall_btn'}} - -{{t 'app_info_access_desc' settings.allowed_users}}
- - {{t 'app_access'}} +{{t 'app_info_access_desc'}} {{#each permissions}} {{ucwords .}}{{#unless @last}}, {{/unless}} {{ else }} {{t 'nobody'}} {{/each}} +
+ + {{t 'groups_and_permissions_manage'}}