diff options
Diffstat (limited to 'app')
32 files changed, 467 insertions, 20 deletions
diff --git a/app/assets/images/icons.svg b/app/assets/images/icons.svg index 55e3a042d..2b0c9a41b 100644 --- a/app/assets/images/icons.svg +++ b/app/assets/images/icons.svg @@ -459,6 +459,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..deaaa66b6 100644 --- a/app/assets/javascripts/application-legacy.js +++ b/app/assets/javascripts/application-legacy.js @@ -679,7 +679,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 +1222,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 +1233,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 8bcfb2fb1..40e82b8da 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -1293,17 +1293,12 @@ div.flash.warning svg.icon-svg, .conflict svg.icon-svg { /***** CommonMark Alerts *****/ .markdown-alert { border-left: 4px solid; - padding: 10px 10px 1px 10px; - margin: 10px 0; -} - -.markdown-alert-title + p { - margin-top: 2px; + padding-left: 0.6em; + margin: 1em 0; } .markdown-alert-title { font-weight: bold; - margin: 0 0 0.5em 0; } .markdown-alert-tip { border-color: #5db651; } @@ -2113,6 +2108,45 @@ 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 .icon-label { + margin-left: 3px; + margin-bottom: -1px; +} +.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/wiki_syntax.css b/app/assets/stylesheets/wiki_syntax.css index 41e780c75..89b117419 100644 --- a/app/assets/stylesheets/wiki_syntax.css +++ b/app/assets/stylesheets/wiki_syntax.css @@ -72,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 e90279641..ad3c8c65f 100644 --- a/app/assets/stylesheets/wiki_syntax_detailed.css +++ b/app/assets/stylesheets/wiki_syntax_detailed.css @@ -63,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/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/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/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/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..66f81033e 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) @@ -41,6 +42,8 @@ module JournalsHelper end if journal.notes.present? + links << reaction_button(journal) + 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/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/reactions_helper.rb b/app/helpers/reactions_helper.rb new file mode 100644 index 000000000..5c0b807ee --- /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), + 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), + 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) + end + end + end + + def reaction_button_wrapper(object, &) + tag.span(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/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..bfef3533a 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 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/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/user.rb b/app/models/user.rb index 4b6387ae5..c1a860f5a 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 }, @@ -92,6 +101,7 @@ class User < Principal 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}")} @@ -274,6 +284,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 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 <del>not</del> <u>allowed</u>.</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 193606ab2..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> @@ -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/show.html.erb b/app/views/issues/show.html.erb index 0a6da1098..e41a91bb3 100644 --- a/app/views/issues/show.html.erb +++ b/app/views/issues/show.html.erb @@ -47,6 +47,9 @@ </div> </div> +<div class="reaction"> + <%= reaction_button @issue %> +</div> <p class="author"> <%= authoring @issue.created_on, @issue.author %>. <% if @issue.created_on != @issue.updated_on %> 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/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/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> |