diff options
Diffstat (limited to 'app')
34 files changed, 514 insertions, 72 deletions
diff --git a/app/assets/images/icons.svg b/app/assets/images/icons.svg index 2b0c9a41b..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"/> diff --git a/app/assets/javascripts/application-legacy.js b/app/assets/javascripts/application-legacy.js index 286e3e2e6..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"); diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 40e82b8da..833e998f8 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -359,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%;} @@ -795,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;} @@ -916,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; } @@ -927,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; } @@ -1091,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; } @@ -1145,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;} @@ -1285,6 +1316,9 @@ 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; } @@ -1303,18 +1337,23 @@ div.flash.warning svg.icon-svg, .conflict svg.icon-svg { .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 { @@ -1590,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;} @@ -1875,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; } @@ -1902,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; @@ -2109,16 +2163,13 @@ color: #555; text-shadow: 1px 1px 0 #fff; img.filecontent.image {background-image: url(/transparent.png);} /* Reaction styles */ -.reaction-button.reacted .icon-svg { - fill: #126fa7; - stroke: none; -} -.reaction-button.reacted:hover .icon-svg { - fill: #c61a1a; +.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; diff --git a/app/assets/stylesheets/responsive.css b/app/assets/stylesheets/responsive.css index 3a2eb46bb..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; } @@ -854,6 +864,13 @@ 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/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/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/reactions_controller.rb b/app/controllers/reactions_controller.rb index f768f939d..71b37e5f8 100644 --- a/app/controllers/reactions_controller.rb +++ b/app/controllers/reactions_controller.rb @@ -60,6 +60,6 @@ class ReactionsController < ApplicationController end def authorize_reactable - render_403 unless Redmine::Reaction.writable?(@object, User.current) + render_403 unless Redmine::Reaction.editable?(@object, User.current) end 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/journals_helper.rb b/app/helpers/journals_helper.rb index 66f81033e..7f1a449fb 100644 --- a/app/helpers/journals_helper.rb +++ b/app/helpers/journals_helper.rb @@ -41,9 +41,9 @@ module JournalsHelper ) end - if journal.notes.present? - links << reaction_button(journal) + links << reaction_button(journal) + if journal.notes.present? if options[:reply_links] url = quoted_issue_path(issue, :journal_id => journal, :journal_indice => indice) links << quote_reply(url, "#journal-#{journal.id}-notes", icon_only: true) 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 index 911da7127..e02e1c9f9 100644 --- a/app/helpers/reactions_helper.rb +++ b/app/helpers/reactions_helper.rb @@ -26,15 +26,15 @@ module ReactionsHelper detail = object.reaction_detail - reaction = detail.user_reaction + 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.writable?(object, User.current) - if reaction&.persisted? - reaction_button_reacted(object, reaction, count, tooltip) + 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 @@ -52,7 +52,7 @@ module ReactionsHelper def reaction_button_reacted(object, reaction, count, tooltip) reaction_button_wrapper object do link_to( - sprite_icon('thumb-up-filled', count), + 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'], @@ -64,7 +64,7 @@ module ReactionsHelper def reaction_button_not_reacted(object, count, tooltip) reaction_button_wrapper object do link_to( - sprite_icon('thumb-up', count), + sprite_icon('thumb-up', count.nonzero?), reactions_path(object_type: object.class.name, object_id: object), remote: true, method: :post, class: 'icon reaction-button', @@ -76,13 +76,13 @@ module ReactionsHelper 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) + sprite_icon('thumb-up', count.nonzero?) end end end def reaction_button_wrapper(object, &) - tag.span(data: { 'reaction-button-id': reaction_id_for(object) }, &) + tag.span(class: 'reaction-button-wrapper', data: { 'reaction-button-id': reaction_id_for(object) }, &) end def build_reaction_tooltip(visible_user_names, count) 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/models/issue.rb b/app/models/issue.rb index bfef3533a..576840843 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -1175,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/reaction.rb b/app/models/reaction.rb index 84c982043..184ed2d6e 100644 --- a/app/models/reaction.rb +++ b/app/models/reaction.rb @@ -25,39 +25,34 @@ class Reaction < ApplicationRecord 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( - # Total number of reactions - :reaction_count, # Users who reacted and are visible to the target user :visible_users, # Reaction of the target user :user_reaction ) do - def initialize(reaction_count: 0, visible_users: [], user_reaction: nil) + 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 = preload(:user) + reactions = visible(user) .for_reactable(reactables) + .preload(:user) .select(:id, :reactable_id, :user_id) .order(id: :desc) - # Prepare IDs of users who reacted and are visible to the user - visible_user_ids = User.visible(user) - .joins(:reactions) - .where(reactions: for_reactable(reactables)) - .pluck(:id).to_set - reactions.each_with_object({}) do |reaction, m| m[reaction.reactable_id] ||= Detail.new m[reaction.reactable_id].then do |detail| - detail.reaction_count += 1 - detail.visible_users << reaction.user if visible_user_ids.include?(reaction.user.id) + detail.visible_users << reaction.user detail.user_reaction = reaction if reaction.user == user 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 9f74a60fb..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 }, @@ -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/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/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/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/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 |