From fd850e00db835d2b84c59014c3b1021fea2294fc Mon Sep 17 00:00:00 2001 From: Israel Madueme Date: Fri, 10 Aug 2018 08:57:01 -0400 Subject: Bug 1456878 - Support markdown comments --- .../template/en/default/email/bugmail.html.tmpl | 7 +- .../en/default/bug_modal/activity_stream.html.tmpl | 15 +- .../template/en/default/bug_modal/edit.html.tmpl | 4 +- .../en/default/bug_modal/new_comment.html.tmpl | 15 +- extensions/BugModal/web/bug_modal.css | 49 ++++- extensions/BugModal/web/bug_modal.js | 212 +++++++++++++++++++-- .../en/default/pages/editcomments.html.tmpl | 2 +- .../hook/bug/comments-aftercomments.html.tmpl | 2 +- .../hook/bug/comments-comment_banner.html.tmpl | 6 +- 9 files changed, 273 insertions(+), 39 deletions(-) (limited to 'extensions') diff --git a/extensions/BMO/template/en/default/email/bugmail.html.tmpl b/extensions/BMO/template/en/default/email/bugmail.html.tmpl index 0b08e4a86..5ca2c2a1b 100644 --- a/extensions/BMO/template/en/default/email/bugmail.html.tmpl +++ b/extensions/BMO/template/en/default/email/bugmail.html.tmpl @@ -50,7 +50,12 @@ at [% comment.creation_ts FILTER time(undef, to_user.timezone) %] [% END %] -
[% comment.body_full({ wrap => 1 }) FILTER quoteUrls(bug, comment) %]
+ [% IF comment.is_markdown %] + [% comment_tag = 'div' %] + [% ELSE %] + [% comment_tag = 'pre' %] + [% END %] + <[% comment_tag FILTER none %] class="comment" style="font-size: initial">[% comment.body_full({ wrap => 1 }) FILTER renderComment(bug, comment) %] [% END %] diff --git a/extensions/BugModal/template/en/default/bug_modal/activity_stream.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/activity_stream.html.tmpl index 08c6b5b64..340bb6f81 100644 --- a/extensions/BugModal/template/en/default/bug_modal/activity_stream.html.tmpl +++ b/extensions/BugModal/template/en/default/bug_modal/activity_stream.html.tmpl @@ -244,8 +244,17 @@ [% END %] [% BLOCK comment_body %] - + [%~ comment.body_full FILTER renderComment(bug, comment) ~%] [% END %] [% diff --git a/extensions/BugModal/template/en/default/bug_modal/edit.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/edit.html.tmpl index e926c04b4..e2e8bc124 100644 --- a/extensions/BugModal/template/en/default/bug_modal/edit.html.tmpl +++ b/extensions/BugModal/template/en/default/bug_modal/edit.html.tmpl @@ -202,7 +202,7 @@ no_label = 1 hide_on_edit = 1 %] -

[% bug.short_desc FILTER quoteUrls(bug) FILTER wbr %]

+

[% bug.short_desc FILTER renderComment(bug, undef, 1) FILTER wbr %]

[% END %] [%# alias %] @@ -1191,7 +1191,7 @@ [% END %] [% END %] -
[% bug.cf_user_story FILTER quoteUrls(bug) %]
+
[% bug.cf_user_story FILTER renderComment(bug, undef) %]
[% IF user.id %] -
- - Comments Subject to Etiquette and Contributor Guidelines +
diff --git a/extensions/BugModal/web/bug_modal.css b/extensions/BugModal/web/bug_modal.css index ee50c6b77..bf291d3b6 100644 --- a/extensions/BugModal/web/bug_modal.css +++ b/extensions/BugModal/web/bug_modal.css @@ -296,7 +296,6 @@ input[type="number"] { #user-story { margin: 0; - white-space: pre-wrap; min-height: 2em; } @@ -630,7 +629,8 @@ body.platform-Win32 .comment-text, body.platform-Win64 .comment-text { font-family: "Fira Mono", monospace; } -.comment-text span.quote, .comment-text span.quote_wrapped { +.comment-text span.quote, .comment-text span.quote_wrapped, +div.comment-text pre { background: #eee !important; color: #444 !important; display: block !important; @@ -644,6 +644,40 @@ body.platform-Win32 .comment-text, body.platform-Win64 .comment-text { border: 1px dashed darkred; } +/* Markdown comments */ +div.comment-text { + white-space: normal; + padding: 0 8px 0 8px; + font-family: inherit !important; +} + +div.comment-text code { + color: #444; + background-color: #eee; + font-size: 13px; + font-family: "Fira Mono","Droid Sans Mono",Menlo,Monaco,"Courier New",monospace; +} + +div.comment-text table { + border-collapse: collapse; +} + +div.comment-text th, div.comment-text td { + padding: 5px 10px; + border: 1px solid #ccc; +} + +div.comment-text hr { + display: block !important; +} + +div.comment-text blockquote { + background: #fcfcfc; + border-left: 5px solid #ccc; + margin: 1.5em 10px; + padding: 0.5em 10px; +} + .comment-tags { padding: 0 8px 2px 8px !important; } @@ -717,11 +751,16 @@ body.platform-Win32 .comment-text, body.platform-Win64 .comment-text { margin-top: 20px; } -#add-comment-private, -#bugzilla-etiquette { +#add-comment-private { float: right; } +#add-comment-tips { + display: flex; + justify-content: space-between; + margin-bottom: 1em; +} + #comment { border: 1px solid #ccc; } @@ -730,7 +769,7 @@ body.platform-Win32 .comment-text, body.platform-Win64 .comment-text { clear: both; width: 100%; box-sizing: border-box !important; - margin: 0 0 1em; + margin: 0 0 0.5em; max-width: 1024px; } diff --git a/extensions/BugModal/web/bug_modal.js b/extensions/BugModal/web/bug_modal.js index a4ae83d72..19b5bfa2f 100644 --- a/extensions/BugModal/web/bug_modal.js +++ b/extensions/BugModal/web/bug_modal.js @@ -858,31 +858,54 @@ $(function() { var prefix = "(In reply to " + comment_author + " from comment #" + comment_id + ")\n"; var reply_text = ""; - if (BUGZILLA.user.settings.quote_replies == 'quoted_reply') { - var text = $('#ct-' + comment_id).text(); - reply_text = prefix + wrapReplyText(text); - } else if (BUGZILLA.user.settings.quote_replies == 'simply_reply') { - reply_text = prefix; + + var quoteMarkdown = function($comment) { + const uid = $comment.data('uniqueid'); + bugzilla_ajax( + { + url: `rest/bug/comment/${uid}`, + }, + (data) => { + const quoted = data['comments'][uid]['text'].replace(/\n/g, "\n > "); + reply_text = `${prefix}\n > ${quoted}`; + populateNewComment(); + } + ); } - // quoting a private comment, check the 'private' cb - $('#add-comment-private-cb').prop('checked', - $('#add-comment-private-cb:checked').length || $('#is-private-' + comment_id + ':checked').length); + var populateNewComment = function() { + // quoting a private comment, check the 'private' cb + $('#add-comment-private-cb').prop('checked', + $('#add-comment-private-cb:checked').length || $('#is-private-' + comment_id + ':checked').length); - // remove embedded links to attachment details - reply_text = reply_text.replace(/(attachment\s+\d+)(\s+\[[^\[\n]+\])+/gi, '$1'); + // remove embedded links to attachment details + reply_text = reply_text.replace(/(attachment\s+\d+)(\s+\[[^\[\n]+\])+/gi, '$1'); - $.scrollTo($('#comment'), function() { - if ($('#comment').val() != reply_text) { - $('#comment').val($('#comment').val() + reply_text); - } + $.scrollTo($('#comment'), function() { + if ($('#comment').val() != reply_text) { + $('#comment').val($('#comment').val() + reply_text); + } - if (BUGZILLA.user.settings.autosize_comments) { - autosize.update($('#comment')); - } + if (BUGZILLA.user.settings.autosize_comments) { + autosize.update($('#comment')); + } - $('#comment').focus(); - }); + $('#comment').trigger('input').focus(); + }); + } + + if (BUGZILLA.user.settings.quote_replies == 'quoted_reply') { + var $comment = $('#ct-' + comment_id); + if ($comment.attr('data-ismarkdown')) { + quoteMarkdown($comment); + } else { + reply_text = prefix + wrapReplyText($comment.text()); + populateNewComment(); + } + } else if (BUGZILLA.user.settings.quote_replies == 'simply_reply') { + reply_text = prefix; + populateNewComment(); + } }); if (BUGZILLA.user.settings.autosize_comments) { @@ -1320,12 +1343,163 @@ $(function() { saveBugComment(event.target.value); }); + function smartLinkPreviews() { + const filterUnique = (value, index, array) => value && array.indexOf(value) === index; + const reduceListToMap = (all, one) => { all[one['id']] = one; return all; }; + + const getResourceId = anchor => { + if (['/bug/', '/attachment/'].some((path) => anchor.pathname.startsWith(path))) { + return anchor.pathname.split('/')[2]; + } else { + return (new URL(anchor.href)).searchParams.get("id"); + } + }; + + const findLinkElements = pathnames => { + return ( + Array + .from(document.querySelectorAll('.comment-text a')) + .filter(anchor => { + return ( + `${anchor.origin}/` === BUGZILLA.constant.URL_BASE && + pathnames.some((p) => anchor.pathname.startsWith(p)) && + /^\d+$/.test(getResourceId(anchor)) + ) + }) + .filter(anchor => + // Get only links created by markdown or private links. + !anchor.hasAttribute('title') || anchor.classList.contains('bz_private_link') + ) + .map(anchor => { + return { + id: getResourceId(anchor), + element: anchor + } + }) + ) + }; + + const enhanceBugLinks = () => { + let bugLinks = findLinkElements(['/show_bug.cgi', '/bug/']); + let bugIds = bugLinks.map((bug) => parseInt(bug['id'])).filter(filterUnique).join(','); + let params = $.param({ + Bugzilla_api_token: BUGZILLA.api_token, + id: bugIds, + include_fields: 'id,summary,status,resolution,is_open' + }); + + if(!bugIds) return; + + fetch(`/rest/bug?${params}`) + .then(response => { + if(response.ok){ + return response.json(); + } + throw new Error(`/rest/bug?ids=${bugIds} response not ok`); + }) + .then(responseJson => { + return responseJson.bugs.reduce(reduceListToMap, {}); + }) + .then(bugs => { + bugLinks.forEach(bugLink => { + let bug = bugs[bugLink['id']]; + if(!bug) return; + + bugLink.element.setAttribute( + "title", `${bug.status} ${bug.resolution} - ${bug.summary}` + ); + bugLink.element.classList.add('bz_bug_link'); + bugLink.element.classList.add(`bz_status_${bug.status}`); + if(!bug.is_open) { + bugLink.element.classList.add('bz_closed'); + } + $(bugLink.element).tooltip({ + position: { my: "left top+8", at: "left bottom", collision: "flipfit" }, + show: { effect: 'none' }, + hide: { effect: 'none' } + }); + }); + }) + .catch(e => console.log(e)); + }; + + const enhanceAttachmentLinks = () => { + let attachmentLinks = findLinkElements(['/attachment.cgi']); + let attachmentIds = ( + attachmentLinks.map(attachment => parseInt(attachment['id'])).filter(filterUnique) + ); + let params = $.param({ + Bugzilla_api_token: BUGZILLA.api_token, + include_fields: 'id,description,is_obsolete' + }); + + if(!attachmentIds) return; + + // Fetch all attachments for this bug only. This endpoint filters out + // attachments the user can't see for us (e.g. ones marked private). + // This one request will likely retrieve most of the attachments we need. + fetch(`/rest/bug/${BUGZILLA.bug_id}/attachment?${params}`) + .then(response => { + if(response.ok){ + return response.json(); + } + throw Error(`/rest/bug/${BUGZILLA.bug_id}/attachment response not ok`); + }) + .then(responseJson => { + return responseJson['bugs'][BUGZILLA.bug_id] || []; + }) + .then(attachments => { + // The BMO rest API that lets us batch request attachment ids unfortunatley + // fails the whole batch if the user is unable to view any of the attachments. + // So, we query each attachment id individually and group them as a promsie. + let missingAttachments = ( + attachmentIds + .filter(id => !attachments.map(attachment => attachment.id).includes(id)) + .map(attachmentId => { + return ( + fetch(`/rest/bug/attachment/${attachmentId}?${params}`) + .then((response) => { + // It's ok if the request failed. + return response.json(); + }) + .then(responseJson => { + // May be undefined. + return responseJson['attachments'][attachmentId]; + }) + ); + }) + ); + return Promise.all(attachments.concat(missingAttachments)); + }) + .then(attachments => { + // Remove undefined attachments and convert from list to dictonary mapped by id. + return attachments.filter(filterUnique).reduce(reduceListToMap, {}); + }) + .then(attachments => { + // Now we have all attachment data the user is able to see. + attachmentLinks.forEach(attachmentLink => { + let attachment = attachments[attachmentLink.id]; + if(!attachment) return; + + attachmentLink.element.setAttribute("title", attachment.description); + if(attachment.is_obsolete){ + attachmentLink.element.classList.add('bz_obsolete'); + } + }); + }) + .catch(e => console.log(e)); + }; + enhanceBugLinks(); + enhanceAttachmentLinks(); + } + // finally switch to edit mode if we navigate back to a page that was editing $(window).on('pageshow', restoreEditMode); $(window).on('pageshow', restoreSavedBugComment); $(window).on('focus', restoreSavedBugComment); restoreEditMode(); restoreSavedBugComment(); + smartLinkPreviews(); }); function confirmUnsafeURL(url) { diff --git a/extensions/EditComments/template/en/default/pages/editcomments.html.tmpl b/extensions/EditComments/template/en/default/pages/editcomments.html.tmpl index 13364f5b1..b38a6dc0b 100644 --- a/extensions/EditComments/template/en/default/pages/editcomments.html.tmpl +++ b/extensions/EditComments/template/en/default/pages/editcomments.html.tmpl @@ -34,7 +34,7 @@
-    [%- a.original ? a.body : a.new FILTER quoteUrls(bug) -%]
+    [%- a.original ? a.body : a.new FILTER renderComment(bug) -%]
   
[% END %] diff --git a/extensions/ShadowBugs/template/en/default/hook/bug/comments-aftercomments.html.tmpl b/extensions/ShadowBugs/template/en/default/hook/bug/comments-aftercomments.html.tmpl index 3b04475fb..6270bd76c 100644 --- a/extensions/ShadowBugs/template/en/default/hook/bug/comments-aftercomments.html.tmpl +++ b/extensions/ShadowBugs/template/en/default/hook/bug/comments-aftercomments.html.tmpl @@ -64,7 +64,7 @@
-  [%- comment_text FILTER quoteUrls(public_bug, comment) -%]
+  [%- comment_text FILTER renderComment(public_bug, comment) -%]
 
[% END %] diff --git a/extensions/UserStory/template/en/default/hook/bug/comments-comment_banner.html.tmpl b/extensions/UserStory/template/en/default/hook/bug/comments-comment_banner.html.tmpl index e063ac942..cbc4fe951 100644 --- a/extensions/UserStory/template/en/default/hook/bug/comments-comment_banner.html.tmpl +++ b/extensions/UserStory/template/en/default/hook/bug/comments-comment_banner.html.tmpl @@ -43,9 +43,9 @@ [% IF bug.cf_user_story != "" %]
-
-        [%- bug.cf_user_story FILTER quoteUrls(bug) -%]
-      
+
+ [%- bug.cf_user_story FILTER renderComment(bug, undef) -%] +
[% ELSE %]
-- cgit v1.2.3-24-g4f1b