diff --git a/media/css/seahub.css b/media/css/seahub.css index 9f11f62311..d3795c1be9 100644 --- a/media/css/seahub.css +++ b/media/css/seahub.css @@ -1087,6 +1087,7 @@ textarea:-moz-placeholder {/* for FF */ .details-panel { width:320px; } +.file-comment-panel-item-name, .details-panel-item-name { display:inline-block; max-width:215px; diff --git a/seahub/templates/js/templates.html b/seahub/templates/js/templates.html index ebc711fa3e..1842374323 100644 --- a/seahub/templates/js/templates.html +++ b/seahub/templates/js/templates.html @@ -616,6 +616,7 @@ @@ -736,6 +738,7 @@ @@ -813,12 +817,15 @@ @@ -2420,3 +2427,46 @@ + +{# file comment #} + + diff --git a/seahub/templates/libraries.html b/seahub/templates/libraries.html index b9557fad40..d5820b51a7 100644 --- a/seahub/templates/libraries.html +++ b/seahub/templates/libraries.html @@ -264,7 +264,7 @@ app["pageOptions"] = { {% endfor %} return mods_enabled; })(), - username: "{{request.user.username}}", + username: "{{request.user.username|escapejs}}", name: "{{request.user.username|email2nickname|escapejs}}", contact_email: "{{ request.user.username|email2contact_email|escapejs }}", events_enabled: {% if events_enabled %} true {% else %} false {% endif %}, diff --git a/static/scripts/app/collections/file-comments.js b/static/scripts/app/collections/file-comments.js new file mode 100644 index 0000000000..6d09bb1ade --- /dev/null +++ b/static/scripts/app/collections/file-comments.js @@ -0,0 +1,24 @@ +define([ + 'underscore', + 'backbone', + 'common' +], function(_, Backbone, Common) { + 'use strict'; + + var collection = Backbone.Collection.extend({ + + url: function() { + return Common.getUrl({name: 'file-comments', repo_id: this.repo_id}); + }, + + parse: function(data) { + return data.comments; // return the array + }, + + setData: function(repo_id) { + this.repo_id = repo_id; + } + }); + + return collection; +}); diff --git a/static/scripts/app/views/dir.js b/static/scripts/app/views/dir.js index 02fe0af31c..7b160397a4 100644 --- a/static/scripts/app/views/dir.js +++ b/static/scripts/app/views/dir.js @@ -14,10 +14,11 @@ define([ 'app/views/dirent-details', 'app/views/fileupload', 'app/views/share', + 'app/views/file-comments', 'app/views/widgets/dropdown' ], function($, progressbar, magnificPopup, simplemodal, _, Backbone, Common, FileTree, Cookies, DirentCollection, DirentView, DirentGridView, - DirentDetailsView, FileUploadView, ShareView, DropdownView) { + DirentDetailsView, FileUploadView, ShareView, FileCommentsView, DropdownView) { 'use strict'; var DirView = Backbone.View.extend({ @@ -74,6 +75,7 @@ define([ this.fileUploadView = new FileUploadView({dirView: this}); this.direntDetailsView = new DirentDetailsView(); + this.fileCommentsView = new FileCommentsView(); this.render(); @@ -159,6 +161,7 @@ define([ this.attached = false; this.direntDetailsView.hide(); + this.fileCommentsView.hide(); }, /***** private functions *****/ @@ -174,6 +177,9 @@ define([ }, reset: function() { + this.direntDetailsView.hide(); + this.fileCommentsView.hide(); + this.renderPath(); this.renderDirOpBar(); @@ -673,8 +679,8 @@ define([ var dirent_name = $.trim($input.val()); if (!dirent_name) { - Common.showFormError(form_id, gettext("It is required.")); - return false; + Common.showFormError(form_id, gettext("It is required.")); + return false; }; if (dirent_name.indexOf('/') != -1) { diff --git a/static/scripts/app/views/dirent-grid.js b/static/scripts/app/views/dirent-grid.js index 5e77dc069a..7673e85875 100644 --- a/static/scripts/app/views/dirent-grid.js +++ b/static/scripts/app/views/dirent-grid.js @@ -139,6 +139,7 @@ define([ this.$('.lock-file').on('click', _.bind(this.lockFile, this)); this.$('.unlock-file').on('click', _.bind(this.unlockFile, this)); this.$('.view-details').on('click', _.bind(this.viewDetails, this)); + this.$('.file-comment').on('click', _.bind(this.viewFileComments, this)); this.$('.set-folder-permission').on('click', _.bind(this.setFolderPerm, this)); return false; @@ -386,6 +387,20 @@ define([ return false; }, + viewFileComments: function() { + var file_icon_size = Common.isHiDPI() ? 48 : 24; + this.dirView.fileCommentsView.show({ + 'is_repo_owner': this.dir.is_repo_owner, + 'repo_id': this.dir.repo_id, + 'path': Common.pathJoin([this.dir.path, this.model.get('obj_name')]), + 'icon_url': this.model.getIconUrl(file_icon_size), + 'file_name': this.model.get('obj_name') + }); + + this.closeMenu(); + return false; + }, + open_via_client: function() { this.closeMenu(); return true; diff --git a/static/scripts/app/views/dirent.js b/static/scripts/app/views/dirent.js index cca9eb6284..3d631b7637 100644 --- a/static/scripts/app/views/dirent.js +++ b/static/scripts/app/views/dirent.js @@ -104,6 +104,7 @@ define([ 'click .lock-file': 'lockFile', 'click .unlock-file': 'unlockFile', 'click .view-details': 'viewDetails', + 'click .file-comment': 'viewFileComments', 'click .open-via-client': 'open_via_client' }, @@ -113,9 +114,13 @@ define([ clickItem: function(e) { var target = e.target || event.srcElement; - if (this.$('td').is(target) && - $('#dirent-details').css('right') == '0px') { // after `#dirent-details` is shown - this.viewDetails(); + if (this.$('td').is(target)) { + if ($('#dirent-details').css('right') == '0px') { // after `#dirent-details` is shown + this.viewDetails(); + } + if ($('#file-comments').css('right') == '0px') { + this.viewFileComments(); + } } }, @@ -668,6 +673,20 @@ define([ return false; }, + viewFileComments: function() { + var file_icon_size = Common.isHiDPI() ? 48 : 24; + this.dirView.fileCommentsView.show({ + 'is_repo_owner': this.dir.is_repo_owner, + 'repo_id': this.dir.repo_id, + 'path': Common.pathJoin([this.dir.path, this.model.get('obj_name')]), + 'icon_url': this.model.getIconUrl(file_icon_size), + 'file_name': this.model.get('obj_name') + }); + + this._hideMenu(); + return false; + }, + open_via_client: function() { this._hideMenu(); return true; diff --git a/static/scripts/app/views/file-comment.js b/static/scripts/app/views/file-comment.js new file mode 100644 index 0000000000..1ca7ded286 --- /dev/null +++ b/static/scripts/app/views/file-comment.js @@ -0,0 +1,99 @@ +define([ + 'jquery', + 'underscore', + 'backbone', + 'common', + 'marked', + 'moment' +], function($, _, Backbone, Common, Marked, Moment) { + 'use strict'; + + var View = Backbone.View.extend({ + + tagName: 'li', + className: 'msg ovhd', + + template: _.template($('#file-comment-tmpl').html()), + + initialize: function(options) { + this.listenTo(this.model, 'destroy', this.remove); + + this.is_repo_owner = options.is_repo_owner; + this.parentView = options.parentView; + }, + + events: { + 'mouseenter': 'highlight', + 'mouseleave': 'rmHighlight', + 'click .js-del-msg': 'delMsg', + 'click .js-reply-msg': 'reply' + }, + + highlight: function() { + this.$el.addClass('hl'); + this.$('.msg-ops').removeClass('vh'); + }, + + rmHighlight: function() { + this.$el.removeClass('hl'); + this.$('.msg-ops').addClass('vh'); + }, + + delMsg: function() { + this.model.destroy({ + wait: true, + success: function() { + }, + error: function(model, response) { + var err_msg; + if (response.responseText) { + err_msg = $.parseJSON(response.responseText).error_msg; + } else { + err_msg = gettext("Failed. Please check the network."); + } + Common.feedback(err_msg, 'error'); + } + }); + return false; + }, + + reply: function() { + this.parentView.replyTo(this.model.get('user_name')); + return false; + }, + + render: function() { + var user_email = this.model.get('user_email'); + + var can_delete_msg = false; + if (this.is_repo_owner || + user_email == app.pageOptions.username) { + can_delete_msg = true; + } + + var user_profile_url = Common.getUrl({ + 'name': 'user_profile', + 'username': encodeURIComponent(user_email) + }); + + var obj = this.model.attributes; + var m = Moment(obj.created_at); + var data = $.extend({}, obj, { + 'content_marked': Marked(obj.comment, { + breaks: true, + sanitize: true + }), + 'time': m.format('LLLL'), + 'time_from_now': Common.getRelativeTimeStr(m), + 'can_delete_msg': can_delete_msg, + 'user_profile_url': user_profile_url + }); + + this.$el.html(this.template(data)); + return this; + } + + }); + + return View; +}); diff --git a/static/scripts/app/views/file-comments.js b/static/scripts/app/views/file-comments.js new file mode 100644 index 0000000000..fde2eda6ee --- /dev/null +++ b/static/scripts/app/views/file-comments.js @@ -0,0 +1,205 @@ +define([ + 'jquery', + 'underscore', + 'backbone', + 'common', + 'app/collections/file-comments', + 'app/views/file-comment' +], function($, _, Backbone, Common, Collection, ItemView) { + 'use strict'; + + var View = Backbone.View.extend({ + + id: 'file-comments', + className: 'right-side-panel', + + template: _.template($('#file-comment-panel-tmpl').html()), + + initialize: function() { + $("#main").append(this.$el); + + this.collection = new Collection(); + this.listenTo(this.collection, 'add', this.addOne); + this.listenTo(this.collection, 'reset', this.reset); + + var _this = this; + $(document).keydown(function(e) { + // ESCAPE key pressed + if (e.which == 27) { + _this.hide(); + } + }); + $(window).resize(function() { + _this.setConHeight(); + }); + }, + + events: { + 'click .js-close': 'close', + 'submit .msg-form': 'formSubmit' + }, + + close: function() { + this.hide(); + return false; + }, + + render: function(data) { + this.$el.html(this.template(data)); + + this.$listContainer = $('.file-discussion-list', this.$el); + this.$emptyTip = $('.no-discussion-tip', this.$el); + this.$loadingTip = $('.loading-tip', this.$el); + this.$conError = $('.file-discussions-con .error', this.$el); + this.$msgInput = $('[name="message"]', this.$el); + }, + + show: function(options) { + this.is_repo_owner = options.is_repo_owner; + this.repo_id = options.repo_id; + this.path = options.path; + + this.collection.setData(this.repo_id); + + this.render({ + 'icon_url': options.icon_url, + 'file_name': options.file_name + }); + this.$el.css({'right': 0}); + this.setConHeight(); + this.getContent(); + }, + + hide: function() { + this.$el.css({'right': '-400px'}); + this.$el.empty(); + }, + + reset: function() { + this.$conError.hide(); + this.$loadingTip.hide(); + this.$listContainer.empty(); + + if (this.collection.length) { + this.$emptyTip.hide(); + this.collection.each(this.addOne, this); + this.$listContainer.show(); + this.scrollConToBottom(); + } else { + this.$emptyTip.show(); + this.$listContainer.hide(); + } + }, + + getContent: function() { + var _this = this; + + this.collection.fetch({ + cache: false, + data: { + p: this.path, + avatar_size: 64 + }, + reset: true, + success: function() { + }, + 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 = $.parseJSON(response.responseText).error_msg; + } + } else { + err_msg = gettext('Please check the network.'); + } + _this.$conError.html(err_msg).show(); + } + }); + }, + + formSubmit: function() { + var _this = this; + var $formError = $('.msg-form .error', this.$el); + var $submitBtn = $('[type="submit"]', this.$el) + var msg = $.trim(this.$msgInput.val()); + if (!msg) { + return false; + } + + $formError.hide(); + Common.disableButton($submitBtn); + + $.ajax({ + url: Common.getUrl({name: 'file-comments', repo_id: this.repo_id}) + + '?p=' + encodeURIComponent(this.path) + '&avatar_size=64', + type: 'POST', + cache: false, + dataType: 'json', + beforeSend: Common.prepareCSRFToken, + data: { + 'comment': msg + }, + success: function(data) { + _this.$msgInput.val(''); + _this.collection.add(data); + if (_this.$emptyTip.is(':visible')) { + _this.$emptyTip.hide(); + _this.$listContainer.show(); + } + _this.scrollConToBottom(); + }, + 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."); + } + $formError.html(err_msg).show(); + }, + complete: function() { + Common.enableButton($submitBtn); + } + }); + + return false; + }, + + addOne: function(item) { + var view = new ItemView({ + model: item, + is_repo_owner: this.is_repo_owner, + parentView: this + }); + + this.$listContainer.append(view.render().el); + }, + + setConHeight: function() { + $('.file-discussions-con', this.$el).css({ + 'max-height': $(window).height() + - this.$el.offset().top + - $('.file-discussions-hd', this.$el).outerHeight(true) + - $('.file-discussions-footer', this.$el).outerHeight(true) + }); + }, + + scrollConToBottom: function() { + var $el = this.$('.file-discussions-con'); + $el.scrollTop($el[0].scrollHeight - $el[0].clientHeight); + }, + + replyTo: function(to_user) { + var str = "@" + to_user + " "; + var $input = this.$msgInput.val(str); + Common.setCaretPos($input[0], str.length); + $input.focus(); + } + + }); + + return View; +}); diff --git a/static/scripts/common.js b/static/scripts/common.js index af435c3a75..6a6cb31384 100644 --- a/static/scripts/common.js +++ b/static/scripts/common.js @@ -98,6 +98,7 @@ define([ case 'dir-details': return siteRoot + 'api/v2.1/repos/' + options.repo_id + '/dir/detail/'; case 'tags': return siteRoot + 'api/v2.1/repos/' + options.repo_id + '/tags/'; + case 'file-comments': return siteRoot + 'api2/repos/' + options.repo_id + '/file/comments/'; // Repos case 'repos': return siteRoot + 'api2/repos/';