Merge ~pappacena/launchpad:comment-editing-ui into launchpad:master
- Git
- lp:~pappacena/launchpad
- comment-editing-ui
- Merge into master
Proposed by
Thiago F. Pappacena
Status: | Merged |
---|---|
Approved by: | Thiago F. Pappacena |
Approved revision: | d0b05df06e00a068657554df752ea1623be6ffc4 |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~pappacena/launchpad:comment-editing-ui |
Merge into: | launchpad:master |
Prerequisite: | ~pappacena/launchpad:comment-editing-revisions-api |
Diff against target: |
754 lines (+593/-19) 10 files modified
lib/canonical/launchpad/icing/css/base.scss (+40/-2) lib/lp/answers/browser/question.py (+6/-0) lib/lp/answers/stories/question-workflow.txt (+7/-6) lib/lp/answers/templates/question-index.pt (+3/-1) lib/lp/answers/templates/questionmessage-display.pt (+29/-9) lib/lp/services/messages/interfaces/message.py (+1/-1) lib/lp/services/messages/javascript/messages.edit.js (+211/-0) lib/lp/services/messages/javascript/tests/test_messages.edit.html (+95/-0) lib/lp/services/messages/javascript/tests/test_messages.edit.js (+175/-0) lib/lp/services/messages/tests/test_yuitests.py (+26/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Review via email: mp+402522@code.launchpad.net |
Commit message
Javascript component to edit messages, and its first usage in QuestionMessage view
Description of the change
To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) : | # |
review:
Approve
Revision history for this message
Thiago F. Pappacena (pappacena) wrote : | # |
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/lib/canonical/launchpad/icing/css/base.scss b/lib/canonical/launchpad/icing/css/base.scss |
2 | index 68bcce1..6ee2779 100644 |
3 | --- a/lib/canonical/launchpad/icing/css/base.scss |
4 | +++ b/lib/canonical/launchpad/icing/css/base.scss |
5 | @@ -2,7 +2,8 @@ |
6 | |
7 | body { |
8 | /* line-height is the same as the sprite height. */ |
9 | - font-family: Ubuntu, 'Bitstream Vera Sans', 'DejaVu Sans', Tahoma, sans-serif; |
10 | + font-family: Ubuntu, 'Bitstream Vera Sans', 'DejaVu Sans', Tahoma, |
11 | + sans-serif; |
12 | font-size: 12px; |
13 | line-height: 18px; |
14 | color: #333; |
15 | @@ -444,7 +445,8 @@ body { |
16 | |
17 | table { |
18 | th, td { |
19 | - /* We don't want extra padding on nested tables, like batch navigation. */ |
20 | + /* We don't want extra padding on nested tables, |
21 | + like batch navigation. */ |
22 | padding: 0; |
23 | } |
24 | } |
25 | @@ -543,6 +545,42 @@ body { |
26 | border-bottom-left-radius: 5px; |
27 | } |
28 | |
29 | + .editable-message { |
30 | + .editable-message-notification { |
31 | + position: absolute; |
32 | + width: 100%; |
33 | + height: 100%; |
34 | + top: 0; |
35 | + left: 0; |
36 | + background-color: white; |
37 | + opacity: 0.9; |
38 | + display: flex; |
39 | + flex-wrap: wrap; |
40 | + justify-content: center; |
41 | + align-items: center; |
42 | + |
43 | + p { |
44 | + display: block; |
45 | + flex-basis: 100%; |
46 | + margin-top: 10px; |
47 | + } |
48 | + .editable-message-notification-dismiss { |
49 | + flex-basis: 100%; |
50 | + text-align: center; |
51 | + padding: 1px; |
52 | + margin-top: -10px; |
53 | + } |
54 | + } |
55 | + |
56 | + .editable-message-form { |
57 | + padding: 0.5em 12px 0; |
58 | + input[type="button"] { |
59 | + padding: 1px; |
60 | + margin: 5px; |
61 | + } |
62 | + } |
63 | + } |
64 | + |
65 | @import 'typography', |
66 | 'colours', |
67 | 'forms', |
68 | diff --git a/lib/lp/answers/browser/question.py b/lib/lp/answers/browser/question.py |
69 | index 68c4b59..9d4986d 100644 |
70 | --- a/lib/lp/answers/browser/question.py |
71 | +++ b/lib/lp/answers/browser/question.py |
72 | @@ -1200,8 +1200,14 @@ class QuestionMessageDisplayView(LaunchpadView): |
73 | # If a comment that isn't visible is being rendered, it's being |
74 | # rendered for an admin or registry_expert. |
75 | css_classes.append("adminHiddenComment") |
76 | + if self.can_edit: |
77 | + css_classes.append("editable-message") |
78 | return " ".join(css_classes) |
79 | |
80 | + @property |
81 | + def can_edit(self): |
82 | + return check_permission('launchpad.Edit', self.context) |
83 | + |
84 | def canConfirmAnswer(self): |
85 | """Return True if the user can confirm this answer.""" |
86 | return (self.display_confirm_button and |
87 | diff --git a/lib/lp/answers/stories/question-workflow.txt b/lib/lp/answers/stories/question-workflow.txt |
88 | index 984aaf4..5e39acd 100755 |
89 | --- a/lib/lp/answers/stories/question-workflow.txt |
90 | +++ b/lib/lp/answers/stories/question-workflow.txt |
91 | @@ -219,7 +219,8 @@ The confirmed answer is also highlighted. |
92 | <img ... src="/@@/favourite-yes" ... title="Marked as best answer"/> |
93 | |
94 | >>> print(soup.find( |
95 | - ... 'div', 'boardCommentBody highlighted').decode_contents()) |
96 | + ... 'div', 'boardCommentBody highlighted editable-message-text' |
97 | + ... ).decode_contents()) |
98 | <p>New version of the firefox package are available with SVG support |
99 | enabled. You can use apt-get or adept to upgrade.</p> |
100 | |
101 | @@ -289,9 +290,9 @@ answerer back to None. |
102 | >>> bestAnswer.find('strong') is None |
103 | True |
104 | |
105 | - >>> bestAnswer.find('div', 'boardCommentBody') |
106 | - <div class="boardCommentBody" itemprop="commentText"><p>New version |
107 | - of the firefox package |
108 | + >>> bestAnswer.find('div', 'boardCommentBody editable-message-text') |
109 | + <div class="boardCommentBody editable-message-text" |
110 | + itemprop="commentText"><p>New version of the firefox package |
111 | are available with SVG support enabled. You can use apt-get or adept to |
112 | upgrade.</p></div> |
113 | |
114 | @@ -356,9 +357,9 @@ The answer's message is also highlighted as the best answer. |
115 | No Privileges Person (no-priv) |
116 | |
117 | >>> message = soup.find( |
118 | - ... 'div', 'boardCommentBody highlighted') |
119 | + ... 'div', 'boardCommentBody highlighted editable-message-text') |
120 | >>> print(message) |
121 | - <div class="boardCommentBody highlighted" |
122 | + <div class="boardCommentBody highlighted editable-message-text" |
123 | itemprop="commentText"><p>New version of the firefox package are |
124 | available with SVG support enabled. You can use apt-get or adept to |
125 | upgrade.</p></div> |
126 | diff --git a/lib/lp/answers/templates/question-index.pt b/lib/lp/answers/templates/question-index.pt |
127 | index e7e233d..13f6270 100644 |
128 | --- a/lib/lp/answers/templates/question-index.pt |
129 | +++ b/lib/lp/answers/templates/question-index.pt |
130 | @@ -17,7 +17,8 @@ |
131 | </style> |
132 | <script type="text/javascript"> |
133 | LPJS.use('base', 'node', 'event', |
134 | - 'lp.app.comment', 'lp.answers.subscribers', |
135 | + 'lp.app.comment', 'lp.answers.subscribers', |
136 | + 'lp.services.messages.edit', |
137 | function(Y) { |
138 | Y.on('domready', function() { |
139 | LP.cache.comment_context = LP.cache.context; |
140 | @@ -29,6 +30,7 @@ |
141 | cl.render(); |
142 | } |
143 | new Y.lp.answers.subscribers.createQuestionSubscribersLoader(); |
144 | + Y.lp.services.messages.edit.setup(); |
145 | }); |
146 | }); |
147 | </script> |
148 | diff --git a/lib/lp/answers/templates/questionmessage-display.pt b/lib/lp/answers/templates/questionmessage-display.pt |
149 | index b69b4b6..fc3651c 100644 |
150 | --- a/lib/lp/answers/templates/questionmessage-display.pt |
151 | +++ b/lib/lp/answers/templates/questionmessage-display.pt |
152 | @@ -7,7 +7,8 @@ |
153 | itemtype="http://schema.org/UserComments" |
154 | tal:define="css_classes view/getBoardCommentCSSClass" |
155 | tal:attributes="class string:${css_classes}; |
156 | - id string:comment-${context/index}"> |
157 | + id string:comment-${context/index}; |
158 | + data-baseurl context/fmt:url"> |
159 | <div class="boardCommentDetails"> |
160 | <table> |
161 | <tbody> |
162 | @@ -25,8 +26,18 @@ |
163 | itemprop="commentTime" |
164 | tal:attributes="title context/datecreated/fmt:datetime; |
165 | datetime context/datecreated/fmt:isodate" |
166 | - tal:content="context/datecreated/fmt:displaydate">Thursday |
167 | - 13:21</time>: |
168 | + tal:content="context/datecreated/fmt:displaydate">Thursday 13:21 |
169 | + </time><span class="editable-message-last-edit-date"><tal:last-edit condition="context/date_last_edited"> |
170 | + (last edit <time |
171 | + itemprop="editTime" |
172 | + tal:attributes="title context/date_last_edited/fmt:datetime; |
173 | + datetime context/date_last_edited/fmt:isodate" |
174 | + tal:content="context/date_last_edited/fmt:displaydate" />)</tal:last-edit>: |
175 | + </span> |
176 | + </td> |
177 | + <td> |
178 | + <img class="sprite edit action-icon editable-message-edit-btn" |
179 | + tal:condition="view/can_edit"/> |
180 | </td> |
181 | <td class="bug-comment-index"> |
182 | <a |
183 | @@ -35,12 +46,21 @@ |
184 | </tr></tbody></table> |
185 | </div> |
186 | |
187 | - <div class="boardCommentBody" |
188 | - tal:attributes="class view/getBodyCSSClass" |
189 | - itemprop="commentText" |
190 | - tal:content="structure |
191 | - context/text_contents/fmt:obfuscate-email/fmt:email-to-html"> |
192 | - Message text. |
193 | + <div class="editable-message-body"> |
194 | + <div class="boardCommentBody" |
195 | + tal:attributes="class python: view.getBodyCSSClass() + ' editable-message-text'" |
196 | + itemprop="commentText" |
197 | + tal:content="structure |
198 | + context/text_contents/fmt:obfuscate-email/fmt:email-to-html"> |
199 | + Message text. |
200 | + </div> |
201 | + </div> |
202 | + |
203 | + <div class="editable-message-form" style="display: none"> |
204 | + <textarea style="width: 100%" rows="10" |
205 | + tal:content="context/text_contents" /> |
206 | + <input type="button" value="Update" class="editable-message-update-btn" /> |
207 | + <input type="button" value="Cancel" class="editable-message-cancel-btn" /> |
208 | </div> |
209 | |
210 | <div class="confirmBox" |
211 | diff --git a/lib/lp/services/messages/interfaces/message.py b/lib/lp/services/messages/interfaces/message.py |
212 | index 598b665..a719d66 100644 |
213 | --- a/lib/lp/services/messages/interfaces/message.py |
214 | +++ b/lib/lp/services/messages/interfaces/message.py |
215 | @@ -58,7 +58,7 @@ class IMessageEdit(Interface): |
216 | |
217 | @export_write_operation() |
218 | @operation_parameters( |
219 | - new_content=TextLine( |
220 | + new_content=Text( |
221 | title=_("Message content"), |
222 | description=_("The new message content string"), |
223 | required=True)) |
224 | diff --git a/lib/lp/services/messages/javascript/messages.edit.js b/lib/lp/services/messages/javascript/messages.edit.js |
225 | new file mode 100644 |
226 | index 0000000..3c4c0e5 |
227 | --- /dev/null |
228 | +++ b/lib/lp/services/messages/javascript/messages.edit.js |
229 | @@ -0,0 +1,211 @@ |
230 | +/* Copyright 2015-2021 Canonical Ltd. This software is licensed under the |
231 | + * GNU Affero General Public License version 3 (see the file LICENSE). |
232 | + * |
233 | + * This modules controls HTML comments in order to make them editable. To do |
234 | + * so, it requires: |
235 | + * - A div container with the class .editable-message containing everything |
236 | + * else related to the message |
237 | + * - A data-baseurl="/path/to/msg" on the .editable-message container |
238 | + * - A .editable-message-body container with the original msg content |
239 | + * - A .editable-message-edit-btn element inside the main container, that will |
240 | + * switch the view to edit form when clicked. |
241 | + * - A .editable-message-form, with a textarea and 2 buttons: |
242 | + * .editable-message-update-btn and .editable-message-cancel-btn. |
243 | + * - A .editable-message-last-edit-date span, where we update the date of the |
244 | + * last message editing. |
245 | + * |
246 | + * Once those HTML elements are available in the page, this module should be |
247 | + * initialized with `lp.services.messages.edit.setup()`. |
248 | + * |
249 | + * @module Y.lp.services.messages.edit |
250 | + * @requires node, DOM, lp.client |
251 | + */ |
252 | +YUI.add('lp.services.messages.edit', function(Y) { |
253 | + var module = Y.namespace('lp.services.messages.edit'); |
254 | + |
255 | + // XXX pappacena 2021-05-21: We should drop this message once we have a |
256 | + // way to list the old message revisions in the web UI. |
257 | + module.msg_edit_success_notification = ( |
258 | + "Message edited, but the original content may still be publicly " + |
259 | + "visible using the API.<br />Please " + |
260 | + "<a href='https://launchpad.net/+apidoc/devel.html#message'>" + |
261 | + "check the API documentation</a> in case you " + |
262 | + "need to remove old message revisions." |
263 | + ); |
264 | + module.msg_edit_error_notification = ( |
265 | + "There was an error updating the comment. " + |
266 | + "Please try again in a few minutes." |
267 | + ); |
268 | + |
269 | + module.htmlify_msg = function(text) { |
270 | + text = text.replace(/&/g, "&"); |
271 | + text = text.replace(/</g, "<"); |
272 | + text = text.replace(/>/g, ">"); |
273 | + text = text.replace(/\n/g, "<br/>"); |
274 | + return "<p>" + text + "</p>"; |
275 | + }; |
276 | + |
277 | + module.showEditMessageField = function(msg_body, msg_form) { |
278 | + msg_body.setStyle('display', 'none'); |
279 | + msg_form.setStyle('display', 'block'); |
280 | + }; |
281 | + |
282 | + module.hideEditMessageField = function(msg_body, msg_form) { |
283 | + msg_body.setStyle('display', 'block'); |
284 | + msg_form.setStyle('display', 'none'); |
285 | + }; |
286 | + |
287 | + module.saveMessageContent = function( |
288 | + msg_path, new_content, on_success, on_failure) { |
289 | + var msg_url = "/api/devel" + msg_path; |
290 | + var config = { |
291 | + on: { |
292 | + success: on_success, |
293 | + failure: on_failure |
294 | + }, |
295 | + parameters: {"new_content": new_content} |
296 | + }; |
297 | + this.lp_client.named_post(msg_url, 'editContent', config); |
298 | + }; |
299 | + |
300 | + module.showNotification = function(container, msg, can_dismiss) { |
301 | + can_dismiss = can_dismiss || false; |
302 | + // Clean up previous notification. |
303 | + module.hideNotification(container); |
304 | + container.setStyle('position', 'relative'); |
305 | + var node = Y.Node.create( |
306 | + "<div class='editable-message-notification'>" + |
307 | + " <p class='block-sprite large-warning'>" + |
308 | + msg + |
309 | + " </p>" + |
310 | + "</div>"); |
311 | + container.append(node); |
312 | + if (can_dismiss) { |
313 | + var dismiss = Y.Node.create( |
314 | + "<div class='editable-message-notification-dismiss'>" + |
315 | + " <input type='button' value='Ok' />" + |
316 | + "</div>"); |
317 | + dismiss.on('click', function() { |
318 | + module.hideNotification(container); |
319 | + }); |
320 | + node.append(dismiss); |
321 | + } |
322 | + }; |
323 | + |
324 | + module.hideNotification = function(container) { |
325 | + var notification = container.one(".editable-message-notification"); |
326 | + if(notification) { |
327 | + notification.remove(); |
328 | + } |
329 | + }; |
330 | + |
331 | + module.showLoading = function(container) { |
332 | + module.showNotification( |
333 | + container, |
334 | + '<img class="spinner" src="/@@/spinner" alt="Loading..." />'); |
335 | + }; |
336 | + |
337 | + module.hideLoading = function(container) { |
338 | + module.hideNotification(container); |
339 | + }; |
340 | + |
341 | + // What to do when a user clicks a message's "edit" button. |
342 | + module.onEditClick = function(elements) { |
343 | + // When clicking edit icon, show the edit form and focus on the |
344 | + // text area. |
345 | + module.showEditMessageField(elements.msg_body, elements.msg_form); |
346 | + elements.msg_form.one('textarea').getDOMNode().focus(); |
347 | + } |
348 | + |
349 | + // What to do when a user clicks "cancel edit" button. |
350 | + module.onEditCancelClick = function(elements) { |
351 | + module.hideEditMessageField(elements.msg_body, elements.msg_form); |
352 | + }; |
353 | + |
354 | + // What to do when a user clicks the update button after editing a msg. |
355 | + module.onUpdateClick = function(elements, baseurl) { |
356 | + // When clicking on "update" button, disable UI elements and send a |
357 | + // request to update the message at the backend. |
358 | + module.showLoading(elements.container); |
359 | + var textarea = elements.textarea.getDOMNode(); |
360 | + var new_content = textarea.value; |
361 | + textarea.disabled = true; |
362 | + elements.update_btn.getDOMNode().disabled = true; |
363 | + |
364 | + module.saveMessageContent( |
365 | + baseurl, new_content, |
366 | + function(err) { module.onMessageSaved(elements, new_content); }, |
367 | + function(err) { module.onMessageSaveError(elements, err); } |
368 | + ); |
369 | + }; |
370 | + |
371 | + // What to do when a message is saved in the backend. |
372 | + module.onMessageSaved = function(elements, new_content) { |
373 | + // When finished updating at the backend, re-enable UI |
374 | + // elements and display the new message. |
375 | + var html_msg = module.htmlify_msg(new_content); |
376 | + elements.msg_body_text.getDOMNode().innerHTML = html_msg; |
377 | + module.hideEditMessageField( |
378 | + elements.msg_body, elements.msg_form); |
379 | + elements.textarea.getDOMNode().disabled = false; |
380 | + elements.update_btn.getDOMNode().disabled = false; |
381 | + module.hideLoading(elements.container); |
382 | + module.showNotification( |
383 | + elements.container, |
384 | + module.msg_edit_success_notification, true); |
385 | + elements.last_edit.getDOMNode().innerHTML = ( |
386 | + ' (last edit a moment ago): '); |
387 | + }; |
388 | + |
389 | + // What to do when a message fails to update on the backend. |
390 | + module.onMessageSaveError = function(elements, err) { |
391 | + // When something goes wrong at the backend, re-enable |
392 | + // UI elements and display an error. |
393 | + module.showNotification( |
394 | + elements.container, |
395 | + module.msg_edit_error_notification, true); |
396 | + elements.textarea.getDOMNode().disabled = false; |
397 | + elements.update_btn.getDOMNode().disabled = false; |
398 | + }; |
399 | + |
400 | + module.wireEventHandlers = function(container) { |
401 | + var node = container.getDOMNode(); |
402 | + var baseurl = node.dataset.baseurl; |
403 | + var elements = { |
404 | + "container": container, |
405 | + "msg_body": container.one('.editable-message-body'), |
406 | + "msg_body_text": container.one('.editable-message-text'), |
407 | + "msg_form": container.one('.editable-message-form'), |
408 | + "edit_btn": container.one('.editable-message-edit-btn'), |
409 | + "update_btn": container.one('.editable-message-update-btn'), |
410 | + "cancel_btn": container.one('.editable-message-cancel-btn'), |
411 | + "last_edit": container.one('.editable-message-last-edit-date') |
412 | + }; |
413 | + elements.textarea = elements.msg_form.one('textarea'); |
414 | + |
415 | + module.hideEditMessageField(elements.msg_body, elements.msg_form); |
416 | + |
417 | + // If the edit button is not present, do not try to bind the |
418 | + // handlers. |
419 | + if (!elements.edit_btn || !baseurl) { |
420 | + return; |
421 | + } |
422 | + |
423 | + elements.edit_btn.on('click', function(e) { |
424 | + module.onEditClick(elements); |
425 | + }); |
426 | + |
427 | + elements.update_btn.on('click', function(e) { |
428 | + module.onUpdateClick(elements, baseurl); |
429 | + }); |
430 | + |
431 | + elements.cancel_btn.on('click', function(e) { |
432 | + module.onEditCancelClick(elements); |
433 | + }); |
434 | + }; |
435 | + |
436 | + module.setup = function() { |
437 | + this.lp_client = new Y.lp.client.Launchpad(); |
438 | + Y.all('.editable-message').each(module.wireEventHandlers); |
439 | + }; |
440 | +}, '0.1', {'requires': ['lp.client', 'node', 'DOM']}); |
441 | diff --git a/lib/lp/services/messages/javascript/tests/test_messages.edit.html b/lib/lp/services/messages/javascript/tests/test_messages.edit.html |
442 | new file mode 100644 |
443 | index 0000000..483f425 |
444 | --- /dev/null |
445 | +++ b/lib/lp/services/messages/javascript/tests/test_messages.edit.html |
446 | @@ -0,0 +1,95 @@ |
447 | +<!DOCTYPE html> |
448 | +<!-- |
449 | +Copyright 2021 Canonical Ltd. This software is licensed under the |
450 | +GNU Affero General Public License version 3 (see the file LICENSE). |
451 | +--> |
452 | + |
453 | +<html> |
454 | + <head> |
455 | + <title>Test message edit</title> |
456 | + |
457 | + <!-- YUI and test setup --> |
458 | + <script type="text/javascript" |
459 | + src="../../../../../../build/js/yui/yui/yui.js"> |
460 | + </script> |
461 | + <link rel="stylesheet" |
462 | + href="../../../../../../build/js/yui/console/assets/console-core.css" /> |
463 | + <link rel="stylesheet" |
464 | + href="../../../../../../build/js/yui/test-console/assets/skins/sam/test-console.css" /> |
465 | + <link rel="stylesheet" |
466 | + href="../../../../../../build/js/yui/test/assets/skins/sam/test.css" /> |
467 | + |
468 | + <script type="text/javascript" |
469 | + src="../../../../../../build/js/lp/app/testing/testrunner.js"></script> |
470 | + |
471 | + <link rel="stylesheet" |
472 | + href="../../../../app/javascript/testing/test.css" /> |
473 | + |
474 | + <!-- Dependencies --> |
475 | + <script type="text/javascript" |
476 | + src="../../../../../../build/js/lp/app/client.js"></script> |
477 | + <script type="text/javascript" |
478 | + src="../../../../../../build/js/lp/app/lp.js"></script> |
479 | + <script type="text/javascript" |
480 | + src="../../../../../../build/js/lp/app/anim/anim.js"></script> |
481 | + <script type="text/javascript" |
482 | + src="../../../../../../build/js/lp/app/extras/extras.js"></script> |
483 | + <script type="text/javascript" |
484 | + src="../../../../../../build/js/lp/app/testing/mockio.js"></script> |
485 | + |
486 | + <!-- The module under test. --> |
487 | + <script type="text/javascript" src="../messages.edit.js"></script> |
488 | + |
489 | + <!-- Any css assert for this module. --> |
490 | + <!-- <link rel="stylesheet" href="../assets/archive-packages-core.css" /> --> |
491 | + |
492 | + <!-- The test suite. --> |
493 | + <script type="text/javascript" src="test_messages.edit.js"></script> |
494 | + |
495 | + </head> |
496 | + <body class="yui3-skin-sam"> |
497 | + <ul id="suites"> |
498 | + <li>lp.services.messages.edit.test</li> |
499 | + </ul> |
500 | + |
501 | + <div class="editable-message" id="first-message" |
502 | + data-baseurl="/message/1"> |
503 | + <div> |
504 | + Comment from @some-user a while ago |
505 | + <span class="editable-message-last-edit-date">:</span> |
506 | + </div> |
507 | + <div class="editable-message-body"> |
508 | + <div class="editable-message-text"></div> |
509 | + The message is above :) |
510 | + </div> |
511 | + <img class="sprite edit action-icon editable-message-edit-btn"> |
512 | + |
513 | + <div class="editable-message-form"> |
514 | + <textarea></textarea> |
515 | + <input type="button" value="Update" class="editable-message-update-btn" /> |
516 | + <input type="button" value="Cancel" class="editable-message-cancel-btn" /> |
517 | + </div> |
518 | + </div> |
519 | + |
520 | + <div class="editable-message" id="second-message" |
521 | + data-baseurl="/message/2"> |
522 | + <div> |
523 | + Comment from @some-user a while ago |
524 | + <span class="editable-message-last-edit-date"> |
525 | + (last edit 5 minutes ago): |
526 | + </span> |
527 | + </div> |
528 | + <div class="editable-message-body"> |
529 | + <div class="editable-message-text"></div> |
530 | + The message is above :) |
531 | + </div> |
532 | + <img class="sprite edit action-icon editable-message-edit-btn"> |
533 | + |
534 | + <div class="editable-message-form"> |
535 | + <textarea></textarea> |
536 | + <input type="button" value="Update" class="editable-message-update-btn" /> |
537 | + <input type="button" value="Cancel" class="editable-message-cancel-btn" /> |
538 | + </div> |
539 | + </div> |
540 | + </body> |
541 | +</html> |
542 | diff --git a/lib/lp/services/messages/javascript/tests/test_messages.edit.js b/lib/lp/services/messages/javascript/tests/test_messages.edit.js |
543 | new file mode 100644 |
544 | index 0000000..a8a982c |
545 | --- /dev/null |
546 | +++ b/lib/lp/services/messages/javascript/tests/test_messages.edit.js |
547 | @@ -0,0 +1,175 @@ |
548 | +/** |
549 | + * Copyright 2012-2021 Canonical Ltd. This software is licensed under the |
550 | + * GNU Affero General Public License version 3 (see the file LICENSE). |
551 | + * |
552 | + * Tests for lp.services.messages.edit. |
553 | + * |
554 | + * @module lp.services.messages.edit |
555 | + * @submodule test |
556 | + */ |
557 | + |
558 | +YUI.add('lp.services.messages.edit.test', function(Y) { |
559 | + |
560 | + var namespace = Y.namespace('lp.services.messages.edit.test'); |
561 | + |
562 | + var suite = new Y.Test.Suite("lp.services.messages.edit Tests"); |
563 | + var module = Y.lp.services.messages.edit; |
564 | + |
565 | + function assertDisplayStyles(items, visibility) { |
566 | + for(var i=items ; i<items.length ; i++) { |
567 | + Y.Assert.areSame(visibility, items[i].getStyle("display")); |
568 | + } |
569 | + } |
570 | + |
571 | + function assertDisplayStyle(item, visibility) { |
572 | + Y.Assert.areSame(visibility, item.getStyle("display")); |
573 | + } |
574 | + |
575 | + var TestMessageEdit = { |
576 | + name: "TestMessageEdit", |
577 | + |
578 | + setUp: function() { |
579 | + this.containers = [ |
580 | + Y.one("#first-message"), Y.one("#second-message")]; |
581 | + this.last_edit = [ |
582 | + this.containers[0].one(".editable-message-last-edit-date"), |
583 | + this.containers[1].one(".editable-message-last-edit-date") |
584 | + ]; |
585 | + this.msg_bodies = [ |
586 | + this.containers[0].one(".editable-message-body"), |
587 | + this.containers[1].one(".editable-message-body") |
588 | + ]; |
589 | + this.msg_texts = [ |
590 | + this.containers[0].one(".editable-message-text"), |
591 | + this.containers[1].one(".editable-message-text") |
592 | + ]; |
593 | + this.msg_forms = [ |
594 | + this.containers[0].one(".editable-message-form"), |
595 | + this.containers[1].one(".editable-message-form") |
596 | + ]; |
597 | + this.edit_icons = [ |
598 | + this.containers[0].one(".editable-message-edit-btn"), |
599 | + this.containers[1].one(".editable-message-edit-btn") |
600 | + ]; |
601 | + this.cancel_btns = [ |
602 | + this.containers[0].one(".editable-message-cancel-btn"), |
603 | + this.containers[1].one(".editable-message-cancel-btn") |
604 | + ]; |
605 | + this.textareas = [ |
606 | + this.msg_forms[0].one("textarea"), |
607 | + this.msg_forms[1].one("textarea") |
608 | + ]; |
609 | + this.update_btns = [ |
610 | + this.containers[0].one(".editable-message-update-btn"), |
611 | + this.containers[1].one(".editable-message-update-btn") |
612 | + ]; |
613 | + |
614 | + for(var i=0 ; i<this.containers.length ; i++) { |
615 | + this.msg_texts[i].getDOMNode().innerHTML = ( |
616 | + "Message number " + i); |
617 | + this.msg_bodies[i].setStyle('display', ''); |
618 | + this.msg_forms[i].setStyle('display', ''); |
619 | + this.textareas[i].getDOMNode().value = ''; |
620 | + this.last_edit[0].getDOMNode().innerHTML = ':'; |
621 | + this.last_edit[1].getDOMNode().innerHTML = ( |
622 | + '(last edit 5 minutes ago):'); |
623 | + } |
624 | + }, |
625 | + |
626 | + test_instantiation_hides_forms: function() { |
627 | + // When editable messages are initialized, the forms should be |
628 | + // hidden. |
629 | + module.setup(); |
630 | + |
631 | + assertDisplayStyles(this.msg_bodies, 'block'); |
632 | + assertDisplayStyles(this.msg_forms, 'none'); |
633 | + }, |
634 | + |
635 | + test_click_edit_icon_shows_form: function() { |
636 | + // Makes sure the form is shown when we click one of the edit icons. |
637 | + module.setup(); |
638 | + this.edit_icons[1].simulate('click'); |
639 | + |
640 | + // Form 1 should be visible... |
641 | + assertDisplayStyle(this.msg_bodies[1], 'none'); |
642 | + assertDisplayStyle(this.msg_forms[1], 'block'); |
643 | + |
644 | + // ... but form 0 should have not be affected. |
645 | + assertDisplayStyle(this.msg_bodies[0], 'block'); |
646 | + assertDisplayStyle(this.msg_forms[0], 'none'); |
647 | + }, |
648 | + |
649 | + test_cancel_button_hides_form: function() { |
650 | + // Makes sure the form is hidden again if the user, after clicking |
651 | + // edit icons, decides to cancel edition. |
652 | + module.setup(); |
653 | + this.edit_icons[1].simulate('click'); |
654 | + this.cancel_btns[1].simulate('click'); |
655 | + |
656 | + assertDisplayStyle(this.msg_bodies[0], 'block'); |
657 | + assertDisplayStyle(this.msg_forms[0], 'none'); |
658 | + assertDisplayStyle(this.msg_bodies[1], 'block'); |
659 | + assertDisplayStyle(this.msg_forms[1], 'none'); |
660 | + }, |
661 | + |
662 | + test_success_save_comment_edition: function() { |
663 | + module.setup(); |
664 | + module.lp_client.io_provider = new Y.lp.testing.mockio.MockIo(); |
665 | + |
666 | + // Edit the comment index #1 |
667 | + this.edit_icons[1].simulate('click'); |
668 | + var new_message = 'edited\nmessage <foo>'; |
669 | + var uri_encoded_new_message = encodeURI(new_message); |
670 | + this.textareas[1].getDOMNode().value = new_message; |
671 | + this.update_btns[1].simulate('click'); |
672 | + |
673 | + // Checks that only the current form interactions are blocked. |
674 | + Y.Assert.isTrue(this.textareas[1].getDOMNode().disabled); |
675 | + Y.Assert.isTrue(this.update_btns[1].getDOMNode().disabled); |
676 | + Y.Assert.isFalse(this.textareas[0].getDOMNode().disabled); |
677 | + Y.Assert.isFalse(this.update_btns[0].getDOMNode().disabled); |
678 | + |
679 | + module.lp_client.io_provider.success({ |
680 | + responseText:'null', |
681 | + responseHeaders: {'Content-Type': 'application/json'} |
682 | + }); |
683 | + Y.Assert.areSame( |
684 | + '<p>edited<br>message <foo></p>', |
685 | + this.msg_texts[1].getDOMNode().innerHTML); |
686 | + |
687 | + // All forms should be released. |
688 | + Y.Assert.isFalse(this.textareas[1].getDOMNode().disabled); |
689 | + Y.Assert.isFalse(this.update_btns[1].getDOMNode().disabled); |
690 | + Y.Assert.isFalse(this.textareas[0].getDOMNode().disabled); |
691 | + Y.Assert.isFalse(this.update_btns[0].getDOMNode().disabled); |
692 | + |
693 | + // Check forms and msg bodies visibility are back to normal. |
694 | + assertDisplayStyle(this.msg_bodies[0], 'block'); |
695 | + assertDisplayStyle(this.msg_forms[0], 'none'); |
696 | + assertDisplayStyle(this.msg_bodies[1], 'block'); |
697 | + assertDisplayStyle(this.msg_forms[1], 'none'); |
698 | + |
699 | + // Check that the request was made correctly. |
700 | + var last_request = module.lp_client.io_provider.last_request; |
701 | + Y.Assert.areSame("/api/devel/message/2", last_request.url); |
702 | + Y.Assert.areSame("POST", last_request.config.method); |
703 | + Y.Assert.areSame( |
704 | + "ws.op=editContent&new_content=" + uri_encoded_new_message, |
705 | + last_request.config.data); |
706 | + |
707 | + // Check that the "last edit" header changed. |
708 | + Y.Assert.areSame(":", this.last_edit[0].getDOMNode().innerHTML); |
709 | + Y.Assert.areSame( |
710 | + " (last edit a moment ago): ", |
711 | + this.last_edit[1].getDOMNode().innerHTML); |
712 | + } |
713 | + |
714 | + }; |
715 | + |
716 | + suite.add(new Y.Test.Case(TestMessageEdit)); |
717 | + |
718 | + namespace.suite = suite; |
719 | + |
720 | +}, "0.1", {"requires": [ |
721 | + "lp.services.messages.edit", "node", "lp.testing.mockio", |
722 | + "node-event-simulate", "test", "lp.anim"]}); |
723 | diff --git a/lib/lp/services/messages/tests/test_yuitests.py b/lib/lp/services/messages/tests/test_yuitests.py |
724 | new file mode 100644 |
725 | index 0000000..e614a1c |
726 | --- /dev/null |
727 | +++ b/lib/lp/services/messages/tests/test_yuitests.py |
728 | @@ -0,0 +1,26 @@ |
729 | +# Copyright 2011-2015 Canonical Ltd. This software is licensed under the |
730 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
731 | + |
732 | +"""Run YUI.test tests.""" |
733 | + |
734 | +__metaclass__ = type |
735 | +__all__ = [] |
736 | + |
737 | +from lp.testing import ( |
738 | + build_yui_unittest_suite, |
739 | + YUIUnitTestCase, |
740 | + ) |
741 | +from lp.testing.layers import YUITestLayer |
742 | + |
743 | + |
744 | +class MessagesYUIUnitTestCase(YUIUnitTestCase): |
745 | + |
746 | + layer = YUITestLayer |
747 | + suite_name = 'MessagesYUIUnitTests' |
748 | + |
749 | + |
750 | +def test_suite(): |
751 | + app_testing_path = 'lp/services/messages' |
752 | + return build_yui_unittest_suite( |
753 | + app_testing_path, |
754 | + MessagesYUIUnitTestCase) |
Pushed the requested changes. This should be good to go now.