Merge lp:~stevenk/launchpad/inline-diff-comments-ui into lp:launchpad

Proposed by Steve Kowalik
Status: Merged
Approved by: Steve Kowalik
Approved revision: no longer in the source branch.
Merged at revision: 16871
Proposed branch: lp:~stevenk/launchpad/inline-diff-comments-ui
Merge into: lp:launchpad
Diff against target: 438 lines (+332/-7)
10 files modified
lib/canonical/launchpad/icing/style.css (+3/-0)
lib/lp/app/browser/stringformatter.py (+1/-1)
lib/lp/app/javascript/client.js (+2/-1)
lib/lp/app/javascript/inlineedit/editor.js (+6/-2)
lib/lp/code/browser/branchmergeproposal.py (+4/-2)
lib/lp/code/javascript/branchmergeproposal.inlinecomments.js (+132/-0)
lib/lp/code/javascript/tests/test_branchmergeproposal.inlinecomments.html (+84/-0)
lib/lp/code/javascript/tests/test_branchmergeproposal.inlinecomments.js (+91/-0)
lib/lp/code/templates/branchmergeproposal-index.pt (+3/-1)
lib/lp/services/features/flags.py (+6/-0)
To merge this branch: bzr merge lp:~stevenk/launchpad/inline-diff-comments-ui
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+184006@code.launchpad.net

Commit message

Add the fully JavaScript UI for inline comments on merge proposal diffs.

Description of the change

Add the fully JavaScript UI for inline comments for diffs. This is protected by a feature flag, so should be safe to land but will change a little, since the server side bits to populate existing comments, save draft comments, and publish them require DB changes and modeling.

I have more then enough LoC to eat the net-positive cost of this branch.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)
Revision history for this message
Samuel Mehrbrodt (sam92) wrote :

Hi,
are there any plans to enable this feature on launchpad.net?
Would be really helpful.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/icing/style.css'
2--- lib/canonical/launchpad/icing/style.css 2012-08-09 04:56:41 +0000
3+++ lib/canonical/launchpad/icing/style.css 2013-12-12 06:04:37 +0000
4@@ -628,6 +628,9 @@
5 margin: 0 0 0 0;
6 }
7
8+tr.ict-header {
9+ background-color: #EEEEEE;
10+}
11
12 /* === Bugs === */
13
14
15=== modified file 'lib/lp/app/browser/stringformatter.py'
16--- lib/lp/app/browser/stringformatter.py 2013-10-04 05:49:30 +0000
17+++ lib/lp/app/browser/stringformatter.py 2013-12-12 06:04:37 +0000
18@@ -882,7 +882,7 @@
19 max_format_lines = config.diff.max_format_lines
20 header_next = False
21 for row, line in enumerate(text.splitlines()[:max_format_lines]):
22- result.append('<tr>')
23+ result.append('<tr id="diff-line-%s">' % (row + 1))
24 result.append('<td class="line-no">%s</td>' % (row + 1))
25 if line.startswith('==='):
26 css_class = 'diff-file text'
27
28=== modified file 'lib/lp/app/javascript/client.js'
29--- lib/lp/app/javascript/client.js 2012-09-07 15:13:40 +0000
30+++ lib/lp/app/javascript/client.js 2013-12-12 06:04:37 +0000
31@@ -1179,7 +1179,8 @@
32 return result;
33 } else {
34 if (this.get('use_html')) {
35- return result.getHTML(attribute).get('innerHTML');
36+ return Y.Node.create(
37+ result.getHTML(attribute).get('innerHTML'));
38 } else {
39 return result.get(attribute);
40 }
41
42=== modified file 'lib/lp/app/javascript/inlineedit/editor.js'
43--- lib/lp/app/javascript/inlineedit/editor.js 2013-03-20 03:41:40 +0000
44+++ lib/lp/app/javascript/inlineedit/editor.js 2013-12-12 06:04:37 +0000
45@@ -1339,9 +1339,13 @@
46 var text = this.get(TEXT),
47 val = this.editor.get(VALUE);
48 text.setData(ORIGINAL_ELLIPSIS_TEXT, val);
49- text.set('innerHTML', '');
50+ text.set('text', '');
51 if (this.editor.get(MULTILINE)) {
52- text.set('innerHTML', val);
53+ if (Y.Lang.isString(val)) {
54+ text.set('text', val);
55+ } else {
56+ text.appendChild(val);
57+ }
58 } else {
59 text.appendChild(document.createTextNode(val));
60 }
61
62=== modified file 'lib/lp/code/browser/branchmergeproposal.py'
63--- lib/lp/code/browser/branchmergeproposal.py 2013-04-10 08:09:05 +0000
64+++ lib/lp/code/browser/branchmergeproposal.py 2013-12-12 06:04:37 +0000
65@@ -623,8 +623,10 @@
66 self.context.source_branch.unique_name),
67 })
68 if getFeatureFlag("longpoll.merge_proposals.enabled"):
69- cache.objects['merge_proposal_event_key'] = (
70- subscribe(self.context).event_key)
71+ cache.objects['merge_proposal_event_key'] = subscribe(
72+ self.context).event_key
73+ if getFeatureFlag("code.inline_diff_comments.enabled"):
74+ cache.objects['inline_diff_comments'] = True
75
76 @action('Claim', name='claim')
77 def claim_action(self, action, data):
78
79=== added file 'lib/lp/code/javascript/branchmergeproposal.inlinecomments.js'
80--- lib/lp/code/javascript/branchmergeproposal.inlinecomments.js 1970-01-01 00:00:00 +0000
81+++ lib/lp/code/javascript/branchmergeproposal.inlinecomments.js 2013-12-12 06:04:37 +0000
82@@ -0,0 +1,132 @@
83+/* Copyright 2013 Canonical Ltd. This software is licensed under the
84+ * GNU Affero General Public License version 3 (see the file LICENSE).
85+ *
86+ * Code for handling inline comments in diffs.
87+ *
88+ * @module lp.code.branchmergeproposal.inlinecomments
89+ * @requires node
90+ */
91+
92+YUI.add('lp.code.branchmergeproposal.inlinecomments', function(Y) {
93+
94+// Grab the namespace in order to be able to expose the connect methods.
95+var namespace = Y.namespace('lp.code.branchmergeproposal.inlinecomments');
96+
97+namespace.add_doubleclick_handler = function() {
98+ var inlinecomments = {};
99+ var rows = Y.one('.diff').all('tr');
100+ var handling_request = false;
101+ handler = function(e) {
102+ if (handling_request === true) {
103+ return;
104+ }
105+ handling_request = true;
106+ var linenumberdata = e.currentTarget.one('.line-no');
107+ var rownumber = linenumberdata.get('text');
108+ var rows = namespace.create_or_return_row(rownumber, null, null, null);
109+ var headerrow = rows[0];
110+ var newrow = rows[1];
111+ var widget = new Y.EditableText({
112+ contentBox: newrow.one('#inlinecomment-' + rownumber + '-draft'),
113+ initial_value_override: inlinecomments[rownumber],
114+ accept_empty: true,
115+ multiline: true,
116+ buttons: 'top'
117+ });
118+ widget.render();
119+ handle_widget_button = function(saved, comment) {
120+ if (saved === true) {
121+ inlinecomments[rownumber] = comment;
122+ }
123+ if (comment === '') {
124+ headerrow.remove(true);
125+ newrow.remove(true);
126+ }
127+ handling_request = false;
128+ };
129+ widget.editor.on('save', function() {
130+ handle_widget_button(true, this.get('value'));
131+ });
132+ widget.editor.on('cancel', function(e) {
133+ handle_widget_button(false, this.get('value'));
134+ });
135+ widget._triggerEdit(e);
136+ };
137+ rows.on('dblclick', handler);
138+};
139+
140+namespace.create_or_return_row = function(rownumber, person, comment, date) {
141+ var suffix = '-draft';
142+ var middle = rownumber + suffix;
143+ var draft = true;
144+ if (person !== null) {
145+ draft = false;
146+ }
147+ var headerrow = null;
148+ if (draft === true) {
149+ headerrow = Y.one('#ict-' + middle + '-header');
150+ if (headerrow !== null) {
151+ return [headerrow, headerrow.next()];
152+ }
153+ }
154+ headerrow = Y.Node.create(
155+ '<tr><td colspan="2"></td></tr>').addClass('ict-header');
156+ var headerspan;
157+ if (person !== null && date !== null) {
158+ headerspan = Y.Node.create(
159+ '<span>Comment by <a></a> on <span></span></span>');
160+ headerspan.one('a').set('href', person.web_link).set(
161+ 'text', person.display_name + ' (' + person.name + ')');
162+ headerspan.one('span').set('text', date);
163+ } else {
164+ headerspan = Y.Node.create('<span></span>').set(
165+ 'text', 'Draft comment.');
166+ }
167+ headerrow.one('td').appendChild(headerspan);
168+ if (draft === true) {
169+ headerrow.set('id', 'ict-' + middle + '-header');
170+ newrow = Y.Node.create('<tr><td></td><td><div>' +
171+ '<span class="yui3-editable_text-text"></span>' +
172+ '<div class="yui3-editable_text-trigger"></div>' +
173+ '</div></td></tr>').set('id', 'ict-' + middle);
174+ newrow.one('td>div').set('id', 'inlinecomment-' + middle);
175+ } else {
176+ newrow = Y.Node.create('<tr><td></td><td><span></span></td></tr>');
177+ newrow.one('span').set('text', comment);
178+ }
179+ // We want to have the comments in order after the line, so grab the
180+ // next row.
181+ var tr = Y.one('#diff-line-' + (parseInt(rownumber, 10) + 1));
182+ if (tr !== null) {
183+ tr.insert(headerrow, 'before');
184+ } else {
185+ // If this is the last row, grab the last child.
186+ tr = Y.one('.diff>tbody>tr:last-child');
187+ tr.insert(headerrow, 'after');
188+ }
189+ // The header row is the tricky one to place, the comment just goes
190+ // after it.
191+ headerrow.insert(newrow, 'after');
192+ return [headerrow, newrow];
193+};
194+
195+namespace.populate_existing_comments = function() {
196+ var i;
197+ for (i = 0; i < LP.cache.published_inline_comments.length; i++) {
198+ var row = LP.cache.published_inline_comments[i];
199+ namespace.create_or_return_row(
200+ row.line, row.person, row.comment, row.date);
201+ }
202+};
203+
204+namespace.setup_inline_comments = function() {
205+ if (LP.cache.inline_diff_comments === true) {
206+ // Add the double-click handler for each row in the diff. This needs
207+ // to be done first since populating existing published and draft
208+ // comments may add more rows.
209+ namespace.add_doubleclick_handler();
210+ namespace.populate_existing_comments();
211+ }
212+};
213+
214+ }, '0.1', {requires: ['event', 'io', 'node', 'widget', 'lp.ui.editor']});
215
216=== added file 'lib/lp/code/javascript/tests/test_branchmergeproposal.inlinecomments.html'
217--- lib/lp/code/javascript/tests/test_branchmergeproposal.inlinecomments.html 1970-01-01 00:00:00 +0000
218+++ lib/lp/code/javascript/tests/test_branchmergeproposal.inlinecomments.html 2013-12-12 06:04:37 +0000
219@@ -0,0 +1,84 @@
220+<!DOCTYPE html>
221+<!--
222+Copyright 2013 Canonical Ltd. This software is licensed under the
223+GNU Affero General Public License version 3 (see the file LICENSE).
224+-->
225+
226+<html>
227+ <head>
228+ <title>code.branchmergeproposal.inlinecomments Tests</title>
229+
230+ <!-- YUI and test setup -->
231+ <script type="text/javascript"
232+ src="../../../../../build/js/yui/yui/yui.js">
233+ </script>
234+ <link rel="stylesheet"
235+ href="../../../../../build/js/yui/console/assets/console-core.css" />
236+ <link rel="stylesheet"
237+ href="../../../../../build/js/yui/test-console/assets/skins/sam/test-console.css" />
238+ <link rel="stylesheet"
239+ href="../../../../../build/js/yui/test/assets/skins/sam/test.css" />
240+
241+ <script type="text/javascript"
242+ src="../../../../../build/js/lp/app/testing/testrunner.js"></script>
243+ <script type="text/javascript"
244+ src="../../../../../build/js/lp/app/testing/helpers.js"></script>
245+
246+ <link rel="stylesheet" href="../../../app/javascript/testing/test.css" />
247+
248+ <!-- Dependencies -->
249+ <script type="text/javascript"
250+ src="../../../../../build/js/lp/app/ui/ui.js"></script>
251+ <script type="text/javascript"
252+ src="../../../../../build/js/lp/app/extras/extras.js"></script>
253+ <script type="text/javascript"
254+ src="../../../../../build/js/lp/app/anim/anim.js"></script>
255+ <script type="text/javascript"
256+ src="../../../../../build/js/lp/app/errors.js"></script>
257+ <script type="text/javascript"
258+ src="../../../../../build/js/lp/app/overlay/overlay.js"></script>
259+ <script type="text/javascript"
260+ src="../../../../../build/js/lp/app/formoverlay/formoverlay.js"></script>
261+ <script type="text/javascript"
262+ src="../../../../../build/js/lp/app/formwidgets/resizing_textarea.js"></script>
263+ <script type="text/javascript"
264+ src="../../../../../build/js/lp/app/ellipsis.js"></script>
265+ <script type="text/javascript"
266+ src="../../../../../build/js/lp/app/inlineedit/editor.js"></script>
267+
268+ <!-- The module under test. -->
269+ <script type="text/javascript" src="../branchmergeproposal.inlinecomments.js"></script>
270+
271+ <!-- The test suite -->
272+ <script type="text/javascript" src="test_branchmergeproposal.inlinecomments.js"></script>
273+
274+ <!-- expected variable -->
275+ <script type="text/javascript">
276+ var LP = {
277+ cache: {}
278+ };
279+ </script>
280+
281+ </head>
282+ <body class="yui3-skin-sam">
283+ <ul id="suites">
284+ <li>lp.code.branchmergeproposal.inlinecomments.test</li>
285+ </ul>
286+ <table class="diff">
287+ <tbody>
288+ <tr id="diff-line-1">
289+ <td class="line-no">1</td>
290+ <td class="diff-header text">foo bar</td>
291+ </tr>
292+ <tr id="diff-line-2">
293+ <td class="line-no">2</td>
294+ <td class="text">baz</td>
295+ </tr>
296+ <tr id="diff-line-3">
297+ <td class="line-no">3</td>
298+ <td class="text">quux</td>
299+ </tr>
300+ </tbody>
301+ </table>
302+ </body>
303+</html>
304
305=== added file 'lib/lp/code/javascript/tests/test_branchmergeproposal.inlinecomments.js'
306--- lib/lp/code/javascript/tests/test_branchmergeproposal.inlinecomments.js 1970-01-01 00:00:00 +0000
307+++ lib/lp/code/javascript/tests/test_branchmergeproposal.inlinecomments.js 2013-12-12 06:04:37 +0000
308@@ -0,0 +1,91 @@
309+/* Copyright 2013 Canonical Ltd. This software is licensed under the
310+ * GNU Affero General Public License version 3 (see the file LICENSE). */
311+
312+YUI.add('lp.code.branchmergeproposal.inlinecomments.test', function (Y) {
313+
314+ var module = Y.lp.code.branchmergeproposal.inlinecomments;
315+ var tests = Y.namespace('lp.code.branchmergeproposal.inlinecomments.test');
316+ tests.suite = new Y.Test.Suite('branchmergeproposal.inlinecomments Tests');
317+
318+ tests.suite.add(new Y.Test.Case({
319+ name: 'code.branchmergeproposal.inlinecomments_tests',
320+
321+ setUp: function () {},
322+ tearDown: function () {},
323+
324+ test_library_exists: function () {
325+ Y.Assert.isObject(
326+ module, "Could not locate the " +
327+ "lp.code.branchmergeproposal.inlinecomments module");
328+ },
329+
330+ test_populatation: function () {
331+ var published_inline_comments = [
332+ {'line': 2, 'person': {'display_name': 'Sample Person',
333+ 'name': 'name.12', 'web_link': 'http://launchpad.dev/~name12'},
334+ 'comment': 'This is preloaded.', 'date': '2012-08-12 17:45'}];
335+ LP.cache.published_inline_comments = published_inline_comments;
336+ module.populate_existing_comments();
337+ var row = Y.one('#diff-line-2').next().next();
338+ Y.Assert.areEqual('This is preloaded.', row.get('text'));
339+ },
340+
341+ test_handler_normal: function() {
342+ module.add_doubleclick_handler();
343+ var line_2 = Y.one('#diff-line-2');
344+ line_2.simulate('dblclick');
345+ var comment = 'This is a comment.';
346+ Y.one('.yui3-ieditor-input>textarea').set('value', comment);
347+ Y.one('.lazr-pos').simulate('click');
348+ Y.Assert.areEqual(
349+ comment, Y.one('.yui3-editable_text-text').get('text'));
350+ },
351+
352+ test_handler_cancel: function() {
353+ var line_2 = Y.one('#diff-line-2');
354+ line_2.simulate('dblclick');
355+ var comment = 'Cancelling test.';
356+ Y.one('.yui3-ieditor-input>textarea').set('value', comment);
357+ Y.one('.lazr-pos').simulate('click');
358+ line_2.simulate('dblclick');
359+ Y.one('.yui3-ieditor-input>textarea').set('value', 'Foobar!');
360+ Y.one('.lazr-neg').simulate('click');
361+ Y.Assert.areEqual(
362+ comment, Y.one('.yui3-editable_text-text').get('text'));
363+ },
364+
365+ test_handler_cancel_immediately: function() {
366+ var line_1 = Y.one('#diff-line-1');
367+ line_1.simulate('dblclick');
368+ Y.one('.lazr-neg').simulate('click');
369+ Y.Assert.isNull(Y.one('#ict-1-draft-header'));
370+ },
371+
372+ test_feature_flag_off: function() {
373+ var called = false;
374+ add_doubleclick_handler = function() {
375+ called = true;
376+ };
377+ module.add_doubleclick_handler = add_doubleclick_handler;
378+ module.setup_inline_comments();
379+ Y.Assert.isFalse(called);
380+ },
381+
382+ test_feature_flag: function() {
383+ LP.cache.published_inline_comments = [];
384+ var called = false;
385+ add_doubleclick_handler = function() {
386+ called = true;
387+ };
388+ module.add_doubleclick_handler = add_doubleclick_handler;
389+ window.LP.cache.inline_diff_comments = true;
390+ module.setup_inline_comments();
391+ Y.Assert.isTrue(called);
392+ }
393+
394+ }));
395+
396+}, '0.1', {
397+ requires: ['node-event-simulate', 'test', 'lp.testing.helpers', 'console',
398+ 'lp.code.branchmergeproposal.inlinecomments']
399+});
400
401=== modified file 'lib/lp/code/templates/branchmergeproposal-index.pt'
402--- lib/lp/code/templates/branchmergeproposal-index.pt 2012-06-28 14:19:28 +0000
403+++ lib/lp/code/templates/branchmergeproposal-index.pt 2013-12-12 06:04:37 +0000
404@@ -197,7 +197,8 @@
405 LPJS.use('io-base', 'lp.code.branchmergeproposal.reviewcomment',
406 'lp.code.branchmergeproposal.status', 'lp.app.comment',
407 'lp.code.branchmergeproposal.updater', 'lp.app.widgets.expander',
408- 'lp.code.branch.revisionexpander', function(Y) {
409+ 'lp.code.branch.revisionexpander',
410+ 'lp.code.branchmergeproposal.inlinecomments', function(Y) {
411
412 Y.on('load', function() {
413 var logged_in = LP.links['me'] !== undefined;
414@@ -237,6 +238,7 @@
415 '.expander-content',
416 false,
417 Y.lp.code.branch.revisionexpander.bmp_diff_loader);
418+ Y.lp.code.branchmergeproposal.inlinecomments.setup_inline_comments();
419 });
420 });
421 -->
422
423=== modified file 'lib/lp/services/features/flags.py'
424--- lib/lp/services/features/flags.py 2013-08-08 08:49:04 +0000
425+++ lib/lp/services/features/flags.py 2013-12-12 06:04:37 +0000
426@@ -221,6 +221,12 @@
427 'disabled',
428 '',
429 ''),
430+ ('code.inline_diff_comments.enabled',
431+ 'space delimited',
432+ 'Names of projects have inline diff comments enabled.',
433+ '',
434+ '',
435+ ''),
436 ])
437
438 # The set of all flag names that are documented.