summaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/images/file-rss.svg1
-rw-r--r--app/assets/images/icons.svg37
-rw-r--r--app/assets/javascripts/application-legacy.js18
-rw-r--r--app/assets/stylesheets/application.css186
-rw-r--r--app/assets/stylesheets/responsive.css31
-rw-r--r--app/assets/stylesheets/rtl.css2
-rw-r--r--app/assets/stylesheets/wiki_syntax.css49
-rw-r--r--app/assets/stylesheets/wiki_syntax_detailed.css58
-rw-r--r--app/controllers/application_controller.rb8
-rw-r--r--app/controllers/messages_controller.rb2
-rw-r--r--app/controllers/news_controller.rb4
-rw-r--r--app/controllers/oauth2_applications_controller.rb38
-rw-r--r--app/controllers/projects_controller.rb2
-rw-r--r--app/controllers/reactions_controller.rb65
-rw-r--r--app/controllers/versions_controller.rb4
-rw-r--r--app/controllers/wiki_controller.rb1
-rw-r--r--app/helpers/application_helper.rb4
-rw-r--r--app/helpers/avatars_helper.rb1
-rw-r--r--app/helpers/icons_helper.rb7
-rw-r--r--app/helpers/issues_helper.rb1
-rw-r--r--app/helpers/journals_helper.rb3
-rw-r--r--app/helpers/messages_helper.rb1
-rw-r--r--app/helpers/news_helper.rb1
-rw-r--r--app/helpers/projects_helper.rb4
-rw-r--r--app/helpers/queries_helper.rb2
-rw-r--r--app/helpers/reactions_helper.rb100
-rw-r--r--app/helpers/settings_helper.rb3
-rw-r--r--app/helpers/watchers_helper.rb4
-rw-r--r--app/javascript/controllers/sticky_issue_header_controller.js22
-rw-r--r--app/models/comment.rb8
-rw-r--r--app/models/issue.rb9
-rw-r--r--app/models/journal.rb1
-rw-r--r--app/models/message.rb2
-rw-r--r--app/models/news.rb2
-rw-r--r--app/models/principal.rb2
-rw-r--r--app/models/reaction.rb60
-rw-r--r--app/models/role.rb26
-rw-r--r--app/models/user.rb42
-rw-r--r--app/models/user_preference.rb2
-rw-r--r--app/models/version.rb2
-rw-r--r--app/views/attachments/_form.html.erb1
-rw-r--r--app/views/doorkeeper/applications/_form.html.erb39
-rw-r--r--app/views/doorkeeper/applications/edit.html.erb6
-rw-r--r--app/views/doorkeeper/applications/index.html.erb33
-rw-r--r--app/views/doorkeeper/applications/new.html.erb6
-rw-r--r--app/views/doorkeeper/applications/show.html.erb54
-rw-r--r--app/views/doorkeeper/authorizations/error.html.erb6
-rw-r--r--app/views/doorkeeper/authorizations/new.html.erb48
-rw-r--r--app/views/doorkeeper/authorizations/show.html.erb8
-rw-r--r--app/views/doorkeeper/authorized_applications/index.html.erb31
-rw-r--r--app/views/help/wiki_syntax/common_mark/en/wiki_syntax_common_mark.html.erb8
-rw-r--r--app/views/help/wiki_syntax/common_mark/en/wiki_syntax_detailed_common_mark.html.erb52
-rw-r--r--app/views/issues/index.html.erb2
-rw-r--r--app/views/issues/show.html.erb16
-rw-r--r--app/views/messages/show.html.erb4
-rw-r--r--app/views/my/account.html.erb1
-rw-r--r--app/views/news/show.html.erb16
-rw-r--r--app/views/projects/index.html.erb2
-rw-r--r--app/views/reactions/_replace_button.js.erb7
-rw-r--r--app/views/reactions/create.js.erb1
-rw-r--r--app/views/reactions/destroy.js.erb1
-rw-r--r--app/views/settings/_display.html.erb22
-rw-r--r--app/views/settings/_general.html.erb2
-rw-r--r--app/views/timelog/index.html.erb2
-rw-r--r--app/views/users/show.api.rsb2
-rw-r--r--app/views/wiki/date_index.html.erb2
-rw-r--r--app/views/wiki/index.html.erb8
67 files changed, 1088 insertions, 107 deletions
diff --git a/app/assets/images/file-rss.svg b/app/assets/images/file-rss.svg
deleted file mode 100644
index 72ab4e29b..000000000
--- a/app/assets/images/file-rss.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="#169"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 2l.117 .007a1 1 0 0 1 .876 .876l.007 .117v4l.005 .15a2 2 0 0 0 1.838 1.844l.157 .006h4l.117 .007a1 1 0 0 1 .876 .876l.007 .117v9a3 3 0 0 1 -2.824 2.995l-.176 .005h-10a3 3 0 0 1 -2.995 -2.824l-.005 -.176v-14a3 3 0 0 1 2.824 -2.995l.176 -.005zm-3 11a1 1 0 0 0 0 2a2 2 0 0 1 1.995 1.85l.005 .15a1 1 0 0 0 2 0a4 4 0 0 0 -4 -4m0 -3a1 1 0 0 0 0 2a5 5 0 0 1 5 5a1 1 0 0 0 2 0a7 7 0 0 0 -7 -7m.01 6h-.01a1 1 0 0 0 -.117 1.993l.127 .007a1 1 0 0 0 0 -2m5.989 -13.001l4.001 4.001h-4z" /></svg> \ No newline at end of file
diff --git a/app/assets/images/icons.svg b/app/assets/images/icons.svg
index 55e3a042d..6283537ce 100644
--- a/app/assets/images/icons.svg
+++ b/app/assets/images/icons.svg
@@ -11,6 +11,11 @@
<path d="M9 12h6"/>
<path d="M12 9v6"/>
</symbol>
+ <symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--alert-circle">
+ <path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0"/>
+ <path d="M12 8v4"/>
+ <path d="M12 16h.01"/>
+ </symbol>
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--angle-down">
<path d="M6 9l6 6l6 -6"/>
</symbol>
@@ -54,6 +59,13 @@
<path d="M12 15v6"/>
<path d="M5 15h3l-3 6h3"/>
</symbol>
+ <symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--apps">
+ <path d="M4 4m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"/>
+ <path d="M4 14m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"/>
+ <path d="M14 14m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"/>
+ <path d="M14 7l6 0"/>
+ <path d="M17 4l0 6"/>
+ </symbol>
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--arrow-right">
<path d="M4 9h8v-3.586a1 1 0 0 1 1.707 -.707l6.586 6.586a1 1 0 0 1 0 1.414l-6.586 6.586a1 1 0 0 1 -1.707 -.707v-3.586h-8a1 1 0 0 1 -1 -1v-4a1 1 0 0 1 1 -1z"/>
</symbol>
@@ -72,6 +84,11 @@
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--bookmarked">
<path d="M18 7v14l-6 -4l-6 4v-14a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4z"/>
</symbol>
+ <symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--bulb">
+ <path d="M3 12h1m8 -9v1m8 8h1m-15.4 -6.4l.7 .7m12.1 -.7l-.7 .7"/>
+ <path d="M9 16a5 5 0 1 1 6 0a3.5 3.5 0 0 0 -1 3a2 2 0 0 1 -4 0a3.5 3.5 0 0 0 -1 -3"/>
+ <path d="M9.7 17l4.6 0"/>
+ </symbol>
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--bullet-end">
<path d="M12 21a9 9 0 1 0 0 -18a9 9 0 0 0 0 18"/>
<path d="M8 12l4 4"/>
@@ -294,6 +311,11 @@
<path d="M8 13h6"/>
<path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12z"/>
</symbol>
+ <symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--message-report">
+ <path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12z"/>
+ <path d="M12 8v3"/>
+ <path d="M12 14v.01"/>
+ </symbol>
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--move">
<path d="M15 14l4 -4l-4 -4"/>
<path d="M19 10h-11a4 4 0 1 0 0 8h1"/>
@@ -336,6 +358,10 @@
<path d="M7 5.03v5.455"/>
<path d="M12 8l5 -3"/>
</symbol>
+ <symbol viewBox="0 0 24 24" id="icon--quote-filled">
+ <path d="M9 5a2 2 0 0 1 2 2v6c0 3.13 -1.65 5.193 -4.757 5.97a1 1 0 1 1 -.486 -1.94c2.227 -.557 3.243 -1.827 3.243 -4.03v-1h-3a2 2 0 0 1 -1.995 -1.85l-.005 -.15v-3a2 2 0 0 1 2 -2z"/>
+ <path d="M18 5a2 2 0 0 1 2 2v6c0 3.13 -1.65 5.193 -4.757 5.97a1 1 0 1 1 -.486 -1.94c2.227 -.557 3.243 -1.827 3.243 -4.03v-1h-3a2 2 0 0 1 -1.995 -1.85l-.005 -.15v-3a2 2 0 0 1 2 -2z"/>
+ </symbol>
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--reload">
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"/>
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"/>
@@ -379,6 +405,10 @@
<path d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z"/>
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0"/>
</symbol>
+ <symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--shield-check">
+ <path d="M11.46 20.846a12 12 0 0 1 -7.96 -14.846a12 12 0 0 0 8.5 -3a12 12 0 0 0 8.5 3a12 12 0 0 1 -.09 7.06"/>
+ <path d="M15 19l2 2l4 -4"/>
+ </symbol>
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--stats">
<path d="M3 13a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v6a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"/>
<path d="M15 9a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v10a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"/>
@@ -459,6 +489,13 @@
<path d="M19 15v6h3"/>
<path d="M11 21v-6l2.5 3l2.5 -3v6"/>
</symbol>
+ <symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--thumb-up">
+ <path d="M7 11v8a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-7a1 1 0 0 1 1 -1h3a4 4 0 0 0 4 -4v-1a2 2 0 0 1 4 0v5h3a2 2 0 0 1 2 2l-1 5a2 3 0 0 1 -2 2h-7a3 3 0 0 1 -3 -3"/>
+ </symbol>
+ <symbol viewBox="0 0 24 24" id="icon--thumb-up-filled">
+ <path d="M13 3a3 3 0 0 1 2.995 2.824l.005 .176v4h2a3 3 0 0 1 2.98 2.65l.015 .174l.005 .176l-.02 .196l-1.006 5.032c-.381 1.626 -1.502 2.796 -2.81 2.78l-.164 -.008h-8a1 1 0 0 1 -.993 -.883l-.007 -.117l.001 -9.536a1 1 0 0 1 .5 -.865a2.998 2.998 0 0 0 1.492 -2.397l.007 -.202v-1a3 3 0 0 1 3 -3z"/>
+ <path d="M5 10a1 1 0 0 1 .993 .883l.007 .117v9a1 1 0 0 1 -.883 .993l-.117 .007h-1a2 2 0 0 1 -1.995 -1.85l-.005 -.15v-7a2 2 0 0 1 1.85 -1.995l.15 -.005h1z"/>
+ </symbol>
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--time">
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0"/>
<path d="M12 7v5l3 3"/>
diff --git a/app/assets/javascripts/application-legacy.js b/app/assets/javascripts/application-legacy.js
index 265ac39c6..f7c1de95c 100644
--- a/app/assets/javascripts/application-legacy.js
+++ b/app/assets/javascripts/application-legacy.js
@@ -426,7 +426,7 @@ function showIssueHistory(journal, url) {
tab_content.find('.journal').show();
tab_content.find('.journal:not(.has-notes)').hide();
tab_content.find('.journal .wiki').show();
- tab_content.find('.journal .contextual .journal-actions').show();
+ tab_content.find('.journal .contextual .journal-actions > *').show();
// always show thumbnails in notes tab
var thumbnails = tab_content.find('.journal .thumbnails');
@@ -439,13 +439,15 @@ function showIssueHistory(journal, url) {
tab_content.find('.journal:not(.has-details)').hide();
tab_content.find('.journal .wiki').hide();
tab_content.find('.journal .thumbnails').hide();
- tab_content.find('.journal .contextual .journal-actions').hide();
+ tab_content.find('.journal .contextual .journal-actions > *').hide();
+ // Show reaction button in properties tab
+ tab_content.find('.journal .contextual .journal-actions .reaction-button-wrapper').show();
break;
default:
tab_content.find('.journal').show();
tab_content.find('.journal .wiki').show();
tab_content.find('.journal .thumbnails').show();
- tab_content.find('.journal .contextual .journal-actions').show();
+ tab_content.find('.journal .contextual .journal-actions > *').show();
}
return false;
@@ -679,7 +681,7 @@ function copyDataClipboardTextToClipboard(target) {
}
function setupCopyButtonsToPreElements() {
- document.querySelectorAll('pre:not(.pre-wrapper pre)').forEach((pre) => {
+ document.querySelectorAll('.wiki pre:not(.pre-wrapper pre)').forEach((pre) => {
// Wrap the <pre> element with a container and add a copy button
const wrapper = document.createElement("div");
wrapper.classList.add("pre-wrapper");
@@ -1222,8 +1224,8 @@ function setupWikiTableSortableHeader() {
});
}
-function setupHoverTooltips() {
- $("[title]:not(.no-tooltip)").tooltip({
+function setupHoverTooltips(container) {
+ $(container || 'body').find("[title]:not(.no-tooltip)").tooltip({
show: {
delay: 400
},
@@ -1233,7 +1235,9 @@ function setupHoverTooltips() {
}
});
}
-
+function removeHoverTooltips(container) {
+ $(container || 'body').find("[title]:not(.no-tooltip)").tooltip('destroy')
+}
$(function() { setupHoverTooltips(); });
function inlineAutoComplete(element) {
diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css
index d6288ad4f..833e998f8 100644
--- a/app/assets/stylesheets/application.css
+++ b/app/assets/stylesheets/application.css
@@ -236,6 +236,7 @@ a.user.user-mention {
#sidebar a.selected {line-height:1.7em; padding:1px 3px 2px 2px; margin-left:-2px; background-color:#9DB9D5; color:#fff; border-radius:2px;}
#sidebar a.selected:hover {text-decoration:none;}
+#sidebar a.selected svg.icon-svg { stroke: #fff !important; }
#sidebar .query.default {font-weight: bold;}
#admin-menu a {line-height:1.7em;}
#admin-menu a.selected:not(:has(svg)) {padding-left: 20px !important; background-position: 2px 40%;}
@@ -316,7 +317,7 @@ div + .drdn-items {border-top:1px solid #ccc;}
width:100%;
height:24px;
display:inline-block;
- padding:3px 18px 3px 6px;
+ padding:1.5px 18px 3px 6px;
border-radius:3px;
border:1px solid #ccc;
margin:0 !important;
@@ -358,11 +359,14 @@ table.list td.buttons a, div.buttons a, table.list td.buttons span.icon-only { m
table.list td.buttons a:last-child, div.buttons a:last-child { margin-right: 0; }
table.list td.buttons img, div.buttons img {vertical-align:middle;}
table.list td.reorder {width:15%; white-space:nowrap; text-align:center; }
-table.list table.progress td {padding-right:0px;}
+table.list table.progress td {padding-right:0; border-top: none;}
table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
table.list tr.overdue td.due_date { color: #c22; }
table.list thead.related-issues th { background-color: inherit; font-size: 11px; border: none; }
#role-permissions-trackers table.list th {white-space:normal;}
+table.list div.wiki p {
+ margin: 0;
+}
.table-list-cell {display: table-cell; vertical-align: top; padding:2px; }
.table-list div.buttons {width: 15%;}
@@ -679,6 +683,36 @@ div.issue .attribute.string_cf .value .wiki p {margin-top: 0; margin-bottom: 0;}
div.issue .attribute.text_cf .value .wiki p:first-of-type {margin-top: 0;}
div.issue.overdue .due-date .value { color: #c22; }
body.controller-issues h2.inline-flex {padding-right: 0}
+div#sticky-issue-header {
+ display: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ background-color: white;
+ border-bottom: 1px solid #d0d7de;
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
+ font-size: 0.8125rem;
+ align-items: center;
+ z-index: 1000;
+ padding: 10px 6px;
+ border-radius: 0px;
+}
+div#sticky-issue-header.is-visible {
+ display: flex;
+}
+div#sticky-issue-header .issue-heading {
+ flex-shrink: 0;
+ white-space: nowrap;
+ margin-right: 6px;
+}
+div#sticky-issue-header .subject {
+ font-weight: bold;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ flex-grow: 1;
+}
#issue_tree table.issues, #relations table.issues {border: 0;}
#issue_tree table.issues td, #relations table.issues td {border: 0;}
@@ -764,6 +798,12 @@ div.journal span.update-info {color: #666; font-size: 0.9em;}
#history div:target h4.note-header {background-color:#DDEEFF;}
#history p.nodata {display: none;}
+/* Prevent content from being hidden behind a #sticky-issue-header when scrolling via anchor links. */
+.controller-issues.action-show div.wiki a[name],
+.controller-issues.action-show #history div[id^="note-"],
+.controller-issues.action-show #history div[id^="change-"] {
+ scroll-margin-top: 50px;
+}
div#activity dl, #search-results { margin-left: 2em; }
div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 22px; font-size: 0.8125rem;}
@@ -885,7 +925,11 @@ ul.projects div.description ul li {list-style-type:initial;}
background-image: none;
padding-left: 0;
}
-#projects-index ul.projects div.root svg {
+#projects-index ul.projects .icon-bookmarked-project svg,
+#projects-index ul.projects .my-project svg {
+ margin-left: 4px;
+}
+#projects-index ul.projects div.root .icon-bookmarked-project svg, #projects-index ul.projects div.root .my-project svg {
stroke-width: 2;
margin-bottom: 10px;
}
@@ -896,7 +940,12 @@ ul.projects div.description ul li {list-style-type:initial;}
background-image: none;
padding-left: 0;
}
-#projects-index a.project ~ svg, table.projects tr.project td.name svg {
+#projects-index div.wiki p {
+ margin-top: 0px;
+}
+
+table.projects td.name .icon-bookmarked-project svg,
+table.projects td.name .my-project svg {
margin-left: 4px;
}
@@ -1060,17 +1109,14 @@ input#months { width: 46px; }
.jstBlock .jstTabs { padding-right: 6px; }
.jstBlock .wiki-preview { padding: 2px; }
-.jstBlock .wiki-preview p:first-child { padding-top: 0 !important; margin-top: 0 !important;}
-.jstBlock .wiki-preview p:last-child { padding-bottom: 0 !important; margin-bottom: 0 !important;}
+.jstBlock .wiki-preview > p:first-child { padding-top: 0 !important; margin-top: 0 !important;}
+.jstBlock .wiki-preview > p:last-child { padding-bottom: 0 !important; margin-bottom: 0 !important;}
.tabular .wiki-preview, .tabular .jstTabs {width: 95%;}
.tabular.settings .wiki-preview, .tabular.settings .jstTabs { width: 99%; }
.tabular.settings .wiki-preview p {padding-left: 0 !important}
.tabular .wiki-preview p {
min-height: initial;
- padding: 0;
- padding-top: 1em !important;
- padding-bottom: 1em !important;
overflow: initial;
}
@@ -1114,10 +1160,26 @@ span.required {color: #bb0000;}
.attachments_fields .icon-attachment, #existing-attachments .icon-attachment {background-image: none; padding-left: 0}
.attachments_fields input.filename, #existing-attachments .filename {border:0; width:250px; color:#555; background-color:inherit; }
.tabular input.filename {max-width:75% !important;}
-.attachments_fields input.filename {height:1.8em;padding-right: 0;}
-.attachments_fields .ajax-waiting input.filename {background:url(/hourglass.png) no-repeat 0px 50%;}
-.attachments_fields .ajax-loading input.filename {background:url(/loading.gif) no-repeat 0px 50%;}
.attachments_fields div.ui-progressbar { width: 100px; height:14px; margin: 2px 0 -5px 8px; display: inline-block; }
+.attachments_fields input.filename {
+ height:1.8em;
+ padding-left: 3px;
+ padding-right: 0;
+}
+.attachments_fields .ajax-waiting {
+ padding-left: 16px;
+ background:url(/hourglass.png) no-repeat 0px 50%;
+}
+.attachments_fields .ajax-waiting .svg-attachment {
+ display: none;
+}
+.attachments_fields .ajax-loading {
+ padding-left: 16px;
+ background: url(/loading.gif) no-repeat 0px 50%;
+}
+.attachments_fields .ajax-loading .svg-attachment {
+ display: none;
+}
a.remove-upload:hover {text-decoration:none !important;}
.existing-attachment.deleted .filename {text-decoration:line-through; color:#999 !important;}
@@ -1136,8 +1198,6 @@ div.thumbnail img {margin: 3px; vertical-align: middle;}
p.other-formats { text-align: right; font-size:0.9em; color: #666; }
.other-formats span + span:before { content: "| "; }
-a.atom { background: url(/file-rss.svg) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
-
em.info {font-style:normal;display:block;font-size:90%;color:#888;}
em.info.error {padding-left:20px; background:url(/exclamation.png) no-repeat 0 50%;}
@@ -1256,11 +1316,45 @@ div.flash.warning svg.icon-svg, .conflict svg.icon-svg {
color: #A6750C;
}
+.warning .oauth-permissions { display:inline-block;text-align:left; }
+.warning .oauth-permissions p { margin-top:0;-webkit-margin-before:0;}
+
#errorExplanation ul { font-size: 0.9em;}
#errorExplanation h2, #errorExplanation p { display: none; }
.conflict-details {font-size:93%;}
+/***** CommonMark Alerts *****/
+.markdown-alert {
+ border-left: 4px solid;
+ padding-left: 0.6em;
+ margin: 1em 0;
+}
+
+.markdown-alert-title {
+ font-weight: bold;
+}
+
+.markdown-alert-tip { border-color: #5db651; }
+.markdown-alert-tip .markdown-alert-title { color: #005f00; }
+.markdown-alert-tip .markdown-alert-title svg {stroke: #005f00; }
+
+.markdown-alert-important { border-color: #800080; }
+.markdown-alert-important .markdown-alert-title { color: #4b006e; }
+.markdown-alert-important .markdown-alert-title svg { stroke: #4b006e; }
+
+.markdown-alert-caution { border-color: #c22; }
+.markdown-alert-caution .markdown-alert-title { color: #880000; }
+.markdown-alert-caution .markdown-alert-title svg { stroke: #880000; }
+
+.markdown-alert-warning { border-color: #e4bc4b; }
+.markdown-alert-warning .markdown-alert-title { color: #a7760c; }
+.markdown-alert-warning .markdown-alert-title svg { stroke: #a7760c; }
+
+.markdown-alert-note { border-color: #169; }
+.markdown-alert-note .markdown-alert-title { color: #1e40af; }
+.markdown-alert-note .markdown-alert-title svg { stroke: #1e40af; }
+
/***** Ajax indicator ******/
#ajax-indicator {
position: absolute; /* fixed not supported by IE */
@@ -1535,7 +1629,12 @@ div.wiki .external {
div.wiki a {word-wrap: break-word;}
div.wiki a.new {color: #b73535;}
-div.wiki p {line-height: 1.6;}
+div.wiki p {
+ line-height: 1.6;
+ margin-top: 1em;
+ margin-bottom: 1em;
+ padding: 0;
+}
div.wiki ul, div.wiki ol {margin-bottom:1em;}
div.wiki li {line-height: 1.6; margin-bottom: 0.125rem;}
div.wiki li>ul, div.wiki li>ol {margin-bottom: 0;}
@@ -1570,7 +1669,7 @@ div.pre-wrapper a.copy-pre-content-link {
border-radius: 3px;
background: #fff;
border: 1px solid #ccc;
- padding: 2px;
+ padding: 0px 3px 3px 3px;
}
div.pre-wrapper:hover a.copy-pre-content-link {
@@ -1610,10 +1709,11 @@ a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor, h4:hover a.wiki-anchor, h5:hover a.wiki-anchor, h6:hover a.wiki-anchor { display: inline; color: #ddd; }
div.wiki img {vertical-align:middle; max-width:100%;}
-div.wiki>.task-list {
- padding-left: 0px;
+
+div.wiki>.contains-task-list {
+ padding-left: 0;
}
-div.wiki .task-list {
+div.wiki .contains-task-list {
list-style-type: none;
}
div.wiki .task-list input.task-list-item-checkbox {
@@ -1819,10 +1919,15 @@ td.gantt_selected_column .gantt_hdr,.gantt_selected_column_container {
flex-shrink: 0;
}
-a.icon:hover svg, a.icon-only:hover svg {
+a.icon:hover .icon-svg, a.icon-only:hover .icon-svg {
stroke: #c61a1a;
}
+a.icon:hover .icon-svg-filled, a.icon-only:hover .icon-svg-filled {
+ stroke: none;
+ fill: #c61a1a;
+}
+
svg.icon-ok {
stroke: #5db651;
}
@@ -1846,6 +1951,11 @@ svg.icon-svg {
vertical-align: middle;
}
+svg.icon-svg-filled {
+ fill: #169;
+ stroke: none;
+}
+
svg.s20 {
width: 1.25rem;
height: 1.25rem;
@@ -2052,6 +2162,42 @@ color: #555; text-shadow: 1px 1px 0 #fff;
img.filecontent.image {background-image: url(/transparent.png);}
+/* Reaction styles */
+.reaction-button:hover, .reaction-button:active {
+ text-decoration: none;
+}
+.reaction-button .icon-label {
+ margin-left: 3px;
+ margin-bottom: -1px;
+ font-weight: bold;
+}
+.reaction-button.readonly {
+ cursor: default;
+}
+.reaction-button.readonly .icon-svg {
+ stroke: #999;
+}
+.reaction-button.readonly .icon-label {
+ color: #999;
+}
+div.issue.details .reaction {
+ float: right;
+ font-size: 0.9em;
+ margin-top: 0.5em;
+ margin-left: 10px;
+ clear: right;
+}
+div.message .reaction {
+ float: right;
+ font-size: 0.9em;
+ margin-left: 10px;
+}
+div.news .reaction {
+ float: right;
+ font-size: 0.9em;
+ margin-left: 10px;
+}
+
/* Custom JQuery styles */
.ui-autocomplete, .ui-menu {
border-radius: 2px;
diff --git a/app/assets/stylesheets/responsive.css b/app/assets/stylesheets/responsive.css
index c5278c87f..b3e8bddd8 100644
--- a/app/assets/stylesheets/responsive.css
+++ b/app/assets/stylesheets/responsive.css
@@ -385,7 +385,7 @@
list-style: none;
}
- .flyout-menu #watchers {
+ .flyout-menu #watchers, .flyout-menu .queries {
display: -webkit-flex;
display: -webkit-box;
display: flex;
@@ -402,11 +402,11 @@
order: 3;
}
- .flyout-menu #watchers h3 {
+ #sidebar-wrapper {
margin-left: -8px;
}
- .flyout-menu #watchers ul li {
+ .flyout-menu #watchers ul li, .flyout-menu ul.queries li {
display: -webkit-flex;
display: -webkit-box;
display: flex;
@@ -418,6 +418,16 @@
-webkit-align-items: center;
-webkit-box-align: center;
align-items: center;
+ border-top: 1px solid rgba(255,255,255,.1);
+ }
+
+ .flyout-menu #watchers ul li a, .flyout-menu ul.queries li a {
+ border-top: none;
+ }
+
+ .flyout-menu ul.queries li a.icon-clear-query {
+ flex-shrink: 0;
+ padding-right: 8px;
}
.flyout-menu ul li a {
@@ -440,7 +450,7 @@
color: white;
}
- .flyout-menu .icon svg {
+ .flyout-menu .icon svg, .flyout-menu .icon-only svg {
stroke: white;
}
@@ -848,6 +858,19 @@
font-size: 1.1em;
text-align: left;
}
+
+ /* Sticky issue header */
+ /* When project-jump.drdn is visible in mobile layout, offset the sticky header by its height to prevent it from being hidden. */
+ div#sticky-issue-header {
+ top: 64px;
+ }
+
+ /* Prevent content from being hidden behind #sticky-issue-header and project-jump when scrolling via anchor links. */
+ .controller-issues.action-show div.wiki a[name],
+ .controller-issues.action-show #history div[id^="note-"],
+ .controller-issues.action-show #history div[id^="change-"] {
+ scroll-margin-top: 114px;
+ }
}
@media all and (max-width: 599px) {
diff --git a/app/assets/stylesheets/rtl.css b/app/assets/stylesheets/rtl.css
index be609dfab..e7daf7ae8 100644
--- a/app/assets/stylesheets/rtl.css
+++ b/app/assets/stylesheets/rtl.css
@@ -238,7 +238,7 @@ a.remove-upload {background: url(/delete.png) no-repeat right 1px top 50%; paddi
div.thumbnails div {margin-right:0px; margin-left:2px;}
-p.other-formats { text-align:left; }
+p.other-formats, p.query-totals { text-align:left; }
a.atom { background: url(/feed.png) no-repeat right 1px top 50%; padding: 2px 16px 3px 0; }
diff --git a/app/assets/stylesheets/wiki_syntax.css b/app/assets/stylesheets/wiki_syntax.css
index d326a3293..89b117419 100644
--- a/app/assets/stylesheets/wiki_syntax.css
+++ b/app/assets/stylesheets/wiki_syntax.css
@@ -1,19 +1,33 @@
@font-face {
- font-family: "Noto Sans";
- src: url("/NotoSans-VariableFont_wdth,wght.woff2") format("woff2");
- font-weight: 100 900;
- font-stretch: 75% 125%;
- font-style: normal;
- font-display: swap;
+ font-family: "Noto Sans";
+ src: url("/NotoSans-Regular.woff2") format("woff2");
+ font-weight: 400;
+ font-style: normal;
+ font-display: swap;
}
@font-face {
- font-family: "Noto Sans";
- src: url("/NotoSans-Italic-VariableFont_wdth,wght.woff2") format("woff2");
- font-weight: 100 900;
- font-stretch: 75% 125%;
- font-style: italic;
- font-display: swap;
+ font-family: "Noto Sans";
+ src: url("/NotoSans-Bold.woff2") format("woff2");
+ font-weight: 700;
+ font-style: normal;
+ font-display: swap;
+}
+
+@font-face {
+ font-family: "Noto Sans";
+ src: url("/NotoSans-Italic.woff2") format("woff2");
+ font-weight: 400;
+ font-style: italic;
+ font-display: swap;
+}
+
+@font-face {
+ font-family: "Noto Sans";
+ src: url("/NotoSans-BoldItalic.woff2") format("woff2");
+ font-weight: 700;
+ font-style: italic;
+ font-display: swap;
}
:root {
@@ -58,3 +72,14 @@ a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
.syntaxhl .s1 { background-color: #fff0f0 }
span.more_info { font-weight: normal; }
+
+.markdown-alert {
+ border-left: 4px solid;
+ padding-left: 10px;
+ margin-left: 10px;
+}
+.markdown-alert-title {
+ font-weight: bold;
+}
+.markdown-alert-note { border-color: #169; }
+.markdown-alert-note .markdown-alert-title { color: #1e40af; } \ No newline at end of file
diff --git a/app/assets/stylesheets/wiki_syntax_detailed.css b/app/assets/stylesheets/wiki_syntax_detailed.css
index 7d7c30f53..ad3c8c65f 100644
--- a/app/assets/stylesheets/wiki_syntax_detailed.css
+++ b/app/assets/stylesheets/wiki_syntax_detailed.css
@@ -1,19 +1,33 @@
@font-face {
- font-family: "Noto Sans";
- src: url("/NotoSans-VariableFont_wdth,wght.woff2") format("woff2");
- font-weight: 100 900;
- font-stretch: 75% 125%;
- font-style: normal;
- font-display: swap;
+ font-family: "Noto Sans";
+ src: url("/NotoSans-Regular.woff2") format("woff2");
+ font-weight: 400;
+ font-style: normal;
+ font-display: swap;
}
@font-face {
- font-family: "Noto Sans";
- src: url("/NotoSans-Italic-VariableFont_wdth,wght.woff2") format("woff2");
- font-weight: 100 900;
- font-stretch: 75% 125%;
- font-style: italic;
- font-display: swap;
+ font-family: "Noto Sans";
+ src: url("/NotoSans-Bold.woff2") format("woff2");
+ font-weight: 700;
+ font-style: normal;
+ font-display: swap;
+}
+
+@font-face {
+ font-family: "Noto Sans";
+ src: url("/NotoSans-Italic.woff2") format("woff2");
+ font-weight: 400;
+ font-style: italic;
+ font-display: swap;
+}
+
+@font-face {
+ font-family: "Noto Sans";
+ src: url("/NotoSans-BoldItalic.woff2") format("woff2");
+ font-weight: 700;
+ font-style: italic;
+ font-display: swap;
}
:root {
@@ -49,3 +63,23 @@ table.list td { background-color: #f5f5f5; vertical-align: middle; padding: 0.3e
.syntaxhl .o { color: #333333 }
.syntaxhl .s2 { background-color: #fff0f0 }
.syntaxhl .si { background-color: #eeeeee }
+
+
+.markdown-alert {
+ border-left: 4px solid;
+ padding-left: 10px;
+ margin-left: 20px;
+}
+.markdown-alert-title {
+ font-weight: bold;
+}
+.markdown-alert-tip { border-color: #5db651; }
+.markdown-alert-tip .markdown-alert-title { color: #005f00; }
+.markdown-alert-important { border-color: #800080; }
+.markdown-alert-important .markdown-alert-title { color: #4b006e; }
+.markdown-alert-caution { border-color: #c22; }
+.markdown-alert-caution .markdown-alert-title { color: #880000; }
+.markdown-alert-warning { border-color: #e4bc4b; }
+.markdown-alert-warning .markdown-alert-title { color: #a7760c; }
+.markdown-alert-note { border-color: #169; }
+.markdown-alert-note .markdown-alert-title { color: #1e40af; } \ No newline at end of file
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 074392709..a01d5c75f 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -131,6 +131,14 @@ class ApplicationController < ActionController::Base
if (key = api_key_from_request)
# Use API key
user = User.find_by_api_key(key)
+ elsif access_token = Doorkeeper.authenticate(request)
+ # Oauth
+ if access_token.accessible?
+ user = User.active.find_by_id(access_token.resource_owner_id)
+ user.oauth_scope = access_token.scopes.all.map(&:to_sym)
+ else
+ doorkeeper_render_error
+ end
elsif /\ABasic /i.match?(request.authorization.to_s)
# HTTP Basic, either username/password or API key/random
authenticate_with_http_basic do |username, password|
diff --git a/app/controllers/messages_controller.rb b/app/controllers/messages_controller.rb
index 22daf9f90..8b26bee73 100644
--- a/app/controllers/messages_controller.rb
+++ b/app/controllers/messages_controller.rb
@@ -51,6 +51,8 @@ class MessagesController < ApplicationController
offset(@reply_pages.offset).
to_a
+ Message.preload_reaction_details(@replies)
+
@reply = Message.new(:subject => "RE: #{@message.subject}")
render :action => "show", :layout => false if request.xhr?
end
diff --git a/app/controllers/news_controller.rb b/app/controllers/news_controller.rb
index 06240e359..dd6bade24 100644
--- a/app/controllers/news_controller.rb
+++ b/app/controllers/news_controller.rb
@@ -67,8 +67,10 @@ class NewsController < ApplicationController
end
def show
- @comments = @news.comments.to_a
+ @comments = @news.comments.preload(:commented).to_a
@comments.reverse! if User.current.wants_comments_in_reverse_order?
+
+ Comment.preload_reaction_details(@comments)
end
def new
diff --git a/app/controllers/oauth2_applications_controller.rb b/app/controllers/oauth2_applications_controller.rb
new file mode 100644
index 000000000..107af2ec0
--- /dev/null
+++ b/app/controllers/oauth2_applications_controller.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+#
+# Redmine - project management software
+# Copyright (C) 2006- Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+class Oauth2ApplicationsController < Doorkeeper::ApplicationsController
+ private
+
+ def application_params
+ params[:doorkeeper_application] ||= {}
+ params[:doorkeeper_application][:scopes] ||= []
+
+ scopes = Redmine::AccessControl.public_permissions.map{|p| p.name.to_s}
+
+ if params[:doorkeeper_application][:scopes].is_a?(Array)
+ scopes |= params[:doorkeeper_application][:scopes]
+ else
+ scopes |= params[:doorkeeper_application][:scopes].split(/\s+/)
+ end
+ params[:doorkeeper_application][:scopes] = scopes.join(' ')
+ super
+ end
+end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index f9a390c58..2a42c99ed 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -176,7 +176,7 @@ class ProjectsController < ApplicationController
respond_to do |format|
format.html do
@principals_by_role = @project.principals_by_role
- @subprojects = @project.children.visible.to_a
+ @subprojects = @project.leaf? ? [] : @project.children.visible.to_a
@news = @project.news.limit(5).includes(:author, :project).reorder("#{News.table_name}.created_on DESC").to_a
with_subprojects = Setting.display_subprojects_issues?
@trackers = @project.rolled_up_trackers(with_subprojects).visible
diff --git a/app/controllers/reactions_controller.rb b/app/controllers/reactions_controller.rb
new file mode 100644
index 000000000..71b37e5f8
--- /dev/null
+++ b/app/controllers/reactions_controller.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+# Redmine - project management software
+# Copyright (C) 2006- Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class ReactionsController < ApplicationController
+ before_action :require_login
+
+ before_action :check_enabled
+ before_action :set_object, :authorize_reactable
+
+ def create
+ respond_to do |format|
+ format.js do
+ @object.reactions.find_or_create_by!(user: User.current)
+ end
+ format.any { head :not_found }
+ end
+ end
+
+ def destroy
+ respond_to do |format|
+ format.js do
+ reaction = @object.reactions.by(User.current).find_by(id: params[:id])
+ reaction&.destroy
+ end
+ format.any { head :not_found }
+ end
+ end
+
+ private
+
+ def check_enabled
+ render_403 unless Setting.reactions_enabled?
+ end
+
+ def set_object
+ object_type = params[:object_type]
+
+ unless Redmine::Reaction::REACTABLE_TYPES.include?(object_type)
+ render_403
+ return
+ end
+
+ @object = object_type.constantize.find(params[:object_id])
+ end
+
+ def authorize_reactable
+ render_403 unless Redmine::Reaction.editable?(@object, User.current)
+ end
+end
diff --git a/app/controllers/versions_controller.rb b/app/controllers/versions_controller.rb
index d52b43ba3..328d3e56e 100644
--- a/app/controllers/versions_controller.rb
+++ b/app/controllers/versions_controller.rb
@@ -51,7 +51,7 @@ class VersionsController < ApplicationController
if @selected_tracker_ids.any? && @versions.any?
issues = Issue.visible.
includes(:project, :tracker).
- preload(:status, :priority, :fixed_version).
+ preload(:status, :priority, :fixed_version, {:assigned_to => :email_address}).
where(:tracker_id => @selected_tracker_ids, :project_id => project_ids, :fixed_version_id => @versions.map(&:id)).
order("#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id")
@issues_by_version = issues.group_by(&:fixed_version)
@@ -69,7 +69,7 @@ class VersionsController < ApplicationController
format.html do
@issues = @version.fixed_issues.visible.
includes(:status, :tracker, :priority).
- preload(:project).
+ preload(:project, {:assigned_to => :email_address}).
reorder("#{Tracker.table_name}.position, #{Issue.table_name}.id").
to_a
end
diff --git a/app/controllers/wiki_controller.rb b/app/controllers/wiki_controller.rb
index 36b90da77..bcb3b0891 100644
--- a/app/controllers/wiki_controller.rb
+++ b/app/controllers/wiki_controller.rb
@@ -240,6 +240,7 @@ class WikiController < ApplicationController
# don't load text
@versions = @page.content.versions.
select("id, author_id, comments, updated_on, version").
+ preload(:author).
reorder('version DESC').
limit(@version_pages.per_page + 1).
offset(@version_pages.offset).
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 847fb9fdd..285528422 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -518,6 +518,8 @@ module ApplicationHelper
def render_flash_messages
s = +''
flash.each do |k, v|
+ next unless v.is_a?(String)
+
s << content_tag('div', notice_icon(k) + v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
end
s.html_safe
@@ -1386,7 +1388,7 @@ module ApplicationHelper
<|
$)
}x
- HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
+ HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/im unless const_defined?(:HEADING_RE)
def parse_sections(text, project, obj, attr, only_path, options)
return unless options[:edit_section_links]
diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb
index b39427bda..88a571b62 100644
--- a/app/helpers/avatars_helper.rb
+++ b/app/helpers/avatars_helper.rb
@@ -44,6 +44,7 @@ module AvatarsHelper
if user.respond_to?(:mail)
email = user.mail
options[:title] = user.name unless options[:title]
+ options[:initials] = user.initials if options[:default] == "initials"
elsif user.to_s =~ %r{<(.+?)>}
email = $1
end
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index f96315c75..6afb84537 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -21,10 +21,10 @@ module IconsHelper
DEFAULT_ICON_SIZE = "18"
DEFAULT_SPRITE = "icons"
- def sprite_icon(icon_name, label = nil, icon_only: false, size: DEFAULT_ICON_SIZE, css_class: nil, sprite: DEFAULT_SPRITE, plugin: nil, rtl: false)
+ def sprite_icon(icon_name, label = nil, icon_only: false, size: DEFAULT_ICON_SIZE, style: :outline, css_class: nil, sprite: DEFAULT_SPRITE, plugin: nil, rtl: false)
sprite = plugin ? "plugin_assets/#{plugin}/#{sprite}.svg" : "#{sprite}.svg"
- svg_icon = svg_sprite_icon(icon_name, size: size, css_class: css_class, sprite: sprite, rtl: rtl)
+ svg_icon = svg_sprite_icon(icon_name, size: size, style: style, css_class: css_class, sprite: sprite, rtl: rtl)
if label
label_classes = ["icon-label"]
@@ -92,8 +92,9 @@ module IconsHelper
private
- def svg_sprite_icon(icon_name, size: DEFAULT_ICON_SIZE, sprite: DEFAULT_SPRITE, css_class: nil, rtl: false)
+ def svg_sprite_icon(icon_name, size: DEFAULT_ICON_SIZE, style: :outline, sprite: DEFAULT_SPRITE, css_class: nil, rtl: false)
css_classes = "s#{size} icon-svg"
+ css_classes += " icon-svg-filled" if style == :filled
css_classes += " #{css_class}" unless css_class.nil?
css_classes += " icon-rtl" if rtl
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 6586a1b7e..ce3607a5d 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -22,6 +22,7 @@ module IssuesHelper
include Redmine::Export::PDF::IssuesPdfHelper
include IssueStatusesHelper
include QueriesHelper
+ include ReactionsHelper
def issue_list(issues, &)
ancestors = []
diff --git a/app/helpers/journals_helper.rb b/app/helpers/journals_helper.rb
index 6c22fc4ca..7f1a449fb 100644
--- a/app/helpers/journals_helper.rb
+++ b/app/helpers/journals_helper.rb
@@ -19,6 +19,7 @@
module JournalsHelper
include Redmine::QuoteReply::Helper
+ include ReactionsHelper
# Returns the attachments of a journal that are displayed as thumbnails
def journal_thumbnail_attachments(journal)
@@ -40,6 +41,8 @@ module JournalsHelper
)
end
+ links << reaction_button(journal)
+
if journal.notes.present?
if options[:reply_links]
url = quoted_issue_path(issue, :journal_id => journal, :journal_indice => indice)
diff --git a/app/helpers/messages_helper.rb b/app/helpers/messages_helper.rb
index fd9ba3bcb..92f788d0c 100644
--- a/app/helpers/messages_helper.rb
+++ b/app/helpers/messages_helper.rb
@@ -19,4 +19,5 @@
module MessagesHelper
include Redmine::QuoteReply::Helper
+ include ReactionsHelper
end
diff --git a/app/helpers/news_helper.rb b/app/helpers/news_helper.rb
index a5c50fdfd..cd7b6734a 100644
--- a/app/helpers/news_helper.rb
+++ b/app/helpers/news_helper.rb
@@ -18,4 +18,5 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module NewsHelper
+ include ReactionsHelper
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 01a5452f7..bae1c4e3a 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -80,8 +80,8 @@ module ProjectsHelper
classes += %w(icon icon-bookmarked-project) if bookmarked_project_ids.include?(project.id)
s = link_to_project(project, {}, :class => classes.uniq.join(' '))
- s << sprite_icon('user', l(:label_my_projects), icon_only: true) if User.current.member_of?(project)
- s << sprite_icon('bookmarked', l(:label_my_bookmarks), icon_only: true) if bookmarked_project_ids.include?(project.id)
+ s << tag.span(sprite_icon('user', l(:label_my_projects), icon_only: true), class: 'icon-only icon-user my-project') if User.current.member_of?(project)
+ s << tag.span(sprite_icon('bookmarked', l(:label_my_bookmarks), icon_only: true), class: 'icon-only icon-bookmarked-project') if bookmarked_project_ids.include?(project.id)
if project.description.present?
s << content_tag('div', textilizable(project.short_description, :project => project), :class => 'wiki description')
end
diff --git a/app/helpers/queries_helper.rb b/app/helpers/queries_helper.rb
index ca7168f27..3aef7083a 100644
--- a/app/helpers/queries_helper.rb
+++ b/app/helpers/queries_helper.rb
@@ -169,7 +169,7 @@ module QueriesHelper
group_name = format_object(group)
end
group_name ||= ""
- group_count = result_count_by_group ? result_count_by_group[group] : nil
+ group_count = result_count_by_group&.[](group)
group_totals = totals_by_group.map {|column, t| total_tag(column, t[group] || 0)}.join(" ").html_safe
end
end
diff --git a/app/helpers/reactions_helper.rb b/app/helpers/reactions_helper.rb
new file mode 100644
index 000000000..e02e1c9f9
--- /dev/null
+++ b/app/helpers/reactions_helper.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+# Redmine - project management software
+# Copyright (C) 2006- Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module ReactionsHelper
+ # Maximum number of users to display in the reaction button tooltip
+ DISPLAY_REACTION_USERS_LIMIT = 10
+
+ def reaction_button(object)
+ return unless Redmine::Reaction.visible?(object, User.current)
+
+ detail = object.reaction_detail
+
+ user_reaction = detail.user_reaction
+ count = detail.reaction_count
+ visible_user_names = detail.visible_users.take(DISPLAY_REACTION_USERS_LIMIT).map(&:name)
+
+ tooltip = build_reaction_tooltip(visible_user_names, count)
+
+ if Redmine::Reaction.editable?(object, User.current)
+ if user_reaction.present?
+ reaction_button_reacted(object, user_reaction, count, tooltip)
+ else
+ reaction_button_not_reacted(object, count, tooltip)
+ end
+ else
+ reaction_button_readonly(object, count, tooltip)
+ end
+ end
+
+ def reaction_id_for(object)
+ dom_id(object, :reaction)
+ end
+
+ private
+
+ def reaction_button_reacted(object, reaction, count, tooltip)
+ reaction_button_wrapper object do
+ link_to(
+ sprite_icon('thumb-up-filled', count.nonzero?, style: :filled),
+ reaction_path(reaction, object_type: object.class.name, object_id: object),
+ remote: true, method: :delete,
+ class: ['icon', 'reaction-button', 'reacted'],
+ title: tooltip
+ )
+ end
+ end
+
+ def reaction_button_not_reacted(object, count, tooltip)
+ reaction_button_wrapper object do
+ link_to(
+ sprite_icon('thumb-up', count.nonzero?),
+ reactions_path(object_type: object.class.name, object_id: object),
+ remote: true, method: :post,
+ class: 'icon reaction-button',
+ title: tooltip
+ )
+ end
+ end
+
+ def reaction_button_readonly(object, count, tooltip)
+ reaction_button_wrapper object do
+ tag.span(class: 'icon reaction-button readonly', title: tooltip) do
+ sprite_icon('thumb-up', count.nonzero?)
+ end
+ end
+ end
+
+ def reaction_button_wrapper(object, &)
+ tag.span(class: 'reaction-button-wrapper', data: { 'reaction-button-id': reaction_id_for(object) }, &)
+ end
+
+ def build_reaction_tooltip(visible_user_names, count)
+ return if count.zero?
+
+ display_user_names = visible_user_names.dup
+ others = count - visible_user_names.size
+
+ if others.positive?
+ display_user_names << I18n.t(:reaction_text_x_other_users, count: others)
+ end
+
+ display_user_names.to_sentence(locale: I18n.locale)
+ end
+end
diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb
index 39a836a03..c1f989805 100644
--- a/app/helpers/settings_helper.rb
+++ b/app/helpers/settings_helper.rb
@@ -244,6 +244,7 @@ module SettingsHelper
['Mystery man', 'mm'],
['Retro', 'retro'],
['Robohash', 'robohash'],
- ['Wavatars', 'wavatar']]
+ ['Wavatars', 'wavatar'],
+ ['Initials', 'initials']]
end
end
diff --git a/app/helpers/watchers_helper.rb b/app/helpers/watchers_helper.rb
index 6e6366846..bfed8adf2 100644
--- a/app/helpers/watchers_helper.rb
+++ b/app/helpers/watchers_helper.rb
@@ -48,7 +48,9 @@ module WatchersHelper
def watchers_list(object)
remove_allowed = User.current.allowed_to?(:"delete_#{object.class.name.underscore}_watchers", object.project)
content = ''.html_safe
- lis = object.watcher_users.sorted.collect do |user|
+ scope = object.watcher_users
+ scope = scope.includes(:email_address) if Setting.gravatar_enabled?
+ lis = scope.sorted.collect do |user|
s = ''.html_safe
s << avatar(user, :size => "16").to_s if user.is_a?(User)
s << link_to_principal(user, class: user.class.to_s.downcase)
diff --git a/app/javascript/controllers/sticky_issue_header_controller.js b/app/javascript/controllers/sticky_issue_header_controller.js
new file mode 100644
index 000000000..aebc7d2dc
--- /dev/null
+++ b/app/javascript/controllers/sticky_issue_header_controller.js
@@ -0,0 +1,22 @@
+import { Controller } from "@hotwired/stimulus";
+
+export default class extends Controller {
+ static targets = ["original", "stickyHeader"];
+
+ connect() {
+ if (!this.originalTarget || !this.stickyHeaderTarget) return;
+
+ this.observer = new IntersectionObserver(
+ ([entry]) => {
+ this.stickyHeaderTarget.classList.toggle("is-visible", !entry.isIntersecting);
+ },
+ { threshold: 0 }
+ );
+
+ this.observer.observe(this.originalTarget);
+ }
+
+ disconnect() {
+ this.observer?.disconnect();
+ }
+}
diff --git a/app/models/comment.rb b/app/models/comment.rb
index 79eb59748..1716537af 100644
--- a/app/models/comment.rb
+++ b/app/models/comment.rb
@@ -19,6 +19,8 @@
class Comment < ApplicationRecord
include Redmine::SafeAttributes
+ include Redmine::Reaction::Reactable
+
belongs_to :commented, :polymorphic => true, :counter_cache => true
belongs_to :author, :class_name => 'User'
@@ -28,6 +30,8 @@ class Comment < ApplicationRecord
safe_attributes 'comments'
+ delegate :visible?, to: :commented
+
def comments=(arg)
self.content = arg
end
@@ -36,6 +40,10 @@ class Comment < ApplicationRecord
content
end
+ def project
+ commented.respond_to?(:project) ? commented.project : nil
+ end
+
private
def send_notification
diff --git a/app/models/issue.rb b/app/models/issue.rb
index ac3b40bf1..576840843 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -25,6 +25,7 @@ class Issue < ApplicationRecord
before_validation :clear_disabled_fields
before_save :set_parent_id
include Redmine::NestedSet::IssueNestedSet
+ include Redmine::Reaction::Reactable
belongs_to :project
belongs_to :tracker
@@ -916,7 +917,8 @@ class Issue < ApplicationRecord
result = journals.
preload(:details).
preload(:user => :email_address).
- reorder(:created_on, :id).to_a
+ reorder(:created_on, :id).
+ to_a
result.each_with_index {|j, i| j.indice = i + 1}
@@ -927,6 +929,9 @@ class Issue < ApplicationRecord
end
Journal.preload_journals_details_custom_fields(result)
result.select! {|journal| journal.notes? || journal.visible_details.any?}
+
+ Journal.preload_reaction_details(result)
+
result
end
@@ -1170,7 +1175,7 @@ class Issue < ApplicationRecord
if leaf?
spent_hours
else
- self_and_descendants.joins(:time_entries).sum("#{TimeEntry.table_name}.hours").to_f || 0.0
+ self_and_descendants.joins(:time_entries).sum("#{TimeEntry.table_name}.hours").to_f
end
end
diff --git a/app/models/journal.rb b/app/models/journal.rb
index 039b182e2..12f2beec8 100644
--- a/app/models/journal.rb
+++ b/app/models/journal.rb
@@ -19,6 +19,7 @@
class Journal < ApplicationRecord
include Redmine::SafeAttributes
+ include Redmine::Reaction::Reactable
belongs_to :journalized, :polymorphic => true
# added as a quick fix to allow eager loading of the polymorphic association
diff --git a/app/models/message.rb b/app/models/message.rb
index c7f78d2d9..9ac88c7d1 100644
--- a/app/models/message.rb
+++ b/app/models/message.rb
@@ -19,6 +19,8 @@
class Message < ApplicationRecord
include Redmine::SafeAttributes
+ include Redmine::Reaction::Reactable
+
belongs_to :board
belongs_to :author, :class_name => 'User'
acts_as_tree :counter_cache => :replies_count, :order => "#{Message.table_name}.created_on ASC"
diff --git a/app/models/news.rb b/app/models/news.rb
index 40cd63db9..174e4c5ac 100644
--- a/app/models/news.rb
+++ b/app/models/news.rb
@@ -19,6 +19,8 @@
class News < ApplicationRecord
include Redmine::SafeAttributes
+ include Redmine::Reaction::Reactable
+
belongs_to :project
belongs_to :author, :class_name => 'User'
has_many :comments, lambda {order("created_on")}, :as => :commented, :dependent => :delete_all
diff --git a/app/models/principal.rb b/app/models/principal.rb
index 77f599c73..86c4edb84 100644
--- a/app/models/principal.rb
+++ b/app/models/principal.rb
@@ -35,6 +35,8 @@ class Principal < ApplicationRecord
:foreign_key => 'user_id'
has_many :projects, :through => :memberships
has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
+ # Always returns nil for groups
+ has_one :email_address, lambda {where :is_default => true}, :autosave => true, :foreign_key => 'user_id'
validate :validate_status
diff --git a/app/models/reaction.rb b/app/models/reaction.rb
new file mode 100644
index 000000000..184ed2d6e
--- /dev/null
+++ b/app/models/reaction.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+# Redmine - project management software
+# Copyright (C) 2006- Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class Reaction < ApplicationRecord
+ belongs_to :reactable, polymorphic: true
+ belongs_to :user
+
+ validates :reactable_type, inclusion: { in: Redmine::Reaction::REACTABLE_TYPES }
+
+ scope :by, ->(user) { where(user: user) }
+ scope :for_reactable, ->(reactable) { where(reactable: reactable) }
+ scope :visible, ->(user) { where(user: User.visible(user)) }
+
+ # Represents reaction details for a reactable object
+ Detail = Struct.new(
+ # Users who reacted and are visible to the target user
+ :visible_users,
+ # Reaction of the target user
+ :user_reaction
+ ) do
+ def initialize(visible_users: [], user_reaction: nil)
+ super
+ end
+
+ def reaction_count = visible_users.size
+ end
+
+ def self.build_detail_map_for(reactables, user)
+ reactions = visible(user)
+ .for_reactable(reactables)
+ .preload(:user)
+ .select(:id, :reactable_id, :user_id)
+ .order(id: :desc)
+
+ reactions.each_with_object({}) do |reaction, m|
+ m[reaction.reactable_id] ||= Detail.new
+
+ m[reaction.reactable_id].then do |detail|
+ detail.visible_users << reaction.user
+ detail.user_reaction = reaction if reaction.user == user
+ end
+ end
+ end
+end
diff --git a/app/models/role.rb b/app/models/role.rb
index 3ca4f92a1..870bbe945 100644
--- a/app/models/role.rb
+++ b/app/models/role.rb
@@ -198,11 +198,14 @@ class Role < ApplicationRecord
# action can be:
# * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
# * a permission Symbol (eg. :edit_project)
- def allowed_to?(action)
+ # scope can be:
+ # * an array of permissions which will be used as filter (logical AND)
+
+ def allowed_to?(action, scope=nil)
if action.is_a? Hash
- allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
+ allowed_actions(scope).include? "#{action[:controller]}/#{action[:action]}"
else
- allowed_permissions.include? action
+ allowed_permissions(scope).include? action
end
end
@@ -298,13 +301,20 @@ class Role < ApplicationRecord
private
- def allowed_permissions
- @allowed_permissions ||= permissions + Redmine::AccessControl.public_permissions.collect {|p| p.name}
+ def allowed_permissions(scope = nil)
+ scope = scope.sort if scope.present? # to maintain stable cache keys
+ @allowed_permissions ||= {}
+ @allowed_permissions[scope] ||= begin
+ unscoped = permissions + Redmine::AccessControl.public_permissions.collect {|p| p.name}
+ scope.present? ? unscoped & scope : unscoped
+ end
end
- def allowed_actions
- @actions_allowed ||=
- allowed_permissions.inject([]) do |actions, permission|
+ def allowed_actions(scope = nil)
+ scope = scope.sort if scope.present? # to maintain stable cache keys
+ @actions_allowed ||= {}
+ @actions_allowed[scope] ||=
+ allowed_permissions(scope).inject([]) do |actions, permission|
actions += Redmine::AccessControl.allowed_actions(permission)
end.flatten
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 1839613c7..496084ceb 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -28,46 +28,55 @@ class User < Principal
USER_FORMATS = {
:firstname_lastname => {
:string => '#{firstname} #{lastname}',
+ :initials => '#{firstname.to_s.first}#{lastname.to_s.first}',
:order => %w(firstname lastname id),
:setting_order => 1
},
:firstname_lastinitial => {
:string => '#{firstname} #{lastname.to_s.chars.first}.',
+ :initials => '#{firstname.to_s.first}#{lastname.to_s.first}',
:order => %w(firstname lastname id),
:setting_order => 2
},
:firstinitial_lastname => {
:string => '#{firstname.to_s.gsub(/(([[:alpha:]])[[:alpha:]]*\.?)/, \'\2.\')} #{lastname}',
+ :initials => '#{firstname.to_s.gsub(/(([[:alpha:]])[[:alpha:]]*\.?)/, \'\2.\').first}#{lastname.to_s.first}',
:order => %w(firstname lastname id),
:setting_order => 2
},
:firstname => {
:string => '#{firstname}',
+ :initials => '#{firstname.to_s.first(2)}',
:order => %w(firstname id),
:setting_order => 3
},
:lastname_firstname => {
:string => '#{lastname} #{firstname}',
+ :initials => '#{lastname.to_s.first}#{firstname.to_s.first}',
:order => %w(lastname firstname id),
:setting_order => 4
},
:lastnamefirstname => {
:string => '#{lastname}#{firstname}',
+ :initials => '#{lastname.to_s.first}#{firstname.to_s.first}',
:order => %w(lastname firstname id),
:setting_order => 5
},
:lastname_comma_firstname => {
:string => '#{lastname}, #{firstname}',
+ :initials => '#{lastname.to_s.first}#{firstname.to_s.first}',
:order => %w(lastname firstname id),
:setting_order => 6
},
:lastname => {
:string => '#{lastname}',
+ :initials => '#{lastname.to_s.first(2)}',
:order => %w(lastname id),
:setting_order => 7
},
:username => {
:string => '#{login}',
+ :initials => '#{login.to_s.first(2)}',
:order => %w(login id),
:setting_order => 8
},
@@ -89,10 +98,10 @@ class User < Principal
:after_remove => Proc.new {|user, group| group.user_removed(user)}
has_many :changesets, :dependent => :nullify
has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
- has_one :atom_token, lambda {where "action='feeds'"}, :class_name => 'Token'
- has_one :api_token, lambda {where "action='api'"}, :class_name => 'Token'
- has_one :email_address, lambda {where :is_default => true}, :autosave => true
+ has_one :atom_token, lambda {where "#{table.name}.action='feeds'"}, :class_name => 'Token'
+ has_one :api_token, lambda {where "#{table.name}.action='api'"}, :class_name => 'Token'
has_many :email_addresses, :dependent => :delete_all
+ has_many :reactions, dependent: :delete_all
belongs_to :auth_source
scope :logged, lambda {where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}")}
@@ -103,6 +112,7 @@ class User < Principal
attr_accessor :password, :password_confirmation, :generate_password
attr_accessor :last_before_login_on
attr_accessor :remote_ip
+ attr_writer :oauth_scope
LOGIN_LENGTH_LIMIT = 60
MAIL_LENGTH_LIMIT = 254
@@ -275,6 +285,14 @@ class User < Principal
end
end
+ # Return user's initials based on name format
+ def initials(formatter = nil)
+ f = self.class.name_formatter(formatter)
+ format = f[:initials] || USER_FORMATS[:firstname_lastname][:initials]
+ initials = eval('"' + format + '"')
+ initials.upcase
+ end
+
def registered?
self.status == STATUS_REGISTERED
end
@@ -715,6 +733,20 @@ class User < Principal
end
end
+ def admin?
+ if authorized_by_oauth?
+ # when signed in via oauth, the user only acts as admin when the admin scope is set
+ super and @oauth_scope.include?(:admin)
+ else
+ super
+ end
+ end
+
+ # true if the user has signed in via oauth
+ def authorized_by_oauth?
+ !@oauth_scope.nil?
+ end
+
# Return true if the user is allowed to do the specified action on a specific context
# Action can be:
# * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
@@ -735,7 +767,7 @@ class User < Principal
roles.any? do |role|
(context.is_public? || role.member?) &&
- role.allowed_to?(action) &&
+ role.allowed_to?(action, @oauth_scope) &&
(block ? yield(role, self) : true)
end
elsif context && context.is_a?(Array)
@@ -754,7 +786,7 @@ class User < Principal
# authorize if user has at least one role that has this permission
roles = self.roles.to_a | [builtin_role]
roles.any? do |role|
- role.allowed_to?(action) &&
+ role.allowed_to?(action, @oauth_scope) &&
(block ? yield(role, self) : true)
end
else
diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb
index 8b19d9a5a..e1842b131 100644
--- a/app/models/user_preference.rb
+++ b/app/models/user_preference.rb
@@ -73,7 +73,7 @@ class UserPreference < ApplicationRecord
if has_attribute? attr_name
super
else
- others ? others[attr_name] : nil
+ others&.[](attr_name)
end
end
diff --git a/app/models/version.rb b/app/models/version.rb
index 707ed59dc..3ca4f2bff 100644
--- a/app/models/version.rb
+++ b/app/models/version.rb
@@ -106,7 +106,7 @@ module FixedIssuesExtension
done = self.open(open).sum do |c|
estimated = c.total_estimated_hours.to_f
estimated = estimated_average unless estimated > 0.0
- ratio = c.closed? ? 100 : (c.done_ratio || 0)
+ ratio = open ? (c.done_ratio || 0) : 100
estimated * ratio
end
progress = done / (estimated_average * issues_count)
diff --git a/app/views/attachments/_form.html.erb b/app/views/attachments/_form.html.erb
index c8bb84123..e5b10fb55 100644
--- a/app/views/attachments/_form.html.erb
+++ b/app/views/attachments/_form.html.erb
@@ -15,6 +15,7 @@
<% if saved_attachments.present? %>
<% saved_attachments.each_with_index do |attachment, i| %>
<span id="attachments_p<%= i %>">
+ <%= sprite_icon('attachment', icon_only: true, size: 16, css_class: 'svg-attachment') %>
<%= text_field_tag("#{attachment_param}[p#{i}][filename]", attachment.filename, :class => 'filename') %>
<% if attachment.container_id.present? %>
<%= link_to sprite_icon('del', l(:button_delete), icon_only: true), "#", :onclick => "$(this).closest('.attachments_form').find('.add_attachment').show(); $(this).parent().remove(); return false;", :class => 'icon-only icon-del' %>
diff --git a/app/views/doorkeeper/applications/_form.html.erb b/app/views/doorkeeper/applications/_form.html.erb
new file mode 100644
index 000000000..e4f778f63
--- /dev/null
+++ b/app/views/doorkeeper/applications/_form.html.erb
@@ -0,0 +1,39 @@
+<%= error_messages_for 'application' %>
+<div class="box tabular">
+ <p><%= f.text_field :name, :required => true %></p>
+
+ <p>
+ <%= f.text_area :redirect_uri, :required => true, :size => 60, :label => :'activerecord.attributes.doorkeeper/application.redirect_uri' %>
+ <em class="info">
+ <%= t('doorkeeper.applications.help.redirect_uri') %>
+ </em>
+ </p>
+</div>
+
+<h3><%= l(:'activerecord.attributes.doorkeeper/application.scopes') %></h3>
+<p><em class="info"><%= l :text_oauth_info_scopes %></em></p>
+<div class="box tabular" id="scopes">
+<fieldset><legend><%= l :label_oauth_admin_access %></legend>
+ <label class="floating" style="width: auto;">
+ <%= check_box_tag 'doorkeeper_application[scopes][]', 'admin', @application.scopes.include?('admin'),
+ :id => "doorkeeper_application_scopes_admin"
+ %>
+ <%= l :text_oauth_admin_permission %>
+ </label>
+</fieldset>
+<% perms_by_module = Redmine::AccessControl.permissions.group_by {|p| p.project_module.to_s} %>
+<% perms_by_module.keys.sort.each do |mod| %>
+ <fieldset><legend><%= mod.blank? ? l(:label_project) : l_or_humanize(mod, :prefix => 'project_module_') %></legend>
+ <% perms_by_module[mod].each do |permission| %>
+ <label class="floating">
+ <%= check_box_tag 'doorkeeper_application[scopes][]', permission.name.to_s, (permission.public? || @application.scopes.include?( permission.name.to_s)),
+ :id => "doorkeeper_application_scopes_#{permission.name}",
+ :disabled => permission.public? %>
+ <%= l_or_humanize(permission.name, :prefix => 'permission_') %>
+ </label>
+ <% end %>
+ </fieldset>
+<% end %>
+<br /><%= check_all_links 'scopes' %>
+<%= hidden_field_tag 'doorkeeper_application[scopes][]', '' %>
+</div>
diff --git a/app/views/doorkeeper/applications/edit.html.erb b/app/views/doorkeeper/applications/edit.html.erb
new file mode 100644
index 000000000..aebc1a841
--- /dev/null
+++ b/app/views/doorkeeper/applications/edit.html.erb
@@ -0,0 +1,6 @@
+<%= title [l('label_oauth_application_plural'), oauth_applications_path], @application.name %>
+
+<%= labelled_form_for @application, url: doorkeeper_submit_path(@application) do |f| %>
+ <%= render :partial => 'form', :locals => {:f => f} %>
+ <%= submit_tag l(:button_save) %>
+<% end %>
diff --git a/app/views/doorkeeper/applications/index.html.erb b/app/views/doorkeeper/applications/index.html.erb
new file mode 100644
index 000000000..0ba31c0e8
--- /dev/null
+++ b/app/views/doorkeeper/applications/index.html.erb
@@ -0,0 +1,33 @@
+<div class="contextual">
+<%= link_to sprite_icon('add', t('.new')), new_oauth_application_path, :class => 'icon icon-add' %>
+</div>
+
+<%= title l 'label_oauth_application_plural' %>
+
+<% if @applications.any? %>
+<div class="autoscroll">
+<table class="list">
+ <thead><tr>
+ <th><%= t('.name') %></th>
+ <th><%= t('.callback_url') %></th>
+ <th><%= t('.scopes') %></th>
+ <th></th>
+ </tr></thead>
+ <tbody>
+ <% @applications.each do |application| %>
+ <tr id="application_<%= application.id %>" class="<%= cycle("odd", "even") %>">
+ <td class="name"><span><%= link_to application.name, oauth_application_path(application) %></span></td>
+ <td class="description"><%= truncate application.redirect_uri.split.join(', '), length: 50 %></td>
+ <td class="description"><%= safe_join application.scopes.map{|scope| h l_or_humanize(scope, prefix: 'permission_')}, ", " %></td>
+ <td class="buttons">
+ <%= link_to sprite_icon('edit', t('doorkeeper.applications.buttons.edit')), edit_oauth_application_path(application), class: 'icon icon-edit' %>
+ <%= link_to sprite_icon('del', t('doorkeeper.applications.buttons.destroy')), oauth_application_path(application), :data => {:confirm => t('doorkeeper.applications.confirmations.destroy')}, :method => :delete, :class => 'icon icon-del' %>
+ </td>
+ </tr>
+ <% end %>
+ </tbody>
+</table>
+</div>
+<% else %>
+ <p class="nodata"><%= l(:label_no_data) %></p>
+<% end %>
diff --git a/app/views/doorkeeper/applications/new.html.erb b/app/views/doorkeeper/applications/new.html.erb
new file mode 100644
index 000000000..e2a39ac93
--- /dev/null
+++ b/app/views/doorkeeper/applications/new.html.erb
@@ -0,0 +1,6 @@
+<%= title [l('label_oauth_application_plural'), oauth_applications_path], t('.title') %>
+
+<%= labelled_form_for @application, url: doorkeeper_submit_path(@application) do |f| %>
+<%= render :partial => 'form', :locals => { :f => f } %>
+<%= submit_tag l(:button_create) %>
+<% end %>
diff --git a/app/views/doorkeeper/applications/show.html.erb b/app/views/doorkeeper/applications/show.html.erb
new file mode 100644
index 000000000..c98e7d29c
--- /dev/null
+++ b/app/views/doorkeeper/applications/show.html.erb
@@ -0,0 +1,54 @@
+<div class="contextual">
+<%= link_to sprite_icon('edit', t('doorkeeper.applications.buttons.edit')), edit_oauth_application_path(@application), :accesskey => accesskey(:edit), class: 'icon icon-edit' %>
+<%= link_to sprite_icon('del', t('doorkeeper.applications.buttons.destroy')), oauth_application_path(@application), :data => {:confirm => t('doorkeeper.applications.confirmations.destroy')}, :method => :delete, :class => 'icon icon-del' %>
+</div>
+
+<%= title [l('label_oauth_application_plural'), oauth_applications_path], @application.name %>
+
+<div class="box">
+ <h3 class="icon icon-passwd"><%= sprite_icon('key', l(:label_information_plural)) %></h3>
+ <p>
+ <span class="label"><%= t('.application_id') %>:</span>
+ <code><%= h @application.uid %></code>
+ </p>
+ <p>
+ <span class="label"><%= t('.secret') %>:</span>
+ <code>
+ <% secret = flash[:application_secret].presence || @application.plaintext_secret %>
+ <% flash.delete :application_secret %>
+ <% if secret.blank? && Doorkeeper.config.application_secret_hashed? %>
+ <%= t('.secret_hashed') %>
+ <% else %>
+ <%= secret %>
+ <% end %>
+ </code>
+ <% if secret.present? && Doorkeeper.config.application_secret_hashed? %>
+ <strong><%= t "text_oauth_copy_secret_now" %></strong>
+ <% end %>
+ </p>
+ <p>
+ <span class="label"><%= t('.scopes') %>:</span>
+ <code><%= safe_join @application.scopes.map{|scope| h l_or_humanize(scope, prefix: 'permission_')}, ", " %></code>
+ </p>
+</div>
+
+<h3><%= t('.callback_urls') %></h3>
+
+<div class="autoscroll">
+<table class="list">
+ <thead><tr>
+ <th><%= t('.callback_url') %></th>
+ <th></th>
+ </tr></thead>
+ <tbody>
+ <% @application.redirect_uri.split.each do |uri| %>
+ <tr class="<%= cycle("odd", "even") %>">
+ <td class="name"><span><%= uri %></span></td>
+ <td class="buttons">
+ <%= link_to sprite_icon('shield-check', t('doorkeeper.applications.buttons.authorize')), oauth_authorization_path(client_id: @application.uid, redirect_uri: uri, response_type: 'code', scope: @application.scopes), class: 'icon icon-authorize', target: '_blank' %>
+ </td>
+ </tr>
+ <% end %>
+ </tbody>
+</table>
+</div>
diff --git a/app/views/doorkeeper/authorizations/error.html.erb b/app/views/doorkeeper/authorizations/error.html.erb
new file mode 100644
index 000000000..59cedf8f3
--- /dev/null
+++ b/app/views/doorkeeper/authorizations/error.html.erb
@@ -0,0 +1,6 @@
+<h2><%= t('doorkeeper.authorizations.error.title') %></h2>
+
+<p id="errorExplanation"><%= @pre_auth.error_response.body[:error_description] %></p>
+<p><a href="javascript:history.back()"><%= l(:button_back) %></a></p>
+
+<% html_title t('doorkeeper.authorizations.error.title') %>
diff --git a/app/views/doorkeeper/authorizations/new.html.erb b/app/views/doorkeeper/authorizations/new.html.erb
new file mode 100644
index 000000000..898f2e645
--- /dev/null
+++ b/app/views/doorkeeper/authorizations/new.html.erb
@@ -0,0 +1,48 @@
+<%= title t('.title') %>
+
+<div class="warning">
+<p><strong><%=h @pre_auth.client.name %></strong></p>
+
+<p><%= raw t('.prompt', client_name: content_tag(:strong, class: "text-info") { @pre_auth.client.name }) %></p>
+
+<div class="oauth-permissions">
+ <p><%= t('.able_to') %>:</p>
+ <ul>
+ <li><%= l :text_oauth_implicit_permissions %></li>
+ <% @pre_auth.scopes.each do |scope| %>
+ <% if scope == 'admin' %>
+ <li><%= l :label_oauth_permission_admin %></li>
+ <% else %>
+ <li><%= l_or_humanize(scope, prefix: 'permission_') %></li>
+ <% end %>
+ <% end %>
+ </ul>
+</div>
+
+<% if @pre_auth.scopes.include?('admin') %>
+ <p><%= l :text_oauth_admin_permission_info %></p>
+<% end %>
+</div>
+
+<p>
+ <%= form_tag oauth_authorization_path, method: :post do %>
+ <%= hidden_field_tag :client_id, @pre_auth.client.uid %>
+ <%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri %>
+ <%= hidden_field_tag :state, @pre_auth.state %>
+ <%= hidden_field_tag :response_type, @pre_auth.response_type %>
+ <%= hidden_field_tag :scope, @pre_auth.scope %>
+ <%= hidden_field_tag :code_challenge, @pre_auth.code_challenge %>
+ <%= hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method %>
+ <%= submit_tag t('doorkeeper.authorizations.buttons.authorize') %>
+ <% end %>
+ <%= form_tag oauth_authorization_path, method: :delete do %>
+ <%= hidden_field_tag :client_id, @pre_auth.client.uid %>
+ <%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri %>
+ <%= hidden_field_tag :state, @pre_auth.state %>
+ <%= hidden_field_tag :response_type, @pre_auth.response_type %>
+ <%= hidden_field_tag :scope, @pre_auth.scope %>
+ <%= hidden_field_tag :code_challenge, @pre_auth.code_challenge %>
+ <%= hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method %>
+ <%= submit_tag t('doorkeeper.authorizations.buttons.deny') %>
+ <% end %>
+</p>
diff --git a/app/views/doorkeeper/authorizations/show.html.erb b/app/views/doorkeeper/authorizations/show.html.erb
new file mode 100644
index 000000000..25ee88a87
--- /dev/null
+++ b/app/views/doorkeeper/authorizations/show.html.erb
@@ -0,0 +1,8 @@
+<%= title [l('label_oauth_authorized_application_plural'), oauth_authorized_applications_path] %>
+
+<fieldset class="tabular"><legend><%= l(:label_information_plural) %></legend>
+ <p>
+ <label><%= t('.title') %>:</label>
+ <code><%= params[:code] %></code>
+ </p>
+</fieldset>
diff --git a/app/views/doorkeeper/authorized_applications/index.html.erb b/app/views/doorkeeper/authorized_applications/index.html.erb
new file mode 100644
index 000000000..0a1fc8a00
--- /dev/null
+++ b/app/views/doorkeeper/authorized_applications/index.html.erb
@@ -0,0 +1,31 @@
+<%= title [t(:label_my_account), my_account_path], l('label_oauth_authorized_application_plural') %>
+
+<% if @applications.any? %>
+<div class="autoscroll">
+<table class="list">
+ <thead><tr>
+ <th><%= t('doorkeeper.authorized_applications.index.application') %></th>
+ <th><%= t('doorkeeper.authorized_applications.index.created_at') %></th>
+ <th></th>
+ </tr></thead>
+ <tbody>
+ <% @applications.each do |application| %>
+ <tr id="application_<%= application.id %>" class="<%= cycle("odd", "even") %>">
+ <td class="name"><span><%= application.name %></span></td>
+ <td ><%= format_date application.created_at %></td>
+ <td class="buttons">
+ <%= link_to sprite_icon('del', t('doorkeeper.authorized_applications.buttons.revoke')), oauth_authorized_application_path(application), :data => {:confirm => t('doorkeeper.authorized_applications.confirmations.revoke')}, :method => :delete, :class => 'icon icon-del' %>
+ </td>
+ </tr>
+ <% end %>
+ </tbody>
+</table>
+</div>
+<% else %>
+ <p class="nodata"><%= l(:label_no_data) %></p>
+<% end %>
+
+<% content_for :sidebar do %>
+<% @user = User.current %>
+<%= render :partial => 'my/sidebar' %>
+<% end %>
diff --git a/app/views/help/wiki_syntax/common_mark/en/wiki_syntax_common_mark.html.erb b/app/views/help/wiki_syntax/common_mark/en/wiki_syntax_common_mark.html.erb
index 486b96424..a650b2751 100644
--- a/app/views/help/wiki_syntax/common_mark/en/wiki_syntax_common_mark.html.erb
+++ b/app/views/help/wiki_syntax/common_mark/en/wiki_syntax_common_mark.html.erb
@@ -81,6 +81,14 @@
<th></th><td>HTML is &lt;del&gt;not&lt;/del&gt; &lt;u&gt;allowed&lt;/u&gt;.</td><td>HTML is <del>not</del> <u>allowed</u>.</td>
</tr>
+<tr><th colspan="3">Alerts <span class="more_info">(<a href="<%= help_wiki_syntax_path(:detailed, anchor: "16") %>" target="_blank">more</a>)</span></th></tr>
+<tr><th></th><td>> [!NOTE]<br>> You can use alerts like [!NOTE], [!TIP], [!IMPORTANT], [!WARNING], and [!CAUTION].</td><td>
+<div class="markdown-alert markdown-alert-note">
+<p class="markdown-alert-title">Note</p>
+<p>You can use alerts like [!NOTE], [!TIP], [!IMPORTANT], [!WARNING], and [!CAUTION].</p>
+</div>
+</td></tr>
+
</table>
<p><a href="<%= help_wiki_syntax_path(:detailed) %>" onclick="window.open('<%= help_wiki_syntax_path(:detailed) %>', '', ''); return false;">More Information</a></p>
diff --git a/app/views/help/wiki_syntax/common_mark/en/wiki_syntax_detailed_common_mark.html.erb b/app/views/help/wiki_syntax/common_mark/en/wiki_syntax_detailed_common_mark.html.erb
index a47a570f1..a74094460 100644
--- a/app/views/help/wiki_syntax/common_mark/en/wiki_syntax_detailed_common_mark.html.erb
+++ b/app/views/help/wiki_syntax/common_mark/en/wiki_syntax_detailed_common_mark.html.erb
@@ -27,6 +27,7 @@
<li><a href='#12'>Macros</a></li>
<li><a href='#13'>Code highlighting</a></li>
<li><a href='#15'>Raw HTML</a></li>
+ <li><a href='#16'>Alerts</a></li>
</ul>
<h2><a name="2" class="wiki-page"></a>Links</h2>
@@ -359,8 +360,8 @@ It can be expanded by clicking a link.
<p>The <strong>style</strong> attribute can be used in raw HTML to apply custom formatting. The following CSS properties are allowed:</p>
<pre><code>
color background-color
- width
- height
+ width min-width max-width
+ height min-height max-height
padding padding-left padding-right padding-top padding-bottom
margin margin-left margin-right margin-top margin-bottom
border border-left border-right border-top border-bottom border-radius border-style border-collapse border-spacing
@@ -369,5 +370,52 @@ It can be expanded by clicking a link.
float
</code></pre>
+ <h2><a name="16" class="wiki-page"></a>Alerts</h2>
+
+ <p>
+ <dl>
+ <dt><code>NOTE</code></dt>
+ <dd>
+ <pre><code>> [!NOTE]<br>> Wiki page edits are preserved as history, allowing you to restore previous versions if needed.</code></pre>
+ <div class="markdown-alert markdown-alert-note">
+ <p class="markdown-alert-title">Note</p>
+ <p>Wiki page edits are preserved as history, allowing you to restore previous versions if needed.</p>
+ </div>
+ </dd>
+ <dt><code>TIP</code></dt>
+ <dd>
+ <pre><code>> [!TIP]<br>> To quickly review the update history of an issue, use the "History" tab for convenient access.</code></pre>
+ <div class="markdown-alert markdown-alert-tip">
+ <p class="markdown-alert-title">Tip</p>
+ <p>To quickly review the update history of an issue, use the "History" tab for convenient access.</p>
+ </div>
+ </dd>
+ <dt><code>WARNING</code></dt>
+ <dd>
+ <pre><code>> [!WARNING]<br>> Deleting an issue is a permanent action. Be certain it is truly necessary before proceeding.</code></pre>
+ <div class="markdown-alert markdown-alert-warning">
+ <p class="markdown-alert-title">Warning</p>
+ <p>Deleting an issue is a permanent action. Be certain it is truly necessary before proceeding.</p>
+ </div>
+ </dd>
+ <dt><code>IMPORTANT</code></dt>
+ <dd>
+ <pre><code>> [!IMPORTANT]<br>> Changing role permissions can affect user access across all projects, so be mindful of potential impacts.</code></pre>
+ <div class="markdown-alert markdown-alert-important">
+ <p class="markdown-alert-title">Important</p>
+ <p>Changing role permissions can affect user access across all projects, so be mindful of potential impacts.</p>
+ </div>
+ </dd>
+ <dt><code>CAUTION</code></dt>
+ <dd>
+ <pre><code>> [!CAUTION]<br>> When installing plugins, make sure to verify compatibility. Version differences can cause unexpected behavior.</code></pre>
+ <div class="markdown-alert markdown-alert-caution">
+ <p class="markdown-alert-title">Caution</p>
+ <p>When installing plugins, make sure to verify compatibility. Version differences can cause unexpected behavior.</p>
+ </div>
+ </dd>
+ </dl>
+ </p>
+
</body>
</html>
diff --git a/app/views/issues/index.html.erb b/app/views/issues/index.html.erb
index af2510827..70f0b740a 100644
--- a/app/views/issues/index.html.erb
+++ b/app/views/issues/index.html.erb
@@ -35,9 +35,9 @@
<% end %>
<% other_formats_links do |f| %>
- <%= f.link_to_with_query_parameters 'Atom', :key => User.current.atom_key %>
<%= f.link_to_with_query_parameters 'CSV', {}, :onclick => "showModal('csv-export-options', '350px'); return false;" %>
<%= f.link_to_with_query_parameters 'PDF' %>
+ <%= f.link_to_with_query_parameters 'Atom', :key => User.current.atom_key %>
<% end %>
<div id="csv-export-options" style="display:none;">
diff --git a/app/views/issues/show.html.erb b/app/views/issues/show.html.erb
index 8f732032a..e41a91bb3 100644
--- a/app/views/issues/show.html.erb
+++ b/app/views/issues/show.html.erb
@@ -37,8 +37,18 @@
<%= assignee_avatar(@issue.assigned_to, :size => "22", :class => "gravatar-child") if @issue.assigned_to %>
</div>
-<div class="subject">
-<%= render_issue_subject_with_tree(@issue) %>
+<div data-controller="sticky-issue-header">
+ <div class="subject" data-sticky-issue-header-target="original">
+ <%= render_issue_subject_with_tree(@issue) %>
+ </div>
+ <div id="sticky-issue-header" data-sticky-issue-header-target="stickyHeader" class="issue">
+ <span class="issue-heading"><%= issue_heading(@issue) %>:</span>
+ <span class="subject"><%= @issue.subject %></span>
+ </div>
+</div>
+
+<div class="reaction">
+ <%= reaction_button @issue %>
</div>
<p class="author">
<%= authoring @issue.created_on, @issue.author %>.
@@ -132,8 +142,8 @@ end %>
<%= render partial: 'action_menu_edit' unless User.current.wants_comments_in_reverse_order? %>
<% other_formats_links do |f| %>
- <%= f.link_to 'Atom', :url => {:key => User.current.atom_key} %>
<%= f.link_to 'PDF' %>
+ <%= f.link_to 'Atom', :url => {:key => User.current.atom_key} %>
<% end %>
<% html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %>
diff --git a/app/views/messages/show.html.erb b/app/views/messages/show.html.erb
index b265cc962..b62709afa 100644
--- a/app/views/messages/show.html.erb
+++ b/app/views/messages/show.html.erb
@@ -27,6 +27,9 @@
<h2><%= avatar(@topic.author) %><%= @topic.subject %></h2>
<div class="message">
+<div class="reaction">
+ <%= reaction_button @topic %>
+</div>
<p><span class="author"><%= authoring @topic.created_on, @topic.author %></span></p>
<div id="message_topic_wiki" class="wiki">
<%= textilizable(@topic, :content) %>
@@ -44,6 +47,7 @@
<% @replies.each do |message| %>
<div class="message reply" id="<%= "message-#{message.id}" %>">
<div class="contextual">
+ <%= reaction_button message %>
<%= quote_reply(
url_for(:action => 'quote', :id => message, :format => 'js'),
"#message-#{message.id} .wiki",
diff --git a/app/views/my/account.html.erb b/app/views/my/account.html.erb
index c8706a5f5..95afbabac 100644
--- a/app/views/my/account.html.erb
+++ b/app/views/my/account.html.erb
@@ -1,6 +1,7 @@
<div class="contextual">
<%= additional_emails_link(@user) %>
<%= link_to(sprite_icon('key', l(:button_change_password)), { :action => 'password'}, :class => 'icon icon-passwd') if @user.change_password_allowed? %>
+<%= link_to(sprite_icon('apps', l('label_oauth_authorized_application_plural')), oauth_authorized_applications_path, :class => 'icon icon-applications') if Setting.rest_api_enabled? %>
<%= call_hook(:view_my_account_contextual, :user => @user)%>
</div>
diff --git a/app/views/news/show.html.erb b/app/views/news/show.html.erb
index d07a09eb7..601f12072 100644
--- a/app/views/news/show.html.erb
+++ b/app/views/news/show.html.erb
@@ -22,12 +22,17 @@
</div>
<% end %>
-<p><% unless @news.summary.blank? %><em><%= @news.summary %></em><br /><% end %>
-<span class="author"><%= authoring @news.created_on, @news.author %></span></p>
-<div class="wiki">
-<%= textilizable(@news, :description) %>
+<div class="news">
+ <div class="reaction">
+ <%= reaction_button @news %>
+ </div>
+ <p><% unless @news.summary.blank? %><em><%= @news.summary %></em><br /><% end %>
+ <span class="author"><%= authoring @news.created_on, @news.author %></span></p>
+ <div class="wiki">
+ <%= textilizable(@news, :description) %>
+ </div>
+ <%= link_to_attachments @news %>
</div>
-<%= link_to_attachments @news %>
<br />
<div id="comments" style="margin-bottom:16px;">
@@ -38,6 +43,7 @@
<% @comments.each do |comment| %>
<% next if comment.new_record? %>
<div class="contextual">
+ <%= reaction_button comment %>
<%= link_to_if_authorized sprite_icon('del', l(:button_delete)), { :controller => 'comments', :action => 'destroy', :id => @news, :comment_id => comment},
:data => {:confirm => l(:text_are_you_sure)}, :method => :delete,
:title => l(:button_delete),
diff --git a/app/views/projects/index.html.erb b/app/views/projects/index.html.erb
index 3a1c047c9..875e736b1 100644
--- a/app/views/projects/index.html.erb
+++ b/app/views/projects/index.html.erb
@@ -31,10 +31,10 @@
<% end %>
<% other_formats_links do |f| %>
- <%= f.link_to 'Atom', :url => {:key => User.current.atom_key} %>
<% if @query.display_type == 'list' %>
<%= f.link_to_with_query_parameters 'CSV', {}, :onclick => "showModal('csv-export-options', '350px'); return false;" %>
<% end %>
+ <%= f.link_to 'Atom', :url => {:key => User.current.atom_key} %>
<% end %>
<% html_title(l(:label_project_plural)) -%>
diff --git a/app/views/reactions/_replace_button.js.erb b/app/views/reactions/_replace_button.js.erb
new file mode 100644
index 000000000..a5c923ea4
--- /dev/null
+++ b/app/views/reactions/_replace_button.js.erb
@@ -0,0 +1,7 @@
+(() => {
+ const button = $('[data-reaction-button-id=<%= reaction_id_for @object %>]');
+
+ removeHoverTooltips(button);
+ button.html($('<%=j reaction_button @object %>').children());
+ setupHoverTooltips(button);
+})();
diff --git a/app/views/reactions/create.js.erb b/app/views/reactions/create.js.erb
new file mode 100644
index 000000000..20f3cc7ed
--- /dev/null
+++ b/app/views/reactions/create.js.erb
@@ -0,0 +1 @@
+<%= render 'replace_button' %>
diff --git a/app/views/reactions/destroy.js.erb b/app/views/reactions/destroy.js.erb
new file mode 100644
index 000000000..20f3cc7ed
--- /dev/null
+++ b/app/views/reactions/destroy.js.erb
@@ -0,0 +1 @@
+<%= render 'replace_button' %>
diff --git a/app/views/settings/_display.html.erb b/app/views/settings/_display.html.erb
index 62c53dfbb..3b2f95798 100644
--- a/app/views/settings/_display.html.erb
+++ b/app/views/settings/_display.html.erb
@@ -22,7 +22,12 @@
<p><%= setting_check_box :gravatar_enabled, :data => {:enables => '#settings_gravatar_default'} %>
<em class="info"><%= t(:text_avatar_server_config_html, :url => Redmine::Configuration['avatar_server_url']) %></em></p>
-<p><%= setting_select :gravatar_default, gravatar_default_setting_options, :blank => :label_none %></p>
+<p>
+ <%= setting_select :gravatar_default, gravatar_default_setting_options, :blank => :label_none %>
+ <em class="<%= Setting.gravatar_default == "initials" ? "info" : "hidden" %>">
+ <%= t(:text_setting_gravatar_default_initials_html) %>
+ </em>
+</p>
<p><%= setting_check_box :thumbnails_enabled, :data => {:enables => '#settings_thumbnails_size'} %></p>
@@ -35,3 +40,18 @@
<%= submit_tag l(:button_save) %>
<% end %>
+
+<%= javascript_tag do %>
+ $('#settings_gravatar_default').on('change', function(e){
+ const gravatar_default = e.target.value;
+ const em = e.target.parentElement.getElementsByTagName('em')[0];
+
+ if (gravatar_default === 'initials') {
+ em.classList.remove('hidden');
+ em.classList.add('info');
+ } else {
+ em.classList.add('hidden');
+ em.classList.remove('info');
+ }
+ });
+<% end %> \ No newline at end of file
diff --git a/app/views/settings/_general.html.erb b/app/views/settings/_general.html.erb
index 043067f18..44206b6c2 100644
--- a/app/views/settings/_general.html.erb
+++ b/app/views/settings/_general.html.erb
@@ -37,6 +37,8 @@
<p><%= setting_text_field :feeds_limit, :size => 6 %></p>
+<p><%= setting_check_box :reactions_enabled %></p>
+
<%= call_hook(:view_settings_general_form) %>
</div>
diff --git a/app/views/timelog/index.html.erb b/app/views/timelog/index.html.erb
index 55e2312b3..d9985e922 100644
--- a/app/views/timelog/index.html.erb
+++ b/app/views/timelog/index.html.erb
@@ -29,8 +29,8 @@
<span class="pagination"><%= pagination_links_full @entry_pages, @entry_count %></span>
<% other_formats_links do |f| %>
- <%= f.link_to_with_query_parameters 'Atom', :key => User.current.atom_key %>
<%= f.link_to_with_query_parameters 'CSV', {}, :onclick => "showModal('csv-export-options', '330px'); return false;" %>
+ <%= f.link_to_with_query_parameters 'Atom', :key => User.current.atom_key %>
<% end %>
<div id="csv-export-options" style="display:none;">
diff --git a/app/views/users/show.api.rsb b/app/views/users/show.api.rsb
index bf415795d..0681903b8 100644
--- a/app/views/users/show.api.rsb
+++ b/app/views/users/show.api.rsb
@@ -11,7 +11,7 @@ api.user do
api.passwd_changed_on @user.passwd_changed_on
api.avatar_url gravatar_url(@user.mail, {rating: nil, size: nil, default: Setting.gravatar_default}) if @user.mail && Setting.gravatar_enabled?
api.twofa_scheme @user.twofa_scheme if User.current.admin? || (User.current == @user)
- api.api_key @user.api_key if User.current.admin? || (User.current == @user)
+ api.api_key @user.api_key if (User.current.admin? || (User.current == @user && !User.current.authorized_by_oauth?))
api.status @user.status if User.current.admin?
render_api_custom_values @user.visible_custom_field_values, api
diff --git a/app/views/wiki/date_index.html.erb b/app/views/wiki/date_index.html.erb
index 7ee5b467d..c8acf933c 100644
--- a/app/views/wiki/date_index.html.erb
+++ b/app/views/wiki/date_index.html.erb
@@ -29,11 +29,11 @@
<% unless @pages.empty? %>
<% other_formats_links do |f| %>
- <%= f.link_to 'Atom', :url => {:controller => 'activities', :action => 'index', :id => @project, :show_wiki_edits => 1, :key => User.current.atom_key} %>
<% if User.current.allowed_to?(:export_wiki_pages, @project) %>
<%= f.link_to('PDF', :url => {:action => 'export', :format => 'pdf'}) %>
<%= f.link_to('HTML', :url => {:action => 'export'}) %>
<% end %>
+ <%= f.link_to 'Atom', :url => {:controller => 'activities', :action => 'index', :id => @project, :show_wiki_edits => 1, :key => User.current.atom_key} %>
<% end %>
<% end %>
diff --git a/app/views/wiki/index.html.erb b/app/views/wiki/index.html.erb
index 3f6d05fc2..a4afcb28e 100644
--- a/app/views/wiki/index.html.erb
+++ b/app/views/wiki/index.html.erb
@@ -22,14 +22,14 @@
<% unless @pages.empty? %>
<% other_formats_links do |f| %>
- <%= f.link_to 'Atom',
- :url => {:controller => 'activities', :action => 'index',
- :id => @project, :show_wiki_edits => 1,
- :key => User.current.atom_key} %>
<% if User.current.allowed_to?(:export_wiki_pages, @project) %>
<%= f.link_to('PDF', :url => {:action => 'export', :format => 'pdf'}) %>
<%= f.link_to('HTML', :url => {:action => 'export'}) %>
<% end %>
+ <%= f.link_to 'Atom',
+ :url => {:controller => 'activities', :action => 'index',
+ :id => @project, :show_wiki_edits => 1,
+ :key => User.current.atom_key} %>
<% end %>
<% end %>