diff --git a/media/css/seahub.css b/media/css/seahub.css index ec8ba519a2..f57222a95a 100644 --- a/media/css/seahub.css +++ b/media/css/seahub.css @@ -3758,3 +3758,22 @@ img.thumbnail { #group-members .outer-caret { right:66px; } +#group-settings { + position:absolute; + top:77px; + right:0; +} +#group-settings .outer-caret { + right:106px; +} +.group-setting-list { + border-bottom:1px solid #e3e3e5; +} +.group-setting-item { + line-height:31px; + cursor:pointer; + margin:5px 0; +} +#add-group-members-form .submit { + margin:0 0 0 5px; +} diff --git a/seahub/templates/js/templates.html b/seahub/templates/js/templates.html index 14b9ccab59..a7958d6a6b 100644 --- a/seahub/templates/js/templates.html +++ b/seahub/templates/js/templates.html @@ -66,7 +66,7 @@ <%= size_formatted %> <%= mtime_relative %> - <%- owner_nickname %> + <%- owner_name %> + + + + + + diff --git a/seahub/templates/libraries.html b/seahub/templates/libraries.html index 96e0fb9769..86025ac746 100644 --- a/seahub/templates/libraries.html +++ b/seahub/templates/libraries.html @@ -159,6 +159,20 @@

+ +
+
+
+ +

{% trans "Settings" %}

+
+
+ +
+

+
+
+ diff --git a/static/scripts/app/router.js b/static/scripts/app/router.js index 173c7e9d5f..e6b22bc8b9 100644 --- a/static/scripts/app/router.js +++ b/static/scripts/app/router.js @@ -26,6 +26,7 @@ define([ 'group/:group_id/': 'showGroupRepos', 'group/:group_id/lib/:repo_id(/*path)': 'showGroupRepoDir', 'group/:group_id/members/': 'showGroupMembers', + 'group/:group_id/settings/': 'showGroupSettings', 'org/': 'showOrgRepos', 'org/lib/:repo_id(/*path)': 'showOrgRepoDir', 'common/lib/:repo_id(/*path)': 'showCommonDir', @@ -184,6 +185,11 @@ define([ this.groupView.showMembers(); }, + showGroupSettings: function(group_id) { + this.showGroupRepos(group_id); + this.groupView.showSettings(); + }, + showOrgRepos: function() { this.switchCurrentView(this.orgView); this.orgView.showRepoList(); diff --git a/static/scripts/app/views/group-manage-members.js b/static/scripts/app/views/group-manage-members.js new file mode 100644 index 0000000000..bf1b6b1b9e --- /dev/null +++ b/static/scripts/app/views/group-manage-members.js @@ -0,0 +1,165 @@ +define([ + 'jquery', + 'underscore', + 'backbone', + 'common', + 'app/collections/group-members', + 'app/views/group-member2' +], function($, _, Backbone, Common, GroupMembers, ItemView) { + 'use strict'; + + var View = Backbone.View.extend({ + + template: _.template($('#group-manage-members-tmpl').html()), + + initialize: function(options) { + + this.group_id = options.group_id; + this.group_name = options.group_name; + this.is_owner = options.is_owner; + + this.render(); + this.$el.modal({ + appendTo: '#main', + focus: false, + containerCss: { + 'width': 560 + } + }); + this.$modalContainer = $('#simplemodal-container').css({'height':'auto'}); + + this.$('[name="user_name"]').select2($.extend({ + width: '268px', + }, Common.contactInputOptionsForSelect2())); + + this.collection = new GroupMembers(); + this.listenTo(this.collection, 'add', this.addOne); + this.renderMemberList(); + this.setConMaxHeight(); + + this.$loadingTip = this.$('.loading-tip'); + this.$listContainer = this.$('tbody'); + this.$listError = this.$('.members .error'); + + var _this = this; + $(window).resize(function() { + _this.setConMaxHeight(); + }); + // click other place to hide '.role-edit' + $(document).click(function(e) { + var target = e.target || event.srcElement; + var $el = _this.$('.role-edit:visible'); + var $td = $el.parent(); + if ($el.length && + !$el.is(target) && + !$el.find('*').is(target) && + !$td.find('.role-edit-icon').is(target)) { + $el.hide(); + $td.find('.cur-role, .role-edit-icon').show(); + } + }); + }, + + render: function() { + var title = gettext("{placeholder} Members").replace('{placeholder}', '' + Common.HTMLescape(this.group_name) + ''); + this.$el.html(this.template({ + title: title, + is_owner: this.is_owner + })); + + return this; + }, + + events: { + 'submit form': 'formSubmit' + }, + + addOne: function(item, collection, options) { + var view = new ItemView({ + model: item, + group_id: this.group_id, + is_owner: this.is_owner + }); + if (options.prepend) { + this.$listContainer.prepend(view.render().el); + } else { + this.$listContainer.append(view.render().el); + } + }, + + renderMemberList: function() { + var _this = this; + this.collection.setGroupId(this.group_id); + this.collection.fetch({ + cache: false, + data: {'avatar_size': 40}, + success: function(collection, response, opts) { + _this.$loadingTip.hide(); + }, + error: function(collection, response, opts) { + _this.$loadingTip.hide(); + var err_msg; + if (response.responseText) { + if (response['status'] == 401 || response['status'] == 403) { + err_msg = gettext("Permission error"); + } else { + err_msg = gettext("Error"); + } + } else { + err_msg = gettext('Please check the network.'); + } + _this.$listError.html(err_msg).show(); + } + }); + }, + + formSubmit: function() { + var _this = this; + var $input = this.$('[name="user_name"]'); + var input_val = $.trim($input.val()); + if (!input_val) { + return false; + } + var input_val_list = input_val.split(','); + if (input_val_list.length == 1) { + this.collection.create({'email': input_val}, { + wait: true, + validate: true, + prepend: true, + success: function() { + $input.select2('val', ''); + }, + error: function(collection, response, options) { + var err_msg; + if (response.responseText) { + err_msg = response.responseJSON.error_msg; + } else { + err_msg = gettext('Please check the network.'); + } + _this.$listError.html(err_msg).show(); + } + }); + } else { + // TODO: input_val_list.length > 1 + } + + return false; + }, + + setConMaxHeight: function() { + var $modalContainer = this.$modalContainer; + this.$('.members').css({ + 'max-height': $(window).height() + - parseInt($modalContainer.css('top')) + - parseInt($modalContainer.css('padding-top')) + - parseInt($modalContainer.css('padding-bottom')) + - this.$('h3').outerHeight(true) + - this.$('form').outerHeight(true), + 'overflow': 'auto' + }); + } + + }); + + return View; +}); diff --git a/static/scripts/app/views/group-member2.js b/static/scripts/app/views/group-member2.js new file mode 100644 index 0000000000..7b1a42fc07 --- /dev/null +++ b/static/scripts/app/views/group-member2.js @@ -0,0 +1,116 @@ +define([ + 'jquery', + 'underscore', + 'backbone', + 'common' +], function($, _, Backbone, Common) { + 'use strict'; + + var View = Backbone.View.extend({ + tagName: 'tr', + + template: _.template($('#group-member2-tmpl').html()), + + events: { + 'mouseenter': 'highlight', + 'mouseleave': 'rmHighlight', + 'click .role-edit-icon': 'showEdit', + 'change .role-edit': 'editRole', + 'click .rm': 'rmMember' + }, + + initialize: function(options) { + this.group_id = options.group_id; + this.is_owner = options.is_owner; + + this.listenTo(this.model, 'change', this.render); + }, + + render: function() { + this.$el.html(this.template($.extend(this.model.attributes, { + is_owner: this.is_owner, + username: app.pageOptions.username + }))); + return this; + }, + + highlight: function() { + this.$el.addClass('hl').find('.op-icon').removeClass('vh'); + }, + + rmHighlight: function() { + this.$el.removeClass('hl').find('.op-icon').addClass('vh'); + }, + + showEdit: function() { + this.$('.cur-role, .role-edit-icon').hide(); + this.$('.role-edit').show(); + }, + + editRole: function() { + var _this = this; + + // '0': member, '1': admin + var val = this.$('[name="role"]').val(); + var is_admin = val == 1 ? true : false; + $.ajax({ + url: Common.getUrl({ + 'name': 'group_member', + 'group_id': this.group_id, + 'email': encodeURIComponent(this.model.get('email')), + }), + type: 'put', + dataType: 'json', + beforeSend: Common.prepareCSRFToken, + data: { + 'is_admin': is_admin + }, + success: function() { + _this.model.set({ + 'is_admin': is_admin + }); + }, + error: function(xhr) { + var err_msg; + if (xhr.responseText) { + err_msg = $.parseJSON(xhr.responseText).error_msg; + } else { + err_msg = gettext("Failed. Please check the network."); + } + // improve it? + Common.feedback(error_msg, 'error'); + } + }); + }, + + rmMember: function() { + var _this = this; + $.ajax({ + url: Common.getUrl({ + 'name': 'group_member', + 'group_id': this.group_id, + 'email': encodeURIComponent(this.model.get('email')), + }), + type: 'delete', + dataType: 'json', + beforeSend: Common.prepareCSRFToken, + success: function() { + _this.remove(); + }, + error: function(xhr) { + var err_msg; + if (xhr.responseText) { + err_msg = $.parseJSON(xhr.responseText).error_msg; + } else { + err_msg = gettext("Failed. Please check the network."); + } + // improve it? + Common.feedback(error_msg, 'error'); + } + }); + } + + }); + + return View; +}); diff --git a/static/scripts/app/views/group-repo.js b/static/scripts/app/views/group-repo.js index 63505cd62b..ca34ca8f13 100644 --- a/static/scripts/app/views/group-repo.js +++ b/static/scripts/app/views/group-repo.js @@ -28,7 +28,11 @@ define([ var obj = this.model.toJSON(); $.extend(obj, { group_id: this.group_id, - is_staff: this.is_staff + is_staff: this.is_staff, + // for '#groups' (no 'share_from_me') + share_from_me: app.pageOptions.username == this.model.get('owner') ? true : false, + // 'owner_name' for '#groups', 'owner_nickname' for '#group/id/' + owner_name: this.model.get('owner_nickname') || this.model.get('owner_name') }); this.$el.html(this.template(obj)); return this; diff --git a/static/scripts/app/views/group-settings.js b/static/scripts/app/views/group-settings.js new file mode 100644 index 0000000000..78ec738a12 --- /dev/null +++ b/static/scripts/app/views/group-settings.js @@ -0,0 +1,370 @@ +define([ + 'jquery', + 'underscore', + 'backbone', + 'common', + 'app/views/group-manage-members' +], function($, _, Backbone, Common, ManageMembersView) { + 'use strict'; + + var View = Backbone.View.extend({ + el: '#group-settings', + + template: _.template($('#group-settings-tmpl').html()), + renameTemplate: _.template($('#group-rename-form-tmpl').html()), + transferTemplate: _.template($('#group-transfer-form-tmpl').html()), + importMembersTemplate: _.template($('#group-import-members-form-tmpl').html()), + + initialize: function(options) { + // group basic info + this.group = {}; + + this.$loadingTip = this.$('.loading-tip'); + this.$listContainer = $('#group-setting-con'); + this.$error = this.$('.error'); + + var _this = this; + $(window).resize(function() { + _this.setConMaxHeight(); + }); + $(document).click(function(e) { + var target = e.target || event.srcElement; + var $popup = _this.$el, + $popup_switch = $('#group-settings-icon'); + + if ($('#group-settings:visible').length && + !$popup.is(target) && + !$popup.find('*').is(target) && + !$popup_switch.is(target)) { + _this.hide(); + } + }); + }, + + events: { + 'click .close': 'hide', + 'mouseenter .group-setting-item': 'highlightItem', + 'mouseleave .group-setting-item': 'rmHighlightItem', + 'click .group-setting-item': 'manageGroup' + }, + + render: function() { + this.$error.hide(); + this.$listContainer.hide(); + this.$loadingTip.show(); + + // the user's role in this group + this.is_owner = false, + this.is_admin = false; + + var _this = this; + $.ajax({ + url: Common.getUrl({ + 'name': 'group', + 'group_id': this.group.id + }), + cache: false, + dataType: 'json', + success: function (data) { + _this.group = data; // {id, name, owner, created_at, avatar_url, admins} + + var username = app.pageOptions.username; + if (username == _this.group.owner) { + _this.is_owner = true; + } else if ($.inArray(username, _this.group.admins) != -1) { + _this.is_admin = true; + } + _this.$listContainer.html(_this.template({ + 'is_owner': _this.is_owner, + 'is_admin': _this.is_admin + })).show(); + }, + error: function(xhr) { + var err_msg; + if (xhr.responseText) { + err_msg = gettext('Error'); + } else { + err_msg = gettext("Please check the network."); + } + _this.$error.html(err_msg).show(); + }, + complete: function() { + _this.$loadingTip.hide(); + } + }); + }, + + // set max-height for '.popover-con' + setConMaxHeight: function() { + this.$('.popover-con').css({'max-height': $(window).height() - this.$el.offset().top - this.$('.popover-hd').outerHeight(true) - 2}); // 2: top, bottom border width of $el + }, + + show: function(options) { + this.group.id = options.group_id; + this.$el.show(); + this.setConMaxHeight(); + this.render(); + app.router.navigate('group/' + this.group.id + '/settings/'); + }, + + hide: function() { + this.$el.hide(); + app.router.navigate('group/' + this.group.id + '/'); + }, + + highlightItem: function(e) { + $(e.currentTarget).addClass('hl'); + }, + + rmHighlightItem: function(e) { + $(e.currentTarget).removeClass('hl'); + }, + + manageGroup: function(e) { + switch($(e.currentTarget).data('op')) { + case 'rename': + this.rename(); + break; + case 'transfer': + this.transfer(); + break; + case 'import-members': + this.importMembers(); + break; + case 'manage-members': + this.manageMembers(); + break; + case 'dismiss': + this.dismiss(); + break; + case 'leave': + this.leave(); + break; + } + }, + + rename: function() { + var _this = this; + + var $form = $(this.renameTemplate()); + $form.modal({focus:false}); + $('#simplemodal-container').css({'width':'auto', 'height':'auto'}); + + $form.submit(function() { + var new_name = $.trim($('[name="new_name"]', $(this)).val()); + if (!new_name || new_name == _this.group.name) { + return false; + } + var $submitBtn = $('[type="submit"]', $(this)); + Common.disableButton($submitBtn); + $.ajax({ + url: Common.getUrl({ + 'name': 'group', + 'group_id': _this.group.id + }), + type: 'put', + dataType: 'json', + beforeSend: Common.prepareCSRFToken, + data: { + 'name': new_name + }, + success: function() { + $.modal.close(); + // TODO: improve + //app.router.navigate('group/' + _this.group_id + '/', {trigger: true, replace: true}); + location.reload(true); + }, + error: function(xhr) { + var error_msg; + if (xhr.responseText) { + error_msg = $.parseJSON(xhr.responseText).error_msg; + } else { + error_msg = gettext("Failed. Please check the network."); + } + $('.error', $form).html(error_msg).show(); + Common.enableButton($submitBtn); + } + }); + return false; + }); + }, + + transfer: function() { + var _this = this; + + var $form = $(this.transferTemplate()); + $form.modal({focus:false}); + $('#simplemodal-container').css({'width':'auto', 'height':'auto'}); + + $('[name="email"]', $form).select2($.extend( + Common.contactInputOptionsForSelect2(), { + width: '268px', + maximumSelectionSize: 1, + placeholder: gettext("Search user or enter email"), // to override 'placeholder' returned by `Common.conta...` + formatSelectionTooBig: gettext("You cannot select any more choices") + })); + + $form.submit(function() { + var email = $.trim($('[name="email"]', $(this)).val()); + if (!email) { + return false; + } + if (email == _this.group.owner) { + return false; + } + + var $submitBtn = $('[type="submit"]', $(this)); + Common.disableButton($submitBtn); + $.ajax({ + url: Common.getUrl({ + 'name': 'group', + 'group_id': _this.group.id + }), + type: 'put', + dataType: 'json', + beforeSend: Common.prepareCSRFToken, + data: { + 'owner': email + }, + success: function() { + // after the transfer, the former owner becomes a common admin of the group. + $.modal.close(); + }, + error: function(xhr) { + var error_msg; + if (xhr.responseText) { + error_msg = $.parseJSON(xhr.responseText).error_msg; + } else { + error_msg = gettext("Failed. Please check the network."); + } + $('.error', $form).html(error_msg).show(); + Common.enableButton($submitBtn); + } + }); + return false; + }); + }, + + // TODO: finish it after the backend py is done. + importMembers: function() { + var _this = this; + var $form = $(this.importMembersTemplate()); + $form.modal({focus:false}); + $('#simplemodal-container').css({'width':'auto', 'height':'auto'}); + + $form.submit(function() { + var $fileInput = $('[name=file]', $form)[0]; + if (!$fileInput.files.length) { + $('.error', $form).removeClass('hide'); + return false; + } + + var $submitBtn = $('[type="submit"]', $(this)); + Common.disableButton($submitBtn); + + var file = $fileInput.files[0]; + var formData = new FormData(); + formData.append('file', file); + $.ajax({ + url: Common.getUrl({ + 'name': 'group_import_members', + 'group_id': _this.group.id + }), + type: 'post', + dataType: 'json', + data: formData, + processData: false, // tell jQuery not to process the data + contentType: false, // tell jQuery not to set contentType + beforeSend: Common.prepareCSRFToken, + success: function(data) { + }, + error: function () { + } + }); + return false; + }); + }, + + manageMembers: function() { + new ManageMembersView({ + 'group_id': this.group.id, + 'group_name': this.group.name, + 'is_owner': this.is_owner + }); + }, + + dismiss: function() { + var _this = this; + var title = gettext('Dismiss Group'); + var content = gettext('Really want to dismiss this group?'); + var yesCallback = function () { + $.ajax({ + url: Common.getUrl({ + 'name': 'group', + 'group_id': _this.group.id + }), + type: 'delete', + dataType: 'json', + beforeSend: Common.prepareCSRFToken, + success: function() { + app.router.navigate('groups/', {trigger: true}); + // TO update side nav - 'group list' + location.reload(true); // improve it ?? + }, + error: function(xhr) { + var error_msg; + if (xhr.responseText) { + error_msg = $.parseJSON(xhr.responseText).error_msg; + } else { + error_msg = gettext("Failed. Please check the network."); + } + Common.feedback(error_msg, 'error'); + }, + complete: function() { + $.modal.close(); + } + }); + }; + Common.showConfirm(title, content, yesCallback); + }, + + leave: function() { + var _this = this; + var title = gettext('Quit Group'); + var content = gettext('Are you sure you want to quit this group?'); + var yesCallback = function () { + $.ajax({ + url: Common.getUrl({ + 'name': 'group_member', + 'group_id': _this.group.id, + 'email': encodeURIComponent(app.pageOptions.username), + }), + type: 'delete', + dataType: 'json', + beforeSend: Common.prepareCSRFToken, + success: function() { + app.router.navigate('groups/', {trigger: true}); + // TO update side nav - 'group list' + location.reload(true); // improve it ?? + }, + error: function(xhr) { + var err_msg; + if (xhr.responseText) { + err_msg = $.parseJSON(xhr.responseText).error_msg; + } else { + err_msg = gettext("Failed. Please check the network."); + } + Common.feedback(error_msg, 'error'); + }, + complete: function() { + $.modal.close(); + } + }); + }; + Common.showConfirm(title, content, yesCallback); + } + + }); + + return View; +}); diff --git a/static/scripts/app/views/group.js b/static/scripts/app/views/group.js index 018c4f4fce..c1c65595be 100644 --- a/static/scripts/app/views/group.js +++ b/static/scripts/app/views/group.js @@ -6,9 +6,10 @@ define([ 'app/collections/group-repos', 'app/views/group-repo', 'app/views/add-group-repo', - 'app/views/group-members' + 'app/views/group-members', + 'app/views/group-settings' ], function($, _, Backbone, Common, GroupRepos, GroupRepoView, - AddGroupRepoView, GroupMembersView) { + AddGroupRepoView, GroupMembersView, GroupSettingsView) { 'use strict'; var GroupView = Backbone.View.extend({ @@ -18,6 +19,7 @@ define([ reposHdTemplate: _.template($('#shared-repos-hd-tmpl').html()), events: { + 'click #group-settings-icon': 'toggleSettingsPanel', 'click #group-members-icon': 'toggleMembersPanel', 'click .repo-create': 'createRepo', 'click .by-name': 'sortByName', @@ -39,6 +41,7 @@ define([ this.dirView = options.dirView; this.membersView = new GroupMembersView(); + this.settingsView = new GroupSettingsView(); }, addOne: function(repo, collection, options) { @@ -190,6 +193,21 @@ define([ this.$emptyTip.hide(); }, + showSettings: function() { + this.settingsView.show({ + 'group_id': this.group_id + }); + }, + + toggleSettingsPanel: function() { + var panel_id = this.settingsView.el.id; + if ($('#' + panel_id + ':visible').length) { // the panel is shown + this.settingsView.hide(); + } else { + this.showSettings(); + } + }, + showMembers: function() { this.membersView.show({'group_id': this.group_id}); }, diff --git a/static/scripts/common.js b/static/scripts/common.js index e8bc324224..6e3685e2d4 100644 --- a/static/scripts/common.js +++ b/static/scripts/common.js @@ -103,8 +103,10 @@ define([ case 'set_notice_seen_by_id': return siteRoot + 'ajax/set_notice_seen_by_id/'; case 'repo_set_password': return siteRoot + 'repo/set_password/'; case 'groups': return siteRoot + 'api/v2.1/groups/'; - case 'group_repos': return siteRoot + 'api2/groups/' + options.group_id + '/repos/'; + case 'group': return siteRoot + 'api/v2.1/groups/' + options.group_id + '/'; case 'group_members': return siteRoot + 'api/v2.1/groups/' + options.group_id + '/members/'; + case 'group_member': return siteRoot + 'api/v2.1/groups/' + options.group_id + '/members/' + options.email + '/'; + case 'group_repos': return siteRoot + 'api2/groups/' + options.group_id + '/repos/'; case 'group_basic_info': return siteRoot + 'ajax/group/' + options.group_id + '/basic-info/'; case 'toggle_group_modules': return siteRoot + 'ajax/group/' + options.group_id + '/toggle-modules/'; case 'toggle_personal_modules': return siteRoot + 'ajax/toggle-personal-modules/';