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/';