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