Thiago F. Pappacena has proposed merging ~pappacena/launchpad:comment-editing-ui-list-revisions into launchpad:master.
Commit message: Showing message revision list in the UI Requested reviews: Launchpad code reviewers (launchpad-reviewers) For more details, see: https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/403213 -- Your team Launchpad code reviewers is requested to review the proposed merge of ~pappacena/launchpad:comment-editing-ui-list-revisions into launchpad:master.
diff --git a/lib/canonical/launchpad/icing/css/base.scss b/lib/canonical/launchpad/icing/css/base.scss index 6ee2779..1e33ca7 100644 --- a/lib/canonical/launchpad/icing/css/base.scss +++ b/lib/canonical/launchpad/icing/css/base.scss @@ -579,6 +579,61 @@ body { margin: 5px; } } + + .message-revision-container { + position: absolute; + background-color: white; + margin-top: 20px; + display: none; + border: 1px solid #ddd; + width: 45em; + z-index: 100; + max-height: 200px; + overflow-y: auto; + overflow-x: hidden; + + .message-revision-container-header { + background-color: #eee; + border-bottom: 1px solid #ddd; + padding: 10px; + + span { + text-align: left; + display: inline-block; + font-size: 110%; + font-weight: bold; + } + + img { + float: right; + cursor: pointer; + } + } + + .message-revision-item { + border-bottom: 1px solid #ddd; + padding: 2px; + + .message-revision-title { + padding: 5px; + cursor: pointer; + font-weight: 300; + } + + .message-revision-body { + display: none; + padding-left: 20px; + padding-bottom: 10px; + } + } + + .active { + background-color: #eee; + .message-revision-title { + font-weight: bold; + } + } + } } @import 'typography', diff --git a/lib/lp/answers/browser/question.py b/lib/lp/answers/browser/question.py index 9d4986d..52cff40 100644 --- a/lib/lp/answers/browser/question.py +++ b/lib/lp/answers/browser/question.py @@ -1195,13 +1195,11 @@ class QuestionMessageDisplayView(LaunchpadView): return check_permission('launchpad.Moderate', self.context) def getBoardCommentCSSClass(self): - css_classes = ["boardComment"] + css_classes = ["boardComment", "editable-message"] if not self.context.visible: # If a comment that isn't visible is being rendered, it's being # rendered for an admin or registry_expert. css_classes.append("adminHiddenComment") - if self.can_edit: - css_classes.append("editable-message") return " ".join(css_classes) @property diff --git a/lib/lp/answers/stories/question-workflow.txt b/lib/lp/answers/stories/question-workflow.txt index 5e39acd..1a0b67a 100755 --- a/lib/lp/answers/stories/question-workflow.txt +++ b/lib/lp/answers/stories/question-workflow.txt @@ -215,7 +215,7 @@ The confirmed answer is also highlighted. >>> soup = find_main_content(owner_browser.contents) >>> bestAnswer = soup.find_all('div', 'boardComment')[-2] - >>> print(bestAnswer.find('img')) + >>> print(bestAnswer.find_all('img')[1]) <img ... src="/@@/favourite-yes" ... title="Marked as best answer"/> >>> print(soup.find( diff --git a/lib/lp/answers/templates/questionmessage-display.pt b/lib/lp/answers/templates/questionmessage-display.pt index fc3651c..c1ed29a 100644 --- a/lib/lp/answers/templates/questionmessage-display.pt +++ b/lib/lp/answers/templates/questionmessage-display.pt @@ -14,6 +14,25 @@ <tbody> <tr> <td> + <div class="message-revision-container"> + <div class="message-revision-container-header"> + <span>Revision history for this message</span> + <img src="/+icing/build/overlay/assets/skins/sam/images/close.gif" + class="message-revision-close"/> + </div> + <script type="text/template"> + <div class='message-revision-item'> + <div class='message-revision-title'> + <a class="js-action"> + Revision #{revision}, created at {date_created} + </a> + </div> + <div class='message-revision-body'>{content}</div> + </div> + </script> + + <div class="message-revision-list"></div> + </div> <tal:bestanswer condition="view/isBestAnswer"> <img src="/@@/favourite-yes" style="float:right;" alt="Best" title="Marked as best answer" /> @@ -28,11 +47,12 @@ datetime context/datecreated/fmt:isodate" tal:content="context/datecreated/fmt:displaydate">Thursday 13:21 </time><span class="editable-message-last-edit-date"><tal:last-edit condition="context/date_last_edited"> - (last edit <time + <a href="#" class="editable-message-last-edit-link" + tal:condition="context/date_last_edited">(last edit <time itemprop="editTime" tal:attributes="title context/date_last_edited/fmt:datetime; datetime context/date_last_edited/fmt:isodate" - tal:content="context/date_last_edited/fmt:displaydate" />)</tal:last-edit>: + tal:content="context/date_last_edited/fmt:displaydate" />)</a></tal:last-edit>: </span> </td> <td> diff --git a/lib/lp/services/messages/interfaces/messagerevision.py b/lib/lp/services/messages/interfaces/messagerevision.py index 3ee5a90..aa0ff0a 100644 --- a/lib/lp/services/messages/interfaces/messagerevision.py +++ b/lib/lp/services/messages/interfaces/messagerevision.py @@ -35,7 +35,8 @@ class IMessageRevisionView(Interface): """IMessageRevision readable attributes.""" id = Int(title=_("ID"), required=True, readonly=True) - revision = Int(title=_("Revision number"), required=True, readonly=True) + revision = exported( + Int(title=_("Revision number"), required=True, readonly=True)) content = exported(Text( title=_("The message at the given revision"), diff --git a/lib/lp/services/messages/javascript/messages.edit.js b/lib/lp/services/messages/javascript/messages.edit.js index 3c4c0e5..912a24c 100644 --- a/lib/lp/services/messages/javascript/messages.edit.js +++ b/lib/lp/services/messages/javascript/messages.edit.js @@ -2,7 +2,8 @@ * GNU Affero General Public License version 3 (see the file LICENSE). * * This modules controls HTML comments in order to make them editable. To do - * so, it requires: + * so, it requires some definitions (see test_messages.edit.html file for the + * complete structure reference): * - A div container with the class .editable-message containing everything * else related to the message * - A data-baseurl="/path/to/msg" on the .editable-message container @@ -14,6 +15,16 @@ * - A .editable-message-last-edit-date span, where we update the date of the * last message editing. * + * For the message revision history, you should define: + * - A .editable-message-last-edit-link, that will show the revisions pop-up + * once clicked. + * - A .message-revision-container, holding the header and the body of the + * revision list, and the template for each revision item. + * - A .message-revision-container-header. + * - A .message-revision-list, where the revisions will be placed. + * - A <script type="text/template">, with the definition of how each item + * should look like. + * * Once those HTML elements are available in the page, this module should be * initialized with `lp.services.messages.edit.setup()`. * @@ -23,15 +34,6 @@ YUI.add('lp.services.messages.edit', function(Y) { var module = Y.namespace('lp.services.messages.edit'); - // XXX pappacena 2021-05-21: We should drop this message once we have a - // way to list the old message revisions in the web UI. - module.msg_edit_success_notification = ( - "Message edited, but the original content may still be publicly " + - "visible using the API.<br />Please " + - "<a href='https://launchpad.net/+apidoc/devel.html#message'>" + - "check the API documentation</a> in case you " + - "need to remove old message revisions." - ); module.msg_edit_error_notification = ( "There was an error updating the comment. " + "Please try again in a few minutes." @@ -115,7 +117,7 @@ YUI.add('lp.services.messages.edit', function(Y) { // text area. module.showEditMessageField(elements.msg_body, elements.msg_form); elements.msg_form.one('textarea').getDOMNode().focus(); - } + }; // What to do when a user clicks "cancel edit" button. module.onEditCancelClick = function(elements) { @@ -134,13 +136,15 @@ YUI.add('lp.services.messages.edit', function(Y) { module.saveMessageContent( baseurl, new_content, - function(err) { module.onMessageSaved(elements, new_content); }, + function(err) { + module.onMessageSaved(elements, new_content, baseurl); + }, function(err) { module.onMessageSaveError(elements, err); } ); }; // What to do when a message is saved in the backend. - module.onMessageSaved = function(elements, new_content) { + module.onMessageSaved = function(elements, new_content, baseurl) { // When finished updating at the backend, re-enable UI // elements and display the new message. var html_msg = module.htmlify_msg(new_content); @@ -150,11 +154,18 @@ YUI.add('lp.services.messages.edit', function(Y) { elements.textarea.getDOMNode().disabled = false; elements.update_btn.getDOMNode().disabled = false; module.hideLoading(elements.container); - module.showNotification( - elements.container, - module.msg_edit_success_notification, true); elements.last_edit.getDOMNode().innerHTML = ( - ' (last edit a moment ago): '); + '<a href="#" class="editable-message-last-edit-link">' + + '(last edit a moment ago):' + + '</a>'); + + // Wire click handler to the newly created "last edit" button. + var last_edit_btn = elements.container.one( + '.editable-message-last-edit-link'); + last_edit_btn.on('click', function(e) { + e.preventDefault(); + module.onLastEditClick(elements, baseurl); + }); }; // What to do when a message fails to update on the backend. @@ -168,6 +179,74 @@ YUI.add('lp.services.messages.edit', function(Y) { elements.update_btn.getDOMNode().disabled = false; }; + module.fillMessageRevisions = function(elements, revisions) { + // Position the message revision list element. + revisions = revisions.reverse(); + var revisions_container = elements.container.one( + ".message-revision-container"); + var last_edit_el = elements.last_edit.getDOMNode(); + var target_position = last_edit_el.getBoundingClientRect(); + var nodes_holder = revisions_container.one(".message-revision-list"); + var template = revisions_container.one( + "script[type='text/template']").getDOMNode().innerHTML; + + revisions_container.setStyle('left', target_position.left); + revisions_container.setStyle('display', 'block'); + revisions_container.one(".message-revision-close").on( + "click", function() { + nodes_holder.getDOMNode().innerHTML = ''; + revisions_container.setStyle('display', 'none'); + }); + + var content = ""; + revisions.forEach(function(rev) { + var attrs = rev.getAttrs(); + var date_created = new Date(attrs.date_created); + attrs.date_created = ( + date_created.toISOString().substr(0,10) + ", " + + date_created.toLocaleTimeString()); + attrs.content = module.htmlify_msg(attrs.content); + content += Y.Lang.sub(template, attrs); + }); + + nodes_holder.getDOMNode().innerHTML = content; + nodes_holder.all(".message-revision-item").each(function(rev_item) { + rev_item.one(".message-revision-title").on('click', function() { + nodes_holder.all('.message-revision-body').setStyle( + 'display', 'none'); + var body = rev_item.one('.message-revision-body'); + var current_display = body.getStyle('display'); + body.setStyle( + 'display', current_display === 'block'? 'none' : 'block'); + nodes_holder.all(".message-revision-item").removeClass( + 'active'); + rev_item.addClass('active'); + }); + }); + }; + + module.onLastEditClick = function(elements, baseurl) { + // Hide all open revision containers. + Y.all('.message-revision-container').each(function(container) { + container.setStyle('display', 'none'); + }); + + var url = "/api/devel" + baseurl + "/revisions"; + var config = { + on: { + success: function(response) { + module.fillMessageRevisions(elements, response.entries); + }, + failure: function(err) { + alert("Error fetching revisions."); + } + }, + // XXX pappacena 2021-05-19: Pagination will be needed here. + size: 100 + }; + this.lp_client.get(url, config); + }; + module.wireEventHandlers = function(container) { var node = container.getDOMNode(); var baseurl = node.dataset.baseurl; @@ -179,12 +258,20 @@ YUI.add('lp.services.messages.edit', function(Y) { "edit_btn": container.one('.editable-message-edit-btn'), "update_btn": container.one('.editable-message-update-btn'), "cancel_btn": container.one('.editable-message-cancel-btn'), - "last_edit": container.one('.editable-message-last-edit-date') + "last_edit": container.one('.editable-message-last-edit-date'), + "last_edit_btn": container.one('.editable-message-last-edit-link') }; elements.textarea = elements.msg_form.one('textarea'); module.hideEditMessageField(elements.msg_body, elements.msg_form); + if (elements.last_edit_btn) { + elements.last_edit_btn.on('click', function(e) { + e.preventDefault(); + module.onLastEditClick(elements, baseurl); + }); + } + // If the edit button is not present, do not try to bind the // handlers. if (!elements.edit_btn || !baseurl) { diff --git a/lib/lp/services/messages/javascript/tests/test_messages.edit.html b/lib/lp/services/messages/javascript/tests/test_messages.edit.html index 94c60ea..21667b5 100644 --- a/lib/lp/services/messages/javascript/tests/test_messages.edit.html +++ b/lib/lp/services/messages/javascript/tests/test_messages.edit.html @@ -52,10 +52,45 @@ GNU Affero General Public License version 3 (see the file LICENSE). <li>lp.services.messages.edit.test</li> </ul> + <!-- First editable message --> <div class="editable-message" id="first-message" data-baseurl="/message/1"> + <!-- Revision list container and its template --> + <div class="message-revision-container"> + <!-- The revision list pop-up header, with the image that closes + the pop-up when clicked. --> + <div class="message-revision-container-header"> + <span>Revision history for this message</span> + <img src="/+icing/build/overlay/assets/skins/sam/images/close.gif" + class="message-revision-close"/> + </div> + <!-- The template for each revision item in the list. The + content of this tag will be repeated multiple times, replacing + the {variable} with each MessageRevision entry returned by + the API. + --> + <script type="text/template"> + <div class='message-revision-item'> + <!-- The description of the revision, that will expand the + revision content once clicked. --> + <div class='message-revision-title'> + <a class="js-action">Revision #{revision}, created at {date_created}</a> + </div> + <!-- The revision content itself --> + <div class='message-revision-body'>{content}</div> + </div> + </script> + <!-- This is the place where all the revisions will be placed, + by applying the above template to each MessageRevision entry + returned by the API. + --> + <div class="message-revision-list"></div> + </div> + + <!-- Message body --> <div> Comment from @some-user a while ago + Comment from @some-user a while ago <span class="editable-message-last-edit-date">:</span> </div> <div class="editable-message-body"> @@ -71,12 +106,34 @@ GNU Affero General Public License version 3 (see the file LICENSE). </div> </div> + <!-- Second editable message --> <div class="editable-message" id="second-message" data-baseurl="/message/2"> + <!-- Revision list container and its template --> + <div class="message-revision-container"> + <div class="message-revision-container-header"> + <span>Revision history for this message</span> + <img src="/+icing/build/overlay/assets/skins/sam/images/close.gif" + class="message-revision-close"/> + </div> + <script type="text/template"> + <div class='message-revision-item'> + <div class='message-revision-title'> + <a class="js-action">Revision #{revision}, created at {date_created}</a> + </div> + <div class='message-revision-body'>{content}</div> + </div> + </script> + <div class="message-revision-list"></div> + </div> + + <!-- Message body --> <div> Comment from @some-user a while ago <span class="editable-message-last-edit-date"> - (last edit 5 minutes ago): + <a href="#" class="editable-message-last-edit-link"> + (last edit 5 minutes ago): + </a> </span> </div> <div class="editable-message-body"> diff --git a/lib/lp/services/messages/javascript/tests/test_messages.edit.js b/lib/lp/services/messages/javascript/tests/test_messages.edit.js index a8a982c..abdefd5 100644 --- a/lib/lp/services/messages/javascript/tests/test_messages.edit.js +++ b/lib/lp/services/messages/javascript/tests/test_messages.edit.js @@ -35,6 +35,14 @@ YUI.add('lp.services.messages.edit.test', function(Y) { this.containers[0].one(".editable-message-last-edit-date"), this.containers[1].one(".editable-message-last-edit-date") ]; + this.revision_history_link = [ + this.containers[0].one(".editable-message-last-edit-link"), + this.containers[1].one(".editable-message-last-edit-link") + ]; + this.revision_history_lists = [ + this.containers[0].one(".message-revision-list"), + this.containers[1].one(".message-revision-list") + ]; this.msg_bodies = [ this.containers[0].one(".editable-message-body"), this.containers[1].one(".editable-message-body") @@ -73,6 +81,7 @@ YUI.add('lp.services.messages.edit.test', function(Y) { this.last_edit[0].getDOMNode().innerHTML = ':'; this.last_edit[1].getDOMNode().innerHTML = ( '(last edit 5 minutes ago):'); + this.revision_history_lists[i].getDOMNode().innerHTML = ''; } }, @@ -160,8 +169,76 @@ YUI.add('lp.services.messages.edit.test', function(Y) { // Check that the "last edit" header changed. Y.Assert.areSame(":", this.last_edit[0].getDOMNode().innerHTML); Y.Assert.areSame( - " (last edit a moment ago): ", + '<a href="#" class="editable-message-last-edit-link">' + + '(last edit a moment ago):</a>', this.last_edit[1].getDOMNode().innerHTML); + }, + + test_shows_revision_history: function() { + module.setup(); + module.lp_client.io_provider = new Y.lp.testing.mockio.MockIo(); + var revisions_container = this.containers[1].one( + '.message-revision-container'); + // Revisions container should not be shown before it's clicked. + Y.Assert.areSame('none', revisions_container.getStyle('display')); + + // Simulates the click and the request. + this.revision_history_link[1].simulate('click'); + + var response = { + "total_size": 1, "start": 0, + "entries": [{ + "revision": 1, + "content": "content 1", + "date_created": "2021-05-10T19:41:36.102749+00:00" + }, { + "revision": 2, + "content": "content 2", + "date_created": "2021-05-11T22:17:11.000123+00:00" + }] + }; + module.lp_client.io_provider.success({ + responseText: Y.JSON.stringify(response), + responseHeaders: {'Content-Type': 'application/json'} + }); + + // Make sure it didn't fill the first container. + Y.Assert.areSame( + "", this.revision_history_lists[0].getDOMNode().innerHTML); + + // Check that revision list pop-up is shown + Y.Assert.areSame('block', revisions_container.getStyle('display')); + + // Check the items in the pop-up revisions list. + var revisions = this.revision_history_lists[1].all( + ".message-revision-item"); + Y.Assert.areSame(2, revisions.size()); + revisions.each(function(rev, i) { + // Entries are shown in reverse order. + var entry = response.entries[response.entries.length - i -1]; + var date_created = new Date(entry.date_created); + entry['formatted_date'] = ( + date_created.toISOString().substr(0,10) + ", " + + date_created.toLocaleTimeString()); + var title = rev.one('.message-revision-title'); + var body = rev.one('.message-revision-body'); + var expected_title = Y.Lang.sub( + '<a class="js-action">' + + 'Revision #{revision}, created at {formatted_date}' + + '</a>', + entry); + Y.Assert.areSame( + expected_title, title.getDOMNode().innerHTML.trim()); + Y.Assert.areSame( + module.htmlify_msg(entry.content), + body.getDOMNode().innerHTML); + }); + + // Lets make sure that a click on the "close" button hides the + // revisions list. + revisions_container.one( + '.message-revision-close').simulate('click'); + Y.Assert.areSame('none', revisions_container.getStyle('display')); } };
_______________________________________________ Mailing list: https://launchpad.net/~launchpad-reviewers Post to : launchpad-reviewers@lists.launchpad.net Unsubscribe : https://launchpad.net/~launchpad-reviewers More help : https://help.launchpad.net/ListHelp