diff options
Diffstat (limited to 'extensions/BugModal')
5 files changed, 262 insertions, 33 deletions
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 %] - <pre class="comment-text [%= "bz_private" IF comment.is_private %]" id="ct-[% comment.count FILTER none %]" - [% IF comment.collapsed +%] style="display:none"[% END ~%] + [% IF comment.is_markdown %] + [% comment_tag = 'div' %] + [% ELSE %] + [% comment_tag = 'pre' %] + [% END %] + + <[% comment_tag FILTER none %] class="comment-text [%= "bz_private" IF comment.is_private %]" + id="ct-[% comment.count FILTER none %]" + data-uniqueid="[% comment.id FILTER none %]" + [% IF comment.is_markdown +%] data-ismarkdown="true" [% END ~%] + [% IF comment.collapsed +%] style="display:none"[% END ~%] >[% FILTER collapse %] [% IF comment.is_about_attachment && comment.attachment.is_image ~%] <a href="attachment.cgi?id=[% comment.attachment.id FILTER none %]" @@ -253,7 +262,7 @@ class="lightbox"><img src="extensions/BugModal/web/image.png" width="16" height="16"></a> [% END %] [% END %] - [%~ comment.body_full FILTER quoteUrls(bug, comment) ~%]</pre> + [%~ comment.body_full FILTER renderComment(bug, comment) ~%]</[% comment_tag FILTER none %]> [% 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 %] - <h1 id="field-value-short_desc">[% bug.short_desc FILTER quoteUrls(bug) FILTER wbr %]</h1> + <h1 id="field-value-short_desc">[% bug.short_desc FILTER renderComment(bug, undef, 1) FILTER wbr %]</h1> [% END %] [%# alias %] @@ -1191,7 +1191,7 @@ [% END %] </div> [% END %] - <pre id="user-story">[% bug.cf_user_story FILTER quoteUrls(bug) %]</pre> + <div id="user-story" class="comment-text">[% bug.cf_user_story FILTER renderComment(bug, undef) %]</div> [% IF user.id %] <textarea id="cf_user_story" name="cf_user_story" style="display:none" rows="10" cols="80"> [%~ bug.cf_user_story FILTER html ~%] diff --git a/extensions/BugModal/template/en/default/bug_modal/new_comment.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/new_comment.html.tmpl index 63663b4d5..63c8cf197 100644 --- a/extensions/BugModal/template/en/default/bug_modal/new_comment.html.tmpl +++ b/extensions/BugModal/template/en/default/bug_modal/new_comment.html.tmpl @@ -45,12 +45,19 @@ <textarea rows="5" cols="80" name="comment" id="comment" aria-labelledby="comment-edit-tab"></textarea> </div> <div id="comment-preview-tabpanel" class="comment-tabpanel" role="tabpanel" aria-labelledby="comment-preview-tab" style="display:none"> - <pre id="comment-preview" class="comment-text"></pre> + <div id="comment-preview" class="comment-text"></div> </div> - <div id="bugzilla-etiquette"> - <a href="page.cgi?id=etiquette.html" target="_blank" tabindex="-1"> - Comments Subject to Etiquette and Contributor Guidelines</a> + <div id="add-comment-tips"> + <div id="comment-markdown-tip"> + <img src="extensions/BMO/web/images/notice.png" width="16" height="16"> + <a href="https://guides.github.com/features/mastering-markdown/" target="_blank">Markdown styling now supported</a> + </div> + + <div id="bugzilla-etiquette"> + <a href="page.cgi?id=etiquette.html" target="_blank" tabindex="-1"> + Comments Subject to Etiquette and Contributor Guidelines</a> + </div> </div> <div id="after-comment-commit-button"> 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) { |