Merge branch 'master' into stable

This commit is contained in:
Alexander Meindl 2021-08-22 10:08:28 +02:00
commit 0610176052
225 changed files with 1832 additions and 1217 deletions

View File

@ -14,7 +14,8 @@ jobs:
- name: Setup Gemfile
run: |
cp test/support/gemfile.rb Gemfile
touch .enable_dev
sed -i "3isource 'https://rubygems.org'" Gemfile
- name: Setup Ruby
uses: ruby/setup-ruby@v1

2
.gitignore vendored
View File

@ -6,7 +6,9 @@ tmp/
Gemfile.lock
.project
.vscode
.bundle
.settings/
docs/_build
docs/_static
docs/_templates
.enable_dev

View File

@ -16,6 +16,10 @@ Metrics/AbcSize:
Metrics/BlockLength:
Enabled: false
Metrics/ParameterLists:
Enabled: true
CountKeywordArgs: false
Metrics/ClassLength:
Enabled: false
@ -58,16 +62,45 @@ Performance/ChainArrayAllocation:
Style/AutoResourceCleanup:
Enabled: true
Style/ExpandPathArguments:
Enabled: true
Exclude:
- additionals.gemspec
- test/**/*
Style/FrozenStringLiteralComment:
Enabled: false
Enabled: true
Exclude:
- '/**/*.rsb'
Style/OptionHash:
Enabled: true
SuspiciousParamNames:
- options
- api_options
- opts
- args
- params
- parameters
- settings
Exclude:
- lib/additionals/patches/*.rb
Style/ReturnNil:
Enabled: true
Style/UnlessLogicalOperators:
Enabled: true
Style/MethodCallWithArgsParentheses:
Enabled: true
AllowParenthesesInMultilineCall: true
AllowParenthesesInChaining: true
EnforcedStyle: omit_parentheses
Style/Documentation:
Enabled: false
# required for travis/ci (symbolic links problem)
Style/ExpandPathArguments:
Enabled: false
Style/HashTransformValues:
Enabled: false

View File

@ -4,6 +4,7 @@ linters:
RuboCop:
ignored_cops:
- Layout/ArgumentAlignment
- Layout/ArrayAlignment
- Layout/BlockEndNewline
- Layout/EmptyLineAfterGuardClause
- Layout/HashAlignment
@ -21,6 +22,7 @@ linters:
- Lint/Void
- Metrics/BlockNesting
- Rails/OutputSafety
- Style/FrozenStringLiteralComment
- Style/IdenticalConditionalBranches
- Style/IfUnlessModifier
- Style/Next

View File

@ -1,6 +1,17 @@
Changelog
=========
3.0.3
+++++
- Ruby 2.7 warnings fixed
- Mermaid 8.11.5 support
- D3 7.0.1 support
- Remove issue macro, which is now supported by Redmine core
- new ticket message can be overwritten for projects
- fixed scope of public project dashboards for all projects
- FontAwesome 5.15.4 support
3.0.2
+++++

18
Gemfile
View File

@ -1,2 +1,20 @@
# frozen_string_literal: true
# Specify your gem's dependencies in additionals.gemspec
gemspec
group :development do
# this is only used for development.
# if you want to use it, do:
# - create .enable_dev file in additionals directory
# - remove rubocop entries from REDMINE/Gemfile
# - remove REDMINE/.rubocop* files
if File.file? File.expand_path './.enable_dev', __dir__
gem 'brakeman', require: false
gem 'pandoc-ruby', require: false
gem 'rubocop', require: false
gem 'rubocop-performance', require: false
gem 'rubocop-rails', require: false
gem 'slim_lint', require: false
end
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'bundler/gem_tasks'
require 'rake/testtask'

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
lib = File.expand_path '../lib', __FILE__
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
$LOAD_PATH.unshift lib unless $LOAD_PATH.include? lib
require 'additionals/version'
Gem::Specification.new do |spec|
@ -17,14 +19,9 @@ Gem::Specification.new do |spec|
spec.require_paths = ['lib']
spec.required_ruby_version = '>= 2.5'
spec.add_runtime_dependency 'deface', '1.5.3'
spec.add_runtime_dependency 'deface', '1.8.1'
spec.add_runtime_dependency 'gemoji', '~> 3.0.0'
spec.add_runtime_dependency 'render_async'
spec.add_runtime_dependency 'rss'
spec.add_runtime_dependency 'slim-rails'
spec.add_development_dependency 'brakeman'
spec.add_development_dependency 'bundler'
spec.add_development_dependency 'rake'
spec.add_development_dependency 'slim_lint'
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class AdditionalsAssignToMeController < ApplicationController
before_action :find_issue
helper :additionals_issues
@ -11,11 +13,11 @@ class AdditionalsAssignToMeController < ApplicationController
return
end
@issue.init_journal(User.current)
@issue.init_journal User.current
@issue.assigned_to = User.current
if !@issue.save || old_user == @issue.assigned_to
flash[:error] = l(:error_issues_could_not_be_assigned_to_me)
flash[:error] = l :error_issues_could_not_be_assigned_to_me
return redirect_to(issue_path(@issue))
end
@ -29,7 +31,7 @@ class AdditionalsAssignToMeController < ApplicationController
private
def find_issue
@issue = Issue.find(params[:issue_id])
@issue = Issue.find params[:issue_id]
raise Unauthorized unless @issue.visible? && @issue.editable?
@project = @issue.project

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class AdditionalsChangeStatusController < ApplicationController
before_action :find_issue
helper :additionals_issues
@ -6,7 +8,7 @@ class AdditionalsChangeStatusController < ApplicationController
issue_old_status_id = @issue.status.id
issue_old_user = @issue.assigned_to
new_status_id = params[:new_status_id].to_i
allowed_status = @issue.sidbar_change_status_allowed_to(User.current, new_status_id)
allowed_status = @issue.sidbar_change_status_allowed_to User.current, new_status_id
if new_status_id < 1 || @issue.status_id == new_status_id || allowed_status.nil?
redirect_to(issue_path(@issue))
@ -18,7 +20,7 @@ class AdditionalsChangeStatusController < ApplicationController
@issue.assigned_to = User.current if @issue.status_x_affected?(new_status_id) && issue_old_user != User.current
if !@issue.save || issue_old_status_id == @issue.status_id
flash[:error] = l(:error_issue_status_could_not_changed)
flash[:error] = l :error_issue_status_could_not_changed
return redirect_to(issue_path(@issue))
end
@ -33,7 +35,7 @@ class AdditionalsChangeStatusController < ApplicationController
private
def find_issue
@issue = Issue.find(params[:issue_id])
@issue = Issue.find params[:issue_id]
raise Unauthorized unless @issue.visible? && @issue.editable?
@project = @issue.project

View File

@ -0,0 +1,65 @@
# frozen_string_literal: true
# This file is a part of redmine_db,
# a Redmine plugin to manage custom database entries.
#
# Copyright (c) 2016-2021 AlphaNodes GmbH
# https://alphanodes.com
class AdditionalsJournalsController < ApplicationController
before_action :find_journal, only: %i[edit update diff]
before_action :authorize, only: %i[edit update]
helper :custom_fields
helper :journals
helper :additionals_journals
def diff
@entry = @journal.journalized
@detail =
if params[:detail_id].present?
@journal.details.find_by id: params[:detail_id]
else
@journal.details.detect { |d| d.property == 'attr' && d.prop_key == 'description' }
end
unless @entry && @detail
render_404
return false
end
raise ::Unauthorized if @detail.property == 'cf' && !@detail.custom_field&.visible_by?(@entry.project, User.current)
@diff = Redmine::Helpers::Diff.new @detail.value, @detail.old_value
end
def edit
return render_403 unless @journal.editable_by? User.current
respond_to do |format|
# TODO: implement non-JS journal update
format.js
end
end
def update
return render_403 unless @journal.editable_by? User.current
@journal.safe_attributes = params[:journal]
@journal.save
@journal.destroy if @journal.details.empty? && @journal.notes.blank?
respond_to do |format|
format.html { redirect_after_update }
format.js
end
end
private
def redirect_after_update
raise 'overwrite it'
end
def find_journal
raise 'overwrite it'
end
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class AdditionalsMacrosController < ApplicationController
before_action :require_login

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'open-uri'
class DashboardAsyncBlocksController < ApplicationController
@ -84,7 +86,7 @@ class DashboardAsyncBlocksController < ApplicationController
rescue ActiveRecord::RecordNotFound
render_404
end
deny_access unless User.current.allowed_to?(:view_project, @project)
deny_access unless User.current.allowed_to? :view_project, @project
@project
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class DashboardsController < ApplicationController
menu_item :dashboards
@ -49,7 +51,7 @@ class DashboardsController < ApplicationController
def show
respond_to do |format|
format.html { head 406 }
format.html { head :not_acceptable }
format.js if request.xhr?
format.api
end
@ -63,7 +65,7 @@ class DashboardsController < ApplicationController
end
def create
@dashboard = Dashboard.new(author: User.current)
@dashboard = Dashboard.new author: User.current
@dashboard.safe_attributes = params[:dashboard]
@dashboard.dashboard_type = assign_dashboard_type
@dashboard.role_ids = params[:dashboard][:role_ids] if params[:dashboard].present?
@ -71,7 +73,7 @@ class DashboardsController < ApplicationController
@allowed_projects = @dashboard.allowed_target_projects
if @dashboard.save
flash[:notice] = l(:notice_successful_create)
flash[:notice] = l :notice_successful_create
respond_to do |format|
format.html { redirect_to dashboard_link_path(@project, @dashboard) }
@ -80,13 +82,13 @@ class DashboardsController < ApplicationController
else
respond_to do |format|
format.html { render action: 'new' }
format.api { render_validation_errors(@dashboard) }
format.api { render_validation_errors @dashboard }
end
end
end
def edit
return render_403 unless @dashboard.editable_by?(User.current)
return render_403 unless @dashboard.editable_by? User.current
@allowed_projects = @dashboard.allowed_target_projects
@ -97,7 +99,7 @@ class DashboardsController < ApplicationController
end
def update
return render_403 unless @dashboard.editable_by?(User.current)
return render_403 unless @dashboard.editable_by? User.current
@dashboard.safe_attributes = params[:dashboard]
@dashboard.role_ids = params[:dashboard][:role_ids] if params[:dashboard].present?
@ -106,9 +108,9 @@ class DashboardsController < ApplicationController
@allowed_projects = @dashboard.allowed_target_projects
if @dashboard.save
flash[:notice] = l(:notice_successful_update)
flash[:notice] = l :notice_successful_update
respond_to do |format|
format.html { redirect_to dashboard_link_path @project, @dashboard }
format.html { redirect_to dashboard_link_path(@project, @dashboard) }
format.api { head :ok }
end
else
@ -124,20 +126,20 @@ class DashboardsController < ApplicationController
begin
@dashboard.destroy
flash[:notice] = l(:notice_successful_delete)
flash[:notice] = l :notice_successful_delete
respond_to do |format|
format.html { redirect_to @project.nil? ? home_path : project_path(@project) }
format.api { head :ok }
end
rescue ActiveRecord::RecordNotDestroyed
flash[:error] = l(:error_remove_db_entry)
flash[:error] = l :error_remove_db_entry
redirect_to dashboard_path(@dashboard)
end
end
def query_statement_invalid(exception)
logger&.error "Query::StatementInvalid: #{exception.message}"
session.delete(additionals_query_session_key('dashboard'))
session.delete additionals_query_session_key('dashboard')
render_error l(:error_query_statement_invalid)
end
@ -145,7 +147,7 @@ class DashboardsController < ApplicationController
block_settings = params[:settings] || {}
block_settings.each do |block, settings|
@dashboard.update_block_settings(block, settings.to_unsafe_hash)
@dashboard.update_block_settings block, settings.to_unsafe_hash
end
@dashboard.save
@updated_blocks = block_settings.keys
@ -183,7 +185,7 @@ class DashboardsController < ApplicationController
def order_blocks
@dashboard.order_blocks params[:group], params[:blocks]
@dashboard.save
head 200
head :ok
end
private
@ -201,11 +203,15 @@ class DashboardsController < ApplicationController
end
def find_dashboard
@dashboard = Dashboard.find(params[:id])
@dashboard = Dashboard.find params[:id]
raise ::Unauthorized unless @dashboard.visible?
if @dashboard.dashboard_type == DashboardContentProject::TYPE_NAME && @dashboard.project.nil?
@dashboard.content_project = find_project_by_project_id
@dashboard.content_project = if params[:dashboard].present? && params[:dashboard][:content_project_id].present?
find_project params[:dashboard][:content_project_id]
else
find_project_by_project_id
end
else
@project = @dashboard.project
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module AdditionalsChartjsHelper
def chartjs_colorschemes_info_url
link_to(l(:label_chartjs_colorscheme_info),
@ -7,6 +9,6 @@ module AdditionalsChartjsHelper
def select_options_for_chartjs_colorscheme(selected)
data = YAML.safe_load(ERB.new(IO.read(File.join(Additionals.plugin_dir, 'config', 'colorschemes.yml'))).result) || {}
grouped_options_for_select(data, selected)
grouped_options_for_select data, selected
end
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module AdditionalsClipboardjsHelper
def clipboardjs_button_for(target, clipboard_text_from_button = nil)
render_clipboardjs_button(target, clipboard_text_from_button) + render_clipboardjs_javascript(target)
@ -34,6 +36,6 @@ module AdditionalsClipboardjsHelper
end
def render_clipboardjs_javascript(target)
javascript_tag("setClipboardJS('#zc_#{target}');")
javascript_tag "setClipboardJS('#zc_#{target}');"
end
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module AdditionalsCustomFieldsHelper
def custom_fields_with_full_with_layout
['IssueCustomField']

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module AdditionalsFontawesomeHelper
# name = TYPE-FA_NAME, eg. fas_car
# fas_cloud-upload-alt
@ -7,7 +9,7 @@ module AdditionalsFontawesomeHelper
# pre_text
# post_text
# title
def font_awesome_icon(name, options = {})
def font_awesome_icon(name, **options)
info = AdditionalsFontAwesome.value_info name
return '' if info.blank?
@ -29,7 +31,7 @@ module AdditionalsFontawesomeHelper
post_text = options[:post_text]
options.delete :post_text
end
s << tag.span(options)
s << tag.span(**options)
if post_text.present?
s << ' '
s << post_text
@ -37,16 +39,16 @@ module AdditionalsFontawesomeHelper
safe_join s
end
def additionals_fontawesome_select(form, selected, options = {})
def additionals_fontawesome_select(form, selected, **options)
options[:include_blank] ||= true unless options[:required]
html_options = {}
additionals_fontawesome_add_selected selected
name, options = Additionals.hash_remove_with_default(:name, options, :icon)
loader, options = Additionals.hash_remove_with_default(:loader, options, true)
html_options[:class], options = Additionals.hash_remove_with_default(:class, options, 'select2-fontawesome-field')
html_options[:style], options = Additionals.hash_remove_with_default(:style, options)
name, options = Additionals.hash_remove_with_default :name, options, :icon
loader, options = Additionals.hash_remove_with_default :loader, options, true
html_options[:class], options = Additionals.hash_remove_with_default :class, options, 'select2-fontawesome-field'
html_options[:style], options = Additionals.hash_remove_with_default :style, options
s = []
s << form.select(name,
@ -54,7 +56,7 @@ module AdditionalsFontawesomeHelper
options,
html_options)
s << additionals_fontawesome_loader(options, html_options) if loader
s << additionals_fontawesome_loader(**options, field_class: html_options[:class]) if loader
safe_join s
end
@ -70,21 +72,20 @@ module AdditionalsFontawesomeHelper
'250px'
end
def additionals_fontawesome_loader(options, html_options = {})
html_options[:class] ||= 'select2-fontawesome-field'
def additionals_fontawesome_loader(field_class: 'select2-fontawesome-field', **options)
options[:template_selection] = 'formatFontawesomeText'
options[:template_result] = 'formatFontawesomeText'
if options[:include_blank]
options[:placeholder] ||= l(:label_disabled)
options[:placeholder] ||= l :label_disabled
options[:allow_clear] ||= true
end
options[:width] = additionals_fontawesome_default_select_width
render(layout: false,
partial: 'additionals/select2_ajax_call.js',
render layout: false,
partial: 'additionals/select2_ajax_call',
formats: [:js],
locals: { field_class: html_options[:class],
locals: { field_class: field_class,
ajax_url: fontawesome_auto_completes_path(selected: @selected_store.join(',')),
options: options })
options: options }
end
end

View File

@ -1,7 +1,9 @@
# frozen_string_literal: true
module AdditionalsIssuesHelper
def author_options_for_select(project, entity = nil, permission = nil)
scope = project.present? ? project.users.visible : User.active.visible
scope = scope.with_permission(permission, project) unless permission.nil?
scope = scope.with_permission permission, project unless permission.nil?
authors = scope.sorted.to_a
unless entity.nil?
@ -15,7 +17,7 @@ module AdditionalsIssuesHelper
s = []
return s unless authors.any?
s << tag.option("<< #{l :label_me} >>", value: User.current.id) if authors.include?(User.current)
s << tag.option("<< #{l :label_me} >>", value: User.current.id) if authors.include? User.current
if entity.nil?
s << options_from_collection_for_select(authors, 'id', 'name')

View File

@ -1,13 +1,15 @@
# frozen_string_literal: true
module AdditionalsJournalsHelper
MultipleValuesDetail = Struct.new(:property, :prop_key, :custom_field, :old_value, :value)
MultipleValuesDetail = Struct.new :property, :prop_key, :custom_field, :old_value, :value
# Returns the textual representation of a journal details
# as an array of strings
def entity_details_to_strings(entity, details, options = {})
def entity_details_to_strings(entity, details, **options)
entity_type = entity.model_name.param_key
show_detail_method = "#{entity_type}_show_detail"
options[:only_path] = options[:only_path] != false
no_html = options.delete(:no_html)
no_html = options.delete :no_html
strings = []
values_by_field = {}
@ -21,21 +23,21 @@ module AdditionalsJournalsHelper
next
end
end
strings << send(show_detail_method, detail, no_html, options)
strings << send(show_detail_method, detail, no_html, **options)
end
if values_by_field.present?
values_by_field.each do |field, changes|
if changes[:added].any?
detail = MultipleValuesDetail.new('cf', field.id.to_s, field)
detail = MultipleValuesDetail.new 'cf', field.id.to_s, field
detail.value = changes[:added]
strings << send(show_detail_method, detail, no_html, options)
strings << send(show_detail_method, detail, no_html, **options)
end
next unless changes[:deleted].any?
detail = MultipleValuesDetail.new('cf', field.id.to_s, field)
detail = MultipleValuesDetail.new 'cf', field.id.to_s, field
detail.old_value = changes[:deleted]
strings << send(show_detail_method, detail, no_html, options)
strings << send(show_detail_method, detail, no_html, **options)
end
end
strings
@ -64,7 +66,7 @@ module AdditionalsJournalsHelper
# Returns the textual representation of a single journal detail
# rubocop: disable Rails/OutputSafety
def entity_show_detail(entity, detail, no_html = false, options = {}) # rubocop:disable Style/OptionalBooleanParameter:
def entity_show_detail(entity, detail, no_html = false, **options) # rubocop:disable Style/OptionalBooleanParameter:
multiple = false
no_detail = false
show_diff = false
@ -82,17 +84,17 @@ module AdditionalsJournalsHelper
if label || show_diff
unless no_html
label = tag.strong(label)
old_value = tag.i(old_value) if detail.old_value
old_value = tag.del(old_value) if detail.old_value && detail.value.blank?
value = tag.i(value) if value
label = tag.strong label
old_value = tag.i old_value if detail.old_value
old_value = tag.del old_value if detail.old_value && detail.value.blank?
value = tag.i value if value
end
html =
if no_detail
l(:text_journal_changed_no_detail, label: label)
l :text_journal_changed_no_detail, label: label
elsif show_diff
s = l(:text_journal_changed_no_detail, label: label)
s = l :text_journal_changed_no_detail, label: label
unless no_html
diff_link = link_to l(:label_diff),
send(diff_url_method,
@ -105,11 +107,11 @@ module AdditionalsJournalsHelper
s.html_safe
elsif detail.value.present?
if detail.old_value.present?
l(:text_journal_changed, label: label, old: old_value, new: value)
l :text_journal_changed, label: label, old: old_value, new: value
elsif multiple
l(:text_journal_added, label: label, value: value)
l :text_journal_added, label: label, value: value
else
l(:text_journal_set_to, label: label, value: value)
l :text_journal_set_to, label: label, value: value
end
else
l(:text_journal_deleted, label: label, old: old_value).html_safe
@ -122,6 +124,33 @@ module AdditionalsJournalsHelper
end
# rubocop: enable Rails/OutputSafety
def render_email_attributes(entry, html: false)
items = send "email_#{entry.class.name.underscore}_attributes", entry, html
if html
tag.ul class: 'details' do
items.map { |s| concat tag.li(s) }.join("\n")
end
else
items.map { |s| "* #{s}" }.join("\n")
end
end
def email_custom_field_values_attributes(entry, html)
items = []
entry.custom_field_values.each do |value|
cf_value = show_value value, false
next if cf_value.blank?
items << if html
tag.strong("#{value.custom_field.name}: ") + cf_value
else
"#{value.custom_field.name}: #{cf_value}"
end
end
items
end
private
def entity_show_detail_prop(detail, options)
@ -138,20 +167,20 @@ module AdditionalsJournalsHelper
prop = { label: custom_field.name }
project = Project.visible.where(id: detail.value).first if detail.value.present?
old_project = Project.visible.where(id: detail.old_value).first if detail.old_value.present?
prop[:value] = link_to_project(project) if project.present?
prop[:old_value] = link_to_project(old_project) if old_project.present?
prop[:value] = link_to_project project if project.present?
prop[:old_value] = link_to_project old_project if old_project.present?
when 'db_entry'
prop = { label: custom_field.name }
db_entry = DbEntry.visible.where(id: detail.value).first if detail.value.present?
old_db_entry = DbEntry.visible.where(id: detail.old_value).first if detail.old_value.present?
prop[:value] = link_to(db_entry.name, db_entry_url(db_entry)) if db_entry.present?
prop[:old_value] = link_to(old_db_entry.name, db_entry_url(old_db_entry)) if old_db_entry.present?
prop[:value] = link_to db_entry.name, db_entry_url(db_entry) if db_entry.present?
prop[:old_value] = link_to old_db_entry.name, db_entry_url(old_db_entry) if old_db_entry.present?
when 'password'
prop = { label: custom_field.name }
password = Password.visible.where(id: detail.value).first if detail.value.present? && defined?(Password)
old_password = Password.visible.where(id: detail.old_value).first if detail.old_value.present? && defined?(Password)
prop[:value] = link_to(password.name, password_url(password)) if password.present?
prop[:old_value] = link_to(old_password.name, password_url(old_password)) if old_password.present?
prop[:value] = link_to password.name, password_url(password) if password.present?
prop[:old_value] = link_to old_password.name, password_url(old_password) if old_password.present?
end
prop

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module AdditionalsMenuHelper
def additionals_top_menu_setup
return if Redmine::Plugin.installed? 'redmine_hrm'
@ -5,7 +7,7 @@ module AdditionalsMenuHelper
if Additionals.setting? :remove_mypage
Redmine::MenuManager.map(:top_menu).delete(:my_page) if Redmine::MenuManager.map(:top_menu).exists?(:my_page)
else
handle_top_menu_item(:my_page, { url: my_page_path, after: :home, if: proc { User.current.logged? } })
handle_top_menu_item(:my_page, url: my_page_path, after: :home, onlyif: proc { User.current.logged? })
end
if Additionals.setting? :remove_help
@ -18,46 +20,47 @@ module AdditionalsMenuHelper
end
end
def handle_top_submenu_item(menu_name, item)
handle_top_menu_item menu_name, item, with_submenu: true
def handle_top_submenu_item(menu_name, **item)
handle_top_menu_item menu_name, with_submenu: true, **item
end
def handle_top_menu_item(menu_name, item, with_submenu: false)
def handle_top_menu_item(menu_name, url:, with_submenu: false, onlyif: nil,
name: nil, parent: nil, title: nil, symbol: nil, before: nil, after: nil, last: false)
Redmine::MenuManager.map(:top_menu).delete(menu_name.to_sym) if Redmine::MenuManager.map(:top_menu).exists?(menu_name.to_sym)
html_options = {}
css_classes = []
css_classes << 'top-submenu' if with_submenu
css_classes << 'external' if item[:url].include? '://'
html_options[:class] = css_classes.join(' ') if css_classes.present?
css_classes << 'external' if url.include? '://'
html_options[:class] = css_classes.join ' ' if css_classes.present?
html_options[:title] = item[:title] if item[:title].present?
html_options[:title] = title if title.present?
menu_options = { parent: item[:parent].present? ? item[:parent].to_sym : nil,
menu_options = { parent: parent.present? ? parent.to_sym : nil,
html: html_options }
menu_options[:if] = menu_options[:if] if menu_options[:if].present?
menu_options[:if] = onlyif if onlyif.present?
menu_options[:caption] = if item[:symbol].present? && item[:name].present?
font_awesome_icon(item[:symbol], post_text: item[:name])
elsif item[:symbol].present?
font_awesome_icon(item[:symbol])
elsif item[:name].present?
item[:name].to_s
menu_options[:caption] = if symbol.present? && name.present?
font_awesome_icon symbol, post_text: name
elsif symbol.present?
font_awesome_icon symbol
elsif name.present?
name.to_s
end
if item[:last].present? && item[:last]
if last
menu_options[:last] = true
elsif item[:before].present?
menu_options[:before] = item[:before]
elsif item[:after].present?
menu_options[:after] = item[:after]
elsif before.present?
menu_options[:before] = before
elsif after.present?
menu_options[:after] = after
else
menu_options[:before] = :help
end
Redmine::MenuManager.map(:top_menu).push menu_name, item[:url], menu_options
Redmine::MenuManager.map(:top_menu).push menu_name, url, **menu_options
end
def render_custom_top_menu_item
@ -99,7 +102,8 @@ module AdditionalsMenuHelper
def additionals_custom_top_menu_item(item, user_roles)
show_entry = false
item[:roles].each do |role|
roles = item.delete :roles
roles.each do |role|
if user_roles.empty? && role.to_i == Role::BUILTIN_ANONYMOUS ||
# if user is logged in and non_member is active in item, always show it
User.current.logged? && role.to_i == Role::BUILTIN_NON_MEMBER
@ -113,13 +117,14 @@ module AdditionalsMenuHelper
break
end
end
break if show_entry == true
break if show_entry
end
menu_name = item.delete :menu_name
if show_entry
handle_top_menu_item item[:menu_name], item
elsif Redmine::MenuManager.map(:top_menu).exists?(item[:menu_name])
Redmine::MenuManager.map(:top_menu).delete(item[:menu_name])
handle_top_menu_item menu_name, item
elsif Redmine::MenuManager.map(:top_menu).exists?(menu_name)
Redmine::MenuManager.map(:top_menu).delete(menu_name)
end
end
@ -127,8 +132,8 @@ module AdditionalsMenuHelper
user_items = [{ title: 'Redmine Guide', url: Redmine::Info.help_url },
{ title: "Redmine #{l :label_macro_plural}", url: additionals_macros_path }]
admin_items = [{ title: 'Additionals',
url: 'https://additionals.readthedocs.io/en/latest/manual/', manual: true },
admin_items = [{ title: "Additionals #{l :label_help_manual}",
url: 'https://additionals.readthedocs.io/en/latest/manual/' },
{ title: 'Redmine Changelog',
url: "https://www.redmine.org/projects/redmine/wiki/Changelog_#{Redmine::VERSION::MAJOR}_#{Redmine::VERSION::MINOR}" },
{ title: 'Redmine Upgrade',
@ -144,13 +149,13 @@ module AdditionalsMenuHelper
begin
plugin_item_base = plugin.id.to_s.camelize.constantize
rescue LoadError
Rails.logger.debug "Ignore plugin #{plugin.id} for help integration"
Rails.logger.debug { "Ignore plugin #{plugin.id} for help integration" }
rescue StandardError => e
raise e unless e.class.to_s == 'NameError'
end
plugin_item = plugin_item_base.try(:additionals_help_items) unless plugin_item_base.nil?
plugin_item = additionals_help_items_fallbacks(plugin.id) if plugin_item.nil?
plugin_item = plugin_item_base.try :additionals_help_items unless plugin_item_base.nil?
plugin_item = additionals_help_items_fallbacks plugin.id if plugin_item.nil?
next if plugin_item.nil?
@ -186,12 +191,12 @@ module AdditionalsMenuHelper
s << if item[:title] == '-'
tag.li tag.hr
else
html_options = { class: "help_item_#{idx}" }
html_options = { class: +"help_item_#{idx}" }
if item[:url].include? '://'
html_options[:class] << ' external'
html_options[:target] = '_blank'
end
tag.li(link_to(item[:title], item[:url], html_options))
tag.li link_to(item[:title], item[:url], html_options)
end
end
safe_join s
@ -200,13 +205,8 @@ module AdditionalsMenuHelper
# Plugin help items definition for plugins,
# which do not have additionals_help_menu_items integration
def additionals_help_items_fallbacks(plugin_id)
plugins = { redmine_wiki_lists: [{ title: 'Wiki Lists Macros',
url: 'https://www.r-labs.org/projects/wiki_lists/wiki/Wiki_Lists_en' }],
redmine_wiki_extensions: [{ title: 'Wiki Extensions',
url: 'https://www.r-labs.org/projects/r-labs/wiki/Wiki_Extensions_en' }],
redmine_git_hosting: [{ title: 'Redmine Git Hosting',
url: 'http://redmine-git-hosting.io/get_started/',
admin: true }],
plugins = { redmine_drawio: [{ title: 'draw.io usage',
url: 'https://github.com/mikitex70/redmine_drawio#usage' }],
redmine_contacts: [{ title: 'Redmine CRM',
url: 'https://www.redmineup.com/pages/help/crm',
admin: true }],

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module AdditionalsProjectsHelper
def project_overview_name(_project, dashboard = nil)
name = [l(:label_overview)]
@ -5,4 +7,10 @@ module AdditionalsProjectsHelper
safe_join name, Additionals::LIST_SEPARATOR
end
def render_api_includes(project, api)
super
api.enable_new_ticket_message project.enable_new_ticket_message
api.new_ticket_message project.new_ticket_message
end
end

View File

@ -1,22 +1,28 @@
# frozen_string_literal: true
module AdditionalsQueriesHelper
def additionals_query_session_key(object_type)
"#{object_type}_query".to_sym
end
def additionals_retrieve_query(object_type, options = {})
def additionals_retrieve_query(object_type, user_filter: nil)
session_key = additionals_query_session_key object_type
query_class = Object.const_get "#{object_type.camelcase}Query"
if params[:query_id].present?
additionals_load_query_id(query_class, session_key, params[:query_id], options, object_type)
additionals_load_query_id query_class,
session_key,
params[:query_id],
object_type,
user_filter: user_filter
elsif api_request? ||
params[:set_filter] ||
session[session_key].nil? ||
session[session_key][:project_id] != (@project ? @project.id : nil)
# Give it a name, required to be valid
@query = query_class.new(name: '_')
@query = query_class.new name: '_'
@query.project = @project
@query.user_filter = options[:user_filter] if options[:user_filter]
@query.build_from_params(params)
@query.user_filter = user_filter if user_filter
@query.build_from_params params
session[session_key] = { project_id: @query.project_id }
# session has a limit to 4k, we have to use a cache for it for larger data
Rails.cache.write(additionals_query_cache_key(object_type),
@ -27,7 +33,7 @@ module AdditionalsQueriesHelper
sort_criteria: params[:sort].presence || @query.sort_criteria.to_a)
else
# retrieve from session
@query = query_class.find_by(id: session[session_key][:id]) if session[session_key][:id]
@query = query_class.find_by id: session[session_key][:id] if session[session_key][:id]
session_data = Rails.cache.read additionals_query_cache_key(object_type)
@query ||= query_class.new(name: '_',
filters: session_data.nil? ? nil : session_data[:filters],
@ -51,14 +57,14 @@ module AdditionalsQueriesHelper
end
end
def additionals_load_query_id(query_class, session_key, query_id, options, object_type)
scope = query_class.where(project_id: nil)
scope = scope.or(query_class.where(project_id: @project.id)) if @project
@query = scope.find(query_id)
def additionals_load_query_id(query_class, session_key, query_id, object_type, user_filter: nil)
scope = query_class.where project_id: nil
scope = scope.or query_class.where(project_id: @project.id) if @project
@query = scope.find query_id
raise ::Unauthorized unless @query.visible?
@query.project = @project
@query.user_filter = options[:user_filter] if options[:user_filter]
@query.user_filter = user_filter if user_filter
session[session_key] = { id: @query.id, project_id: @query.project_id }
@query.sort_criteria = params[:sort] if params[:sort].present?
@ -76,62 +82,58 @@ module AdditionalsQueriesHelper
"#{object_type}_query_data_#{session.id}_#{project_id}"
end
def additionals_select2_search_users(options = {})
def additionals_select2_search_users(all_visible: false, where_filter: nil, where_params: nil)
q = params[:q].to_s.strip
exclude_id = params[:user_id].to_i
scope = User.active.where type: 'User'
scope = scope.visible unless options[:all_visible]
scope = scope.where.not(id: exclude_id) if exclude_id.positive?
scope = scope.where(options[:where_filter], options[:where_params]) if options[:where_filter]
q.split.map { |search_string| scope = scope.like(search_string) } if q.present?
scope = scope.visible unless all_visible
scope = scope.where.not id: exclude_id if exclude_id.positive?
scope = scope.where where_filter, where_params if where_filter
q.split.map { |search_string| scope = scope.like search_string } if q.present?
scope = scope.order(last_login_on: :desc)
.limit(Additionals::SELECT2_INIT_ENTRIES)
@users = scope.to_a.sort! { |x, y| x.name <=> y.name }
render layout: false, partial: 'auto_completes/additionals_users'
end
def additionals_query_to_xlsx(items, query, options = {})
def additionals_query_to_xlsx(items, query, no_id_link: false)
require 'write_xlsx'
columns = if options[:columns].present? || options[:c].present?
query.available_columns
else
query.columns
end
options[:filename] = StringIO.new('')
options = { no_id_link: no_id_link,
filename: StringIO.new(+'') }
export_to_xlsx(items, columns, options)
export_to_xlsx items, query.columns, options
options[:filename].string
end
def additionals_result_to_xlsx(items, columns, options = {})
def additionals_result_to_xlsx(items, columns, options)
raise 'option filename is mission' if options[:filename].blank?
require 'write_xlsx'
export_to_xlsx(items, columns, options)
export_to_xlsx items, columns, options
end
def export_to_xlsx(items, columns, options)
workbook = WriteXLSX.new(options[:filename])
workbook = WriteXLSX.new options[:filename]
worksheet = workbook.add_worksheet
# Freeze header row and # column.
freeze_row = options[:freeze_first_row].nil? || options[:freeze_first_row] ? 1 : 0
freeze_column = options[:freeze_first_column].nil? || options[:freeze_first_column] ? 1 : 0
worksheet.freeze_panes(freeze_row, freeze_column)
worksheet.freeze_panes freeze_row, freeze_column
options[:columns_width] = if options[:xlsx_write_header_row].present?
send(options[:xlsx_write_header_row], workbook, worksheet, columns)
send options[:xlsx_write_header_row], workbook, worksheet, columns
else
xlsx_write_header_row(workbook, worksheet, columns)
xlsx_write_header_row workbook, worksheet, columns
end
options[:columns_width] = if options[:xlsx_write_item_rows].present?
send(options[:xlsx_write_item_rows], workbook, worksheet, columns, items, options)
send options[:xlsx_write_item_rows], workbook, worksheet, columns, items, options
else
xlsx_write_item_rows(workbook, worksheet, columns, items, options)
xlsx_write_item_rows workbook, worksheet, columns, items, options
end
columns.size.times do |index|
worksheet.set_column(index, index, options[:columns_width][index])
worksheet.set_column index, index, options[:columns_width][index]
end
workbook.close
@ -152,20 +154,20 @@ module AdditionalsQueriesHelper
columns_width
end
def xlsx_write_item_rows(workbook, worksheet, columns, items, options = {})
hyperlink_format = workbook.add_format(xlsx_cell_format(:link))
even_text_format = workbook.add_format(xlsx_cell_format(:text, '', 0))
even_text_format.set_num_format(0x31)
odd_text_format = workbook.add_format(xlsx_cell_format(:text, '', 1))
odd_text_format.set_num_format(0x31)
def xlsx_write_item_rows(workbook, worksheet, columns, items, options)
hyperlink_format = workbook.add_format xlsx_cell_format(:link)
even_text_format = workbook.add_format xlsx_cell_format(:text, '', 0)
even_text_format.set_num_format 0x31
odd_text_format = workbook.add_format xlsx_cell_format(:text, '', 1)
odd_text_format.set_num_format 0x31
items.each_with_index do |line, line_index|
columns.each_with_index do |c, column_index|
value = csv_content(c, line)
value = csv_content(c, line).dup
if c.name == :id # ID
if options[:no_id_link].blank?
link = url_for(controller: line.class.name.underscore.pluralize, action: 'show', id: line.id)
worksheet.write(line_index + 1, column_index, link, hyperlink_format, value)
link = url_for controller: line.class.name.underscore.pluralize, action: 'show', id: line.id
worksheet.write line_index + 1, column_index, link, hyperlink_format, value
else
# id without link
worksheet.write(line_index + 1,
@ -173,13 +175,13 @@ module AdditionalsQueriesHelper
value,
workbook.add_format(xlsx_cell_format(:cell, value, line_index)))
end
elsif xlsx_hyperlink_cell?(value)
worksheet.write(line_index + 1, column_index, value[0..254], hyperlink_format, value)
elsif xlsx_hyperlink_cell? value
worksheet.write line_index + 1, column_index, value[0..254], hyperlink_format, value
elsif !c.inline?
# block column can be multiline strings
value.gsub!("\r\n", "\n")
value.gsub! "\r\n", "\n"
text_format = line_index.even? ? even_text_format : odd_text_format
worksheet.write_rich_string(line_index + 1, column_index, value, text_format)
worksheet.write_rich_string line_index + 1, column_index, value, text_format
else
worksheet.write(line_index + 1,
column_index,
@ -187,7 +189,7 @@ module AdditionalsQueriesHelper
workbook.add_format(xlsx_cell_format(:cell, value, line_index)))
end
width = xlsx_get_column_width(value)
width = xlsx_get_column_width value
options[:columns_width][column_index] = width if options[:columns_width][column_index] < width
end
end
@ -238,7 +240,7 @@ module AdditionalsQueriesHelper
# TODO: this is a temporary fix and should be removed
# after https://www.redmine.org/issues/29830 is in Redmine core.
def query_as_hidden_field_tags(query)
tags = hidden_field_tag('set_filter', '1', id: nil)
tags = hidden_field_tag 'set_filter', '1', id: nil
if query.filters.present?
query.filters.each do |field, filter|
@ -254,7 +256,7 @@ module AdditionalsQueriesHelper
ignored_block_columns = query.block_columns.map(&:name)
query.columns.each do |column|
next if ignored_block_columns.include?(column.name)
next if ignored_block_columns.include? column.name
tags << hidden_field_tag('c[]', column.name, id: nil)
end
@ -268,11 +270,4 @@ module AdditionalsQueriesHelper
tags
end
def render_query_group_view(query, locals = {})
return if locals[:group_name].blank?
render partial: 'queries/additionals_group_view',
locals: { query: query }.merge(locals)
end
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module AdditionalsRoutesHelper
def _dashboards_path(project, *args)
if project
@ -71,12 +73,18 @@ module AdditionalsRoutesHelper
end
end
def dashboard_link_path(project, dashboard, options = {})
def dashboard_link_path(project, dashboard, **options)
options[:dashboard_id] = dashboard.id
if dashboard.dashboard_type == DashboardContentProject::TYPE_NAME
project_path project, options
case dashboard.dashboard_type
when DashboardContentProject::TYPE_NAME
project_path project, **options
when DashboardContentWelcome::TYPE_NAME
home_path(**options)
else
home_path options
dashboard_type_name = dashboard.dashboard_type[0..-10]
route_helper = "DashboardContent#{dashboard_type_name}::ROUTE_HELPER".constantize
send route_helper, **options
end
end
end

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
module AdditionalsSelect2Helper
def additionals_select2_tag(name, option_tags = nil, options = {})
s = select_tag(name, option_tags, options)
def additionals_select2_tag(name, option_tags, options)
s = select_tag name, option_tags, options
id = options.delete(:id) || sanitize_to_id(name)
s << hidden_field_tag("#{name}[]", '') if options[:multiple] && options.fetch(:include_hidden, true)
@ -8,7 +10,7 @@ module AdditionalsSelect2Helper
end
# Transforms select filters of +type+ fields into select2
def additionals_transform_to_select2(type, options = {})
javascript_tag("setSelect2Filter('#{type}', #{options.to_json});") unless type.empty?
def additionals_transform_to_select2(type, options)
javascript_tag "setSelect2Filter('#{type}', #{options.to_json});" unless type.empty?
end
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module AdditionalsSettingsHelper
def additionals_settings_tabs
tabs = [{ name: 'general', partial: 'additionals/settings/general', label: :label_general },
@ -13,7 +15,7 @@ module AdditionalsSettingsHelper
tabs
end
def additionals_settings_checkbox(name, options = {})
def additionals_settings_checkbox(name, **options)
active_value = options.delete(:active_value).presence || @settings.present? && @settings[name]
tag_name = options.delete(:tag_name).presence || "settings[#{name}]"
@ -36,53 +38,61 @@ module AdditionalsSettingsHelper
s = [label_tag(tag_name, label_title)]
s << hidden_field_tag(tag_name, 0, id: nil) if !custom_value || value_is_bool
s << check_box_tag(tag_name, value, checked, options)
s << check_box_tag(tag_name, value, checked, **options)
safe_join s
end
def additionals_settings_numberfield(name, options = {})
additionals_settings_input_field :number_field_tag, name, options
def additionals_settings_numberfield(name, **options)
additionals_settings_input_field :number_field_tag, name, **options
end
def additionals_settings_textfield(name, options = {})
additionals_settings_input_field :text_field_tag, name, options
def additionals_settings_textfield(name, **options)
additionals_settings_input_field :text_field_tag, name, **options
end
def additionals_settings_passwordfield(name, options = {})
additionals_settings_input_field :password_field_tag, name, options
def additionals_settings_passwordfield(name, **options)
additionals_settings_input_field :password_field_tag, name, **options
end
def additionals_settings_urlfield(name, options = {})
additionals_settings_input_field :url_field_tag, name, options
def additionals_settings_urlfield(name, **options)
additionals_settings_input_field :url_field_tag, name, **options
end
def additionals_settings_select(name, values, options = {})
def additionals_settings_timefield(name, **options)
additionals_settings_input_field :time_field_tag, name, **options
end
def additionals_settings_select(name, values, **options)
tag_name = options.delete(:tag_name).presence || "settings[#{name}]"
label_title = [options.delete(:label).presence || l("label_#{name}")]
label_title << tag.span('*', class: 'required') if options[:required].present?
safe_join [label_tag(tag_name, safe_join(label_title, ' ')),
select_tag(tag_name, values, options)]
select_tag(tag_name, values, **options)]
end
def additionals_settings_textarea(name, options = {})
def additionals_settings_textarea(name, **options)
label_title = options.delete(:label).presence || l("label_#{name}")
value = options.delete(:value).presence || @settings[name]
value = if options.key? :value
options.delete :value
elsif @settings.present?
@settings[name]
end
options[:class] = 'wiki-edit' unless options.key?(:class)
options[:rows] = addtionals_textarea_cols(value) unless options.key?(:rows)
options[:class] = 'wiki-edit' unless options.key? :class
options[:rows] = addtionals_textarea_cols value unless options.key? :rows
safe_join [label_tag("settings[#{name}]", label_title),
text_area_tag("settings[#{name}]", value, options)]
text_area_tag("settings[#{name}]", value, **options)]
end
private
def additionals_settings_input_field(tag_field, name, options = {})
def additionals_settings_input_field(tag_field, name, **options)
tag_name = options.delete(:tag_name).presence || "settings[#{name}]"
value = if options.key? :value
options.delete(:value).presence
options.delete :value
elsif @settings.present?
@settings[name]
end
@ -91,6 +101,6 @@ module AdditionalsSettingsHelper
label_title << tag.span('*', class: 'required') if options[:required].present?
safe_join [label_tag(tag_name, safe_join(label_title, ' ')),
send(tag_field, tag_name, value, options)], ' '
send(tag_field, tag_name, value, **options)], ' '
end
end

View File

@ -1,22 +1,24 @@
# frozen_string_literal: true
module AdditionalsWikiPdfHelper
include Redmine::Export::PDF
def wiki_page_to_pdf(page, project)
pdf = ITCPDF.new(current_language)
pdf.set_title("#{project} - #{page.title}")
pdf = ITCPDF.new current_language
pdf.set_title "#{project} - #{page.title}"
pdf.alias_nb_pages
pdf.footer_date = format_date(User.current.today)
pdf.footer_date = format_date User.current.today
pdf.add_page
unless Additionals.setting?(:wiki_pdf_remove_title)
pdf.SetFontStyle('B', 11)
unless Additionals.setting? :wiki_pdf_remove_title
pdf.SetFontStyle 'B', 11
pdf.RDMMultiCell(190, 5,
"#{project} - #{page.title} - # #{page.content.version}")
end
pdf.ln
# Set resize image scale
pdf.set_image_scale(1.6)
pdf.SetFontStyle('', 9)
if Additionals.setting?(:wiki_pdf_remove_attachments)
pdf.set_image_scale 1.6
pdf.SetFontStyle '', 9
if Additionals.setting? :wiki_pdf_remove_attachments
pdf.RDMwriteFormattedCell(190,
5,
'',
@ -28,7 +30,7 @@ module AdditionalsWikiPdfHelper
headings: false,
inline_attachments: false), page.attachments)
else
write_wiki_page(pdf, page)
write_wiki_page pdf, page
end
pdf.output
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module DashboardsHelper
def dashboard_async_cache(dashboard, block, async, settings, &content_block)
cache render_async_cache_key(_dashboard_async_blocks_path(@project, dashboard.async_params(block, async, settings))),
@ -30,17 +32,13 @@ module DashboardsHelper
safe_join classes, ' '
end
def sidebar_dashboards(dashboard, project = nil, user = nil)
user ||= User.current
scope = Dashboard.visible.includes([:author])
def sidebar_dashboards(dashboard, project = nil)
scope = Dashboard.visible.includes [:author]
scope = if project.present?
scope = scope.project_only
scope.where(project_id: project.id)
.or(scope.where(system_default: true)
.where(project_id: nil))
.or(scope.where(author_id: user.id)
.where(project_id: nil))
.or(scope.where(project_id: nil))
else
scope.where dashboard_type: dashboard.dashboard_type
end
@ -111,7 +109,7 @@ module DashboardsHelper
return '' unless dashboards.any?
tag.h3(title, class: 'dashboards') +
tag.ul(class: 'dashboards') do
tag.ul(class: 'dashboards') do # rubocop: disable Style/MethodCallWithArgsParentheses
dashboards.each do |dashboard|
selected = dashboard.id == if params[:dashboard_id].present?
params[:dashboard_id].to_i
@ -119,7 +117,7 @@ module DashboardsHelper
active_dashboard.id
end
css = 'dashboard'
css = +'dashboard'
css << ' selected' if selected
li_class = nil
@ -143,14 +141,14 @@ module DashboardsHelper
end
end
def dashboard_link(dashboard, project, options = {})
def dashboard_link(dashboard, project, **options)
if options[:title].blank? && dashboard.public?
author = if dashboard.author_id == User.current.id
l :label_me
else
dashboard.author
end
options[:title] = l(:label_dashboard_author, name: author)
options[:title] = l :label_dashboard_author, name: author
end
name = options.delete(:name) || dashboard.name
@ -171,10 +169,10 @@ module DashboardsHelper
end
end
def delete_dashboard_link(url, options = {})
def delete_dashboard_link(url)
options = { method: :delete,
data: { confirm: l(:text_are_you_sure) },
class: 'icon icon-del' }.merge(options)
class: 'icon icon-del' }
link_to l(:button_dashboard_delete), url, options
end
@ -252,7 +250,7 @@ module DashboardsHelper
exposed_params: %i[sort],
partial: 'dashboards/blocks/query_list' }
partial_locals[:async][:unique_params] = [Redmine::Utils.random_hex(16)] if params[:refresh].present?
partial_locals[:async] = partial_locals[:async].merge(block_definition[:async]) if block_definition[:async]
partial_locals[:async] = partial_locals[:async].merge block_definition[:async] if block_definition[:async]
elsif block_definition[:async]
partial_locals[:async] = block_definition[:async]
end
@ -277,9 +275,9 @@ module DashboardsHelper
title << query_block[:label]
title << if query_block[:with_project]
link_to(query.name, send(query_block[:link_helper], project, query.as_params))
link_to query.name, send(query_block[:link_helper], project, query.as_params)
else
link_to(query.name, send(query_block[:link_helper], query.as_params))
link_to query.name, send(query_block[:link_helper], query.as_params)
end
safe_join title, Additionals::LIST_SEPARATOR
@ -289,9 +287,9 @@ module DashboardsHelper
return if dashboard.visibility == Dashboard::VISIBILITY_PRIVATE
title = if query.visibility == Query::VISIBILITY_PRIVATE
l(:alert_only_visible_by_yourself)
l :alert_only_visible_by_yourself
elsif block_definition.key?(:admin_only) && block_definition[:admin_only]
l(:alert_only_visible_by_admins)
l :alert_only_visible_by_admins
end
return if title.nil?
@ -322,7 +320,7 @@ module DashboardsHelper
max_entries = settings[:max_entries] || DashboardContent::DEFAULT_MAX_ENTRIES
scope = Document.visible
scope = scope.where(project: dashboard.project) if dashboard.project
scope = scope.where project: dashboard.project if dashboard.project
documents = scope.order(created_on: :desc)
.limit(max_entries)
@ -357,10 +355,10 @@ module DashboardsHelper
days = 7 if days < 1 || days > 365
scope = TimeEntry.where user_id: User.current.id
scope = scope.where(project_id: dashboard.content_project.id) unless dashboard.content_project.nil?
scope = scope.where project_id: dashboard.content_project.id unless dashboard.content_project.nil?
entries_today = scope.where(spent_on: User.current.today)
entries_days = scope.where(spent_on: User.current.today - (days - 1)..User.current.today)
entries_today = scope.where spent_on: User.current.today
entries_days = scope.where spent_on: User.current.today - (days - 1)..User.current.today
render partial: 'dashboards/blocks/my_spent_time',
locals: { block: block,
@ -379,7 +377,7 @@ module DashboardsHelper
Redmine::Activity::Fetcher.new(user, options)
.events(nil, nil, limit: max_entries)
.group_by { |event| user.time_to_date(event.event_datetime) }
.group_by { |event| user.time_to_date event.event_datetime }
end
def dashboard_feed_catcher(url, max_entries)
@ -391,7 +389,7 @@ module DashboardsHelper
begin
URI.parse(url).open do |rss_feed|
rss = RSS::Parser.parse(rss_feed)
rss = RSS::Parser.parse rss_feed
rss.items.each do |item|
cnt += 1
feed[:items] << { title: item.title.try(:content)&.presence || item.title,
@ -430,7 +428,7 @@ module DashboardsHelper
# Renders a single block content
def render_dashboard_block_content(block, block_definition, dashboard, overwritten_settings = {})
settings = dashboard.layout_settings block
settings = settings.merge(overwritten_settings) if overwritten_settings.present?
settings = settings.merge overwritten_settings if overwritten_settings.present?
partial = block_definition[:partial]
partial_locals = build_dashboard_partial_locals block, block_definition, settings, dashboard
@ -441,7 +439,7 @@ module DashboardsHelper
begin
render partial: partial, locals: partial_locals
rescue ActionView::MissingTemplate
Rails.logger.warn("Partial \"#{partial}\" missing for block \"#{block}\" found in #{dashboard.name} (id=#{dashboard.id})")
Rails.logger.warn "Partial \"#{partial}\" missing for block \"#{block}\" found in #{dashboard.name} (id=#{dashboard.id})"
nil
end
else

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class AdditionalsJob < ActiveJob::Base
# Automatically retry jobs that encountered a deadlock
# retry_on ActiveRecord::Deadlocked

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class AdditionalsChart
include ActiveRecord::Sanitization
include Redmine::I18n
@ -15,7 +17,7 @@ class AdditionalsChart
end
# build return value
def build_chart_data(datasets, options = {})
def build_chart_data(datasets, **options)
cached_labels = labels
data = { datasets: datasets.to_json,
labels: cached_labels.keys,
@ -23,13 +25,13 @@ class AdditionalsChart
required_labels = options.key?(:required_labels) ? options.delete(:required_labels) : 2
data[:valid] = cached_labels.any? && cached_labels.count >= required_labels unless options.key?(:valid)
data[:width] = self::CHART_DEFAULT_WIDTH unless options.key?(:width)
data[:height] = self::CHART_DEFAULT_HEIGHT unless options.key?(:height)
data[:value_link_method] = '_project_issues_path' unless options.key?(:value_link_method)
data[:valid] = cached_labels.any? && cached_labels.count >= required_labels unless options.key? :valid
data[:width] = self::CHART_DEFAULT_WIDTH unless options.key? :width
data[:height] = self::CHART_DEFAULT_HEIGHT unless options.key? :height
data[:value_link_method] = '_project_issues_path' unless options.key? :value_link_method
data[:color_schema] = color_schema
data.merge(options)
data.merge options
end
private
@ -37,7 +39,7 @@ class AdditionalsChart
def build_values_without_gaps(data, gap_value = 0)
values = []
labels.each do |label, _label_id|
values << if data.key?(label)
values << if data.key? label
data[label]
else
gap_value

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class AdditionalsFontAwesome
include Redmine::I18n
@ -9,7 +11,7 @@ class AdditionalsFontAwesome
data = YAML.safe_load(ERB.new(IO.read(File.join(Additionals.plugin_dir, 'config', 'fontawesome_icons.yml'))).result) || {}
icons = {}
data.each do |key, values|
icons[key] = { unicode: values['unicode'], label: values['label'] } if values['styles'].include?(convert_type2style(type))
icons[key] = { unicode: values['unicode'], label: values['label'] } if values['styles'].include? convert_type2style(type)
end
icons
end
@ -48,7 +50,7 @@ class AdditionalsFontAwesome
end
def classes(value)
info = value_info(value)
info = value_info value
return '' if info.blank?
info[:classes]
@ -65,27 +67,27 @@ class AdditionalsFontAwesome
# show only one value as current selected
# (all other options are retrieved by select2
def active_option_for_select(selected)
info = value_info(selected, with_details: true)
info = value_info selected, with_details: true
return [] if info.blank?
[[info[:label], selected]]
end
def value_info(value, options = {})
def value_info(value, with_details: false)
return {} if value.blank?
values = value.split('_')
values = value.split '_'
return {} unless values.count == 2
info = { type: values[0].to_sym,
name: "fa-#{values[1]}" }
info[:classes] = "#{info[:type]} #{info[:name]}"
info[:font_weight] = font_weight(info[:type])
info[:font_family] = font_family(info[:type])
info[:font_weight] = font_weight info[:type]
info[:font_family] = font_family info[:type]
if options[:with_details]
info.merge!(load_details(info[:type], values[1]))
if with_details
info.merge! load_details(info[:type], values[1])
return {} if info[:unicode].blank?
end
@ -94,12 +96,12 @@ class AdditionalsFontAwesome
def search_for_select(search, selected = nil)
# could be more then one
selected_store = selected.to_s.split(',')
icons = search_in_type(:far, search, selected_store)
selected_store = selected.to_s.split ','
icons = search_in_type :far, search, selected_store
cnt = icons.count
return icons if cnt >= SEARCH_LIMIT
icons += search_in_type(:fas, search, selected_store, cnt)
icons += search_in_type :fas, search, selected_store, cnt
cnt = icons.count
return icons if cnt >= SEARCH_LIMIT
@ -109,7 +111,7 @@ class AdditionalsFontAwesome
def convert2mermaid(icon)
return if icon.blank?
parts = icon.split('_')
parts = icon.split '_'
return unless parts.count == 2
"#{parts.first}:fa-#{parts.last}"
@ -125,7 +127,7 @@ class AdditionalsFontAwesome
search[0].downcase
elsif search_length.zero? && selected_store.any?
selected = selected_store.first
fa = selected.split('_')
fa = selected.split '_'
search = fa[1][0] if fa.count > 1
search
end
@ -147,7 +149,7 @@ class AdditionalsFontAwesome
end
def load_details(type, name)
return {} unless FONTAWESOME_ICONS.key?(type)
return {} unless FONTAWESOME_ICONS.key? type
values = FONTAWESOME_ICONS[type][name]
return {} if values.blank?

View File

@ -1,9 +1,11 @@
# frozen_string_literal: true
class AdditionalsImport < Import
class_attribute :import_class
# Returns the objects that were imported
def saved_objects
object_ids = saved_items.pluck(:obj_id)
object_ids = saved_items.pluck :obj_id
import_class.where(id: object_ids).order(:id)
end
@ -24,17 +26,17 @@ class AdditionalsImport < Import
object.custom_field_values.each_with_object({}) do |v, h|
value = case v.custom_field.field_format
when 'date'
row_date(row, "cf_#{v.custom_field.id}")
row_date row, "cf_#{v.custom_field.id}"
else
row_value(row, "cf_#{v.custom_field.id}")
row_value row, "cf_#{v.custom_field.id}"
end
next unless value
h[v.custom_field.id.to_s] =
if value.is_a? Array
value.map { |val| v.custom_field.value_from_keyword(val.strip, object) }.flatten!&.compact
value.map { |val| v.custom_field.value_from_keyword val.strip, object }.flatten!&.compact
else
v.custom_field.value_from_keyword(value, object)
v.custom_field.value_from_keyword value, object
end
end
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class AdditionalsInfo
include Redmine::I18n
@ -43,11 +45,11 @@ class AdditionalsInfo
days = (secs / 86_400).round
end
if days >= 1
"#{days} #{l(:days, count: days)}"
"#{days} #{l :days, count: days}"
elsif hours >= 1
"#{hours} #{l(:hours, count: hours)}"
"#{hours} #{l :hours, count: hours}"
else
"#{min} #{l(:minutes, count: min)}"
"#{min} #{l :minutes, count: min}"
end
else
# this should be work on macOS
@ -61,7 +63,7 @@ class AdditionalsInfo
end
else
days = `uptime | awk '{print $3}'`.to_i.round
"#{days} #{l(:days, count: days)}"
"#{days} #{l :days, count: days}"
end
end
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class AdditionalsJournal
class << self
def save_journal_history(journal, prop_key, ids_old, ids)
@ -6,7 +8,7 @@ class AdditionalsJournal
ids_all.each do |id|
next if ids_old.include?(id) && ids.include?(id)
if ids.include?(id)
if ids.include? id
value = id
old_value = nil
else
@ -29,10 +31,10 @@ class AdditionalsJournal
new_entries = entries.select { |entry| entry.id.blank? }
return true if new_entries.blank?
new_entries.map! { |entry| entry.send(entry_id) }
new_entries.map! { |entry| entry.send entry_id }
return false if new_entries.count != new_entries.uniq.count
old_entries.map! { |entry| entry.send(entry_id) }
old_entries.map! { |entry| entry.send entry_id }
return false unless (old_entries & new_entries).count.zero?
true

View File

@ -1,75 +1,81 @@
# frozen_string_literal: true
class AdditionalsMacro
def self.all(options = {})
all = Redmine::WikiFormatting::Macros.available_macros
macros = {}
macro_list = []
class << self
def all(only_names: false, filtered: [], controller_only: nil)
all = Redmine::WikiFormatting::Macros.available_macros
macros = {}
macro_list = []
# needs to run every request (for each user once)
permissions = build_permissions(options)
# needs to run every request (for each user once)
permissions = build_permissions controller_only: controller_only
if options[:filtered].present?
options[:filtered] << 'hello_world'
else
options[:filtered] = ['hello_world']
if filtered.present?
filtered << 'hello_world'
else
filtered = ['hello_world']
end
all.each do |macro, macro_options|
next if filtered.include? macro.to_s
next unless macro_allowed macro, permissions
macro_list << macro.to_s
macros[macro] = macro_options
end
if only_names
macro_list.sort
else
macros.sort
end
end
all.each do |macro, macro_options|
next if options[:filtered].include?(macro.to_s)
next unless macro_allowed(macro, permissions)
private
macro_list << macro.to_s
macros[macro] = macro_options
def macro_allowed(macro, permissions)
permissions.each do |permission|
next if permission[:list].exclude? macro
return false unless permission[:access]
end
true
end
if options[:only_names]
macro_list.sort
else
macros.sort
end
end
def build_permissions(controller_only: nil)
gpermission = []
macro_permissions.each do |permission|
permission[:access] = if controller_only &&
permission[:controller].present? &&
controller_only.to_sym != permission[:controller]
false
else
User.current.allowed_to? permission[:permission], nil, global: true
end
gpermission << permission
end
def self.macro_allowed(macro, permissions)
permissions.each do |permission|
next if permission[:list].exclude?(macro)
return false unless permission[:access]
gpermission
end
true
end
def self.build_permissions(options)
gpermission = []
macro_permissions.each do |permission|
permission[:access] = if options[:controller_only] &&
permission[:controller].present? &&
options[:controller_only].to_sym != permission[:controller]
false
else
User.current.allowed_to?(permission[:permission], nil, global: true)
end
gpermission << permission
def macro_permissions
[{ list: %i[issue issue_name_link],
permission: :view_issues },
{ list: %i[password password_query password_tag password_tag_count],
permission: :view_passwords },
{ list: %i[contact deal contact_avatar contact_note contact_plain],
permission: :view_contacts },
{ list: %i[db db_query db_tag db_tag_count],
permission: :view_db_entries },
{ list: %i[child_pages last_updated_at last_updated_by lastupdated_at lastupdated_by
new_page recently_updated recent comments comment_form tags taggedpages tagcloud
show_count count vote show_vote terms_accept terms_reject],
permission: :view_wiki_pages,
controller: :wiki },
{ list: %i[mail send_file],
permission: :view_helpdesk_tickets },
{ list: %i[kb article_id article category],
permission: :view_kb_articles }]
end
gpermission
end
def self.macro_permissions
[{ list: %i[issue issue_name_link],
permission: :view_issues },
{ list: %i[password password_query password_tag password_tag_count],
permission: :view_passwords },
{ list: %i[contact deal contact_avatar contact_note contact_plain],
permission: :view_contacts },
{ list: %i[db db_query db_tag db_tag_count],
permission: :view_db_entries },
{ list: %i[child_pages last_updated_at last_updated_by lastupdated_at lastupdated_by
new_page recently_updated recent comments comment_form tags taggedpages tagcloud
show_count count vote show_vote terms_accept terms_reject],
permission: :view_wiki_pages,
controller: :wiki },
{ list: %i[mail send_file],
permission: :view_helpdesk_tickets },
{ list: %i[kb article_id article category],
permission: :view_kb_articles }]
end
end

View File

@ -1,17 +1,19 @@
# frozen_string_literal: true
module AdditionalsQuery
def column_with_prefix?(prefix)
columns.detect { |c| c.name.to_s.start_with?("#{prefix}.") }.present?
columns.detect { |c| c.name.to_s.start_with? "#{prefix}." }.present?
end
def available_column_names(options = {})
def available_column_names(only_sortable: false)
names = available_columns.dup
names.flatten!
names.select! { |col| col.sortable.present? } if options[:only_sortable]
names.select! { |col| col.sortable.present? } if only_sortable
names.map(&:name)
end
def sql_for_enabled_module(table_field, module_names)
module_names = Array(module_names)
module_names = Array module_names
sql = []
module_names.each do |module_name|
@ -19,7 +21,7 @@ module AdditionalsQuery
" AND #{EnabledModule.table_name}.name='#{module_name}')"
end
sql.join(' AND ')
sql.join ' AND '
end
def fix_sql_for_text_field(field, operator, value, table_name = nil, target_field = nil)
@ -27,15 +29,15 @@ module AdditionalsQuery
target_field = field if target_field.blank?
sql = []
sql << "(#{sql_for_field(field, operator, value, table_name, target_field)})"
sql << "(#{sql_for_field field, operator, value, table_name, target_field})"
sql << "#{table_name}.#{target_field} != ''" if operator == '*'
sql.join(' AND ')
sql.join ' AND '
end
def initialize_ids_filter(options = {})
if options[:label]
add_available_filter 'ids', type: :integer, label: options[:label]
def initialize_ids_filter(label: nil)
if label
add_available_filter 'ids', type: :integer, label: label
else
add_available_filter 'ids', type: :integer, name: '#'
end
@ -82,35 +84,29 @@ module AdditionalsQuery
values: -> { project_statuses_values }
end
def initialize_project_filter(options = {})
if project.nil? || options[:always]
add_available_filter 'project_id', order: options[:position],
def initialize_project_filter(always: false, position: nil)
if project.nil? || always
add_available_filter 'project_id', order: position,
type: :list,
values: -> { project_values }
end
return if project.nil? || project.leaf? || subproject_values.empty?
add_available_filter 'subproject_id', order: options[:position],
add_available_filter 'subproject_id', order: position,
type: :list_subprojects,
values: -> { subproject_values }
end
def initialize_created_filter(options = {})
add_available_filter 'created_on', order: options[:position],
def initialize_created_filter(position: nil, label: nil)
add_available_filter 'created_on', order: position,
type: :date_past,
label: options[:label].presence
label: label
end
def initialize_updated_filter(options = {})
add_available_filter 'updated_on', order: options[:position],
def initialize_updated_filter(position: nil, label: nil)
add_available_filter 'updated_on', order: position,
type: :date_past,
label: options[:label].presence
end
def initialize_tags_filter(options = {})
add_available_filter 'tags', order: options[:position],
type: :list_optional,
values: -> { tag_values(project) }
label: label
end
def initialize_approved_filter
@ -123,38 +119,26 @@ module AdditionalsQuery
label: :field_approved
end
def initialize_author_filter(options = {})
add_available_filter 'author_id', order: options[:position],
def initialize_author_filter(position: nil)
add_available_filter 'author_id', order: position,
type: :list_optional,
values: -> { author_values }
end
def initialize_assignee_filter(options = {})
add_available_filter 'assigned_to_id', order: options[:position],
def initialize_assignee_filter(position: nil)
add_available_filter 'assigned_to_id', order: position,
type: :list_optional,
values: -> { assigned_to_all_values }
end
def initialize_watcher_filter(options = {})
def initialize_watcher_filter(position: nil)
return unless User.current.logged?
add_available_filter 'watcher_id', order: options[:position],
add_available_filter 'watcher_id', order: position,
type: :list,
values: -> { watcher_values_for_manage_public_queries }
end
def tag_values(project)
values = if project
queried_class.available_tags project: project.id
else
queried_class.available_tags
end
return [] if values.blank?
values.collect { |t| [t.name, t.name] }
end
# issue independend values. Use assigned_to_values from Redmine, if you want it only for issues
def assigned_to_all_values
assigned_to_values = []
@ -168,7 +152,7 @@ module AdditionalsQuery
# and with users (not groups)
def watcher_values_for_manage_public_queries
watcher_values = [["<< #{l :label_me} >>", 'me']]
watcher_values += users.collect { |s| [s.name, s.id.to_s] } if User.current.allowed_to?(:manage_public_queries, project, global: true)
watcher_values += users.collect { |s| [s.name, s.id.to_s] } if User.current.allowed_to? :manage_public_queries, project, global: true
watcher_values
end
@ -177,16 +161,12 @@ module AdditionalsQuery
db_table = Watcher.table_name
"#{queried_table_name}.id #{operator == '=' ? 'IN' : 'NOT IN'}" \
" (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='#{watchable_type}' AND" \
" #{sql_for_field field, '=', value, db_table, 'user_id'})"
end
def sql_for_tags_field(field, _operator, value)
AdditionalTags.sql_for_tags_field queried_class, operator_for(field), value
" (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='#{watchable_type}' AND" \
" #{sql_for_field field, '=', value, db_table, 'user_id'})"
end
def sql_for_is_private_field(_field, operator, value)
if bool_operator(operator, value)
if bool_operator operator, value
return '' if value.count > 1
"#{queried_table_name}.is_private = #{self.class.connection.quoted_true}"
@ -215,10 +195,10 @@ module AdditionalsQuery
raise ::Query::StatementInvalid, e.message
end
def results_scope(options = {})
def results_scope(**options)
order_option = [group_by_sort_order, (options[:order] || sort_clause)].flatten!.to_a.reject(&:blank?)
objects_scope(options)
objects_scope(**options.except(:order, :limit, :offset))
.order(order_option)
.joins(joins_for_order_statement(order_option.join(',')))
.limit(options[:limit])

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class Dashboard < ActiveRecord::Base
include Redmine::I18n
include Redmine::SafeAttributes
@ -22,10 +24,10 @@ class Dashboard < ActiveRecord::Base
VISIBILITY_ROLES = 1
VISIBILITY_PUBLIC = 2
scope :by_project, (->(project_id) { where(project_id: project_id) if project_id.present? })
scope :sorted, (-> { order("#{Dashboard.table_name}.name") })
scope :welcome_only, (-> { where(dashboard_type: DashboardContentWelcome::TYPE_NAME) })
scope :project_only, (-> { where(dashboard_type: DashboardContentProject::TYPE_NAME) })
scope :by_project, (->(project_id) { where project_id: project_id if project_id.present? })
scope :sorted, (-> { order "#{Dashboard.table_name}.name" })
scope :welcome_only, (-> { where dashboard_type: DashboardContentWelcome::TYPE_NAME })
scope :project_only, (-> { where dashboard_type: DashboardContentProject::TYPE_NAME })
safe_attributes 'name', 'description', 'enable_sidebar',
'always_expose', 'project_id', 'author_id',
@ -47,9 +49,11 @@ class Dashboard < ActiveRecord::Base
safe_attributes 'system_default',
if: (lambda do |dashboard, user|
user.allowed_to?(:set_system_dashboards, dashboard.project, global: true)
user.allowed_to? :set_system_dashboards, dashboard.project, global: true
end)
before_validation :strip_whitespace
before_save :dashboard_type_check, :visibility_check, :set_options_hash, :clear_unused_block_settings
before_destroy :check_destroy_system_default
@ -73,10 +77,10 @@ class Dashboard < ActiveRecord::Base
def default(dashboard_type, project = nil, user = User.current)
recently_id = User.current.pref.recently_used_dashboard dashboard_type, project
scope = where(dashboard_type: dashboard_type)
scope = where dashboard_type: dashboard_type
scope = scope.where(project_id: project.id).or(scope.where(project_id: nil)) if project.present?
dashboard = scope.visible.find_by(id: recently_id) if recently_id.present?
dashboard = scope.visible.find_by id: recently_id if recently_id.present?
if dashboard.blank?
scope = scope.where(system_default: true).or(scope.where(author_id: user.id))
@ -86,7 +90,7 @@ class Dashboard < ActiveRecord::Base
Rails.logger.debug 'default cleanup required'
# Remove invalid recently_id
if project.present?
User.current.pref.recently_used_dashboards[dashboard_type].delete(project.id)
User.current.pref.recently_used_dashboards[dashboard_type].delete project.id
else
User.current.pref.recently_used_dashboards[dashboard_type] = nil
end
@ -101,7 +105,7 @@ class Dashboard < ActiveRecord::Base
["#{table}.name"]
end
def visible(user = User.current, options = {})
def visible(user = User.current, **options)
scope = left_outer_joins :project
scope = scope.where(projects: { id: nil }).or(scope.where(Project.allowed_to_condition(user, :view_project, options)))
@ -109,14 +113,14 @@ class Dashboard < ActiveRecord::Base
scope.where.not(visibility: VISIBILITY_PRIVATE).or(scope.where(author_id: user.id))
elsif user.memberships.any?
scope.where("#{table_name}.visibility = ?" \
" OR (#{table_name}.visibility = ? AND #{table_name}.id IN (" \
"SELECT DISTINCT d.id FROM #{table_name} d" \
" INNER JOIN #{table_name_prefix}dashboard_roles#{table_name_suffix} dr ON dr.dashboard_id = d.id" \
" INNER JOIN #{MemberRole.table_name} mr ON mr.role_id = dr.role_id" \
" INNER JOIN #{Member.table_name} m ON m.id = mr.member_id AND m.user_id = ?" \
" INNER JOIN #{Project.table_name} p ON p.id = m.project_id AND p.status <> ?" \
' WHERE d.project_id IS NULL OR d.project_id = m.project_id))' \
" OR #{table_name}.author_id = ?",
" OR (#{table_name}.visibility = ? AND #{table_name}.id IN (" \
"SELECT DISTINCT d.id FROM #{table_name} d" \
" INNER JOIN #{table_name_prefix}dashboard_roles#{table_name_suffix} dr ON dr.dashboard_id = d.id" \
" INNER JOIN #{MemberRole.table_name} mr ON mr.role_id = dr.role_id" \
" INNER JOIN #{Member.table_name} m ON m.id = mr.member_id AND m.user_id = ?" \
" INNER JOIN #{Project.table_name} p ON p.id = m.project_id AND p.status <> ?" \
' WHERE d.project_id IS NULL OR d.project_id = m.project_id))' \
" OR #{table_name}.author_id = ?",
VISIBILITY_PUBLIC,
VISIBILITY_ROLES,
user.id,
@ -152,7 +156,7 @@ class Dashboard < ActiveRecord::Base
super
else
h = (self[:options] || {}).dup
h.update(attr_name => value)
h.update attr_name => value
self[:options] = h
value
end
@ -236,7 +240,7 @@ class Dashboard < ActiveRecord::Base
return if content.groups.exclude?(group) || blocks.blank?
blocks = blocks.map(&:underscore) & layout.values.flatten
blocks.each { |block| remove_block(block) }
blocks.each { |block| remove_block block }
layout[group] = blocks
end
@ -261,7 +265,7 @@ class Dashboard < ActiveRecord::Base
end
def editable?(usr = User.current)
@editable ||= editable_by?(usr)
@editable ||= editable_by? usr
end
def destroyable_by?(usr = User.current)
@ -274,7 +278,7 @@ class Dashboard < ActiveRecord::Base
end
def destroyable?
@destroyable ||= destroyable_by?(User.current)
@destroyable ||= destroyable_by? User.current
end
def to_s
@ -285,7 +289,7 @@ class Dashboard < ActiveRecord::Base
def css_classes(user = User.current)
s = ['dashboard']
s << 'created-by-me' if author_id == user.id
s.join(' ')
s.join ' '
end
def allowed_target_projects(user = User.current)
@ -293,7 +297,7 @@ class Dashboard < ActiveRecord::Base
end
# this is used to get unique cache for blocks
def async_params(block, options, settings = {})
def async_params(block, options, settings)
if block.blank?
msg = 'block is missing for dashboard_async'
Rails.log.error msg
@ -326,7 +330,7 @@ class Dashboard < ActiveRecord::Base
unique_params += options[:unique_params].reject(&:blank?) if options[:unique_params].present?
# Rails.logger.debug "debug async_params for #{block}: unique_params=#{unique_params.inspect}"
config[:unique_key] = Digest::SHA256.hexdigest(unique_params.join('_'))
config[:unique_key] = Digest::SHA256.hexdigest unique_params.join('_')
end
# Rails.logger.debug "debug async_params for #{block}: config=#{config.inspect}"
@ -343,9 +347,13 @@ class Dashboard < ActiveRecord::Base
private
def strip_whitespace
name&.strip!
end
def clear_unused_block_settings
blocks = layout.values.flatten
layout_settings.keep_if { |block, _settings| blocks.include?(block) }
layout_settings.keep_if { |block, _settings| blocks.include? block }
end
def remove_unused_role_relations
@ -391,7 +399,7 @@ class Dashboard < ActiveRecord::Base
.where(dashboard_type: dashboard_type)
.where.not(id: id)
scope = scope.where(project: project) if dashboard_type == DashboardContentProject::TYPE_NAME
scope = scope.where project: project if dashboard_type == DashboardContentProject::TYPE_NAME
scope.update_all system_default: false
end
@ -409,22 +417,22 @@ class Dashboard < ActiveRecord::Base
end
def validate_visibility
errors.add(:visibility, :must_be_for_everyone) if system_default? && visibility != VISIBILITY_PUBLIC
errors.add :visibility, :must_be_for_everyone if system_default? && visibility != VISIBILITY_PUBLIC
end
def validate_name
return if name.blank?
scope = self.class.visible.where(name: name)
scope = self.class.visible.where name: name
if dashboard_type == DashboardContentProject::TYPE_NAME
scope = scope.project_only
scope = scope.where project_id: project_id
scope = scope.or(scope.where(project_id: nil)) if project_id.present?
scope = scope.or scope.where(project_id: nil) if project_id.present?
else
scope = scope.welcome_only
end
scope = scope.where.not(id: id) unless new_record?
errors.add(:name, :name_not_unique) if scope.count.positive?
scope = scope.where.not id: id unless new_record?
errors.add :name, :name_not_unique if scope.count.positive?
end
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class DashboardContent
include Redmine::I18n
@ -100,7 +102,7 @@ class DashboardContent
def find_block(block)
block.to_s =~ /\A(.*?)(__\d+)?\z/
name = Regexp.last_match(1)
name = Regexp.last_match 1
available_blocks.key?(name) ? available_blocks[name].merge(name: name) : nil
end

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
class DashboardContentProject < DashboardContent
TYPE_NAME = 'ProjectDashboard'.freeze
TYPE_NAME = 'ProjectDashboard'
def block_definitions
blocks = super

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
class DashboardContentWelcome < DashboardContent
TYPE_NAME = 'WelcomeDashboard'.freeze
TYPE_NAME = 'WelcomeDashboard'
def block_definitions
blocks = super

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class DashboardRole < ActiveRecord::Base
include Redmine::SafeAttributes

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
class QueryRelationsColumn < QueryColumn
# NOTE: used for CSV and PDF export
def value_object(object)
(object.send name).map(&:name).join "#{Query.additional_csv_separator} "
end
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
Deface::Override.new virtual_path: 'admin/info',
name: 'add-system_info',
insert_after: 'table.list',

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
unless Redmine::Plugin.installed? 'redmine_servicedesk'
if defined?(CONTACTS_VERSION_TYPE) && CONTACTS_VERSION_TYPE == 'PRO version'
Deface::Override.new virtual_path: 'contacts/_form',

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
Deface::Override.new virtual_path: 'custom_fields/formats/_text',
name: 'custom_fields-formats-text',
replace: 'erb[silent]:contains(\'if @custom_field.class.name == "IssueCustomField"\')',

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
Deface::Override.new virtual_path: 'issues/_edit',
name: 'edit-issue-permission',
replace: 'erb[silent]:contains("User.current.allowed_to?(:log_time, @project)")',

View File

@ -1,5 +1,9 @@
Deface::Override.new virtual_path: 'issues/_list',
name: 'list-issue-back-url',
replace: 'erb[loud]:contains("hidden_field_tag \'back_url\'")',
original: '6652d55078bb57ac4614e456b01f8a203b8096ec',
text: '<%= query_list_back_url_tag @project %>'
# frozen_string_literal: true
unless Redmine::Plugin.installed? 'redmine_reporting'
Deface::Override.new virtual_path: 'issues/_list',
name: 'list-issue-back-url',
replace: 'erb[loud]:contains("hidden_field_tag \'back_url\'")',
original: '6652d55078bb57ac4614e456b01f8a203b8096ec',
text: '<%= query_list_back_url_tag @project %>'
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
Deface::Override.new virtual_path: 'issues/_action_menu',
name: 'show-issue-log-time',
replace: 'erb[loud]:contains("User.current.allowed_to?(:log_time, @project)")',

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
if Redmine::VERSION.to_s >= '4.3' || Redmine::VERSION.to_s >= '4.2' && Redmine::VERSION.to_s.include?('devel')
Deface::Override.new virtual_path: 'projects/settings/_issues',
name: 'add-project-issue-settings',
insert_before: 'div.box.tabular',
original: 'f14b1e325de2e0de0e81443716705547a6c8471f',
text: '<%= call_hook :view_projects_issue_settings, f: f, project: @project %>'
else
Deface::Override.new virtual_path: 'projects/settings/_issues',
name: 'add-project-issue-settings',
insert_before: 'div.box.tabular',
original: '468bd73ed808b51fc5589f6c9c23e93a5b7b787a',
text: '<%= call_hook :view_projects_issue_settings, f: f, project: @project %>'
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
Deface::Override.new virtual_path: 'reports/_simple',
name: 'report-simple-user-scope',
insert_before: 'erb[silent]:contains("rows.empty?")',

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
Deface::Override.new virtual_path: 'roles/_form',
name: 'roles-form-hide',
insert_before: 'p.manage_members_shown',

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
unless Redmine::Plugin.installed? 'redmine_hrm'
if Redmine::VERSION.to_s >= '4.2'
Deface::Override.new virtual_path: 'users/show',

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
Deface::Override.new virtual_path: 'wiki/edit',
name: 'wiki-edit-bottom',
insert_before: 'fieldset',

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
Deface::Override.new virtual_path: 'wiki/show',
name: 'wiki-show-bottom',
insert_before: 'p.wiki-update-info',

View File

@ -1,35 +1,37 @@
div id="#{export_format}-export-options" style="display: none"
h3.title = l(:label_export_options, export_format: export_format.upcase)
div id="#{export_format}-export-options" style="display: none;"
h3.title = l :label_export_options, export_format: export_format.upcase
= form_tag(url, method: :get, id: "#{export_format}-export-form") do
= query_as_hidden_field_tags @query
= form_tag url, method: :get, id: "#{export_format}-export-form" do
= query_as_hidden_field_tags query
- if defined?(selected_columns_only) && selected_columns_only
= hidden_field_tag 'c[]', ''
= l(:description_selected_columns)
- else
p
label
= radio_button_tag 'c[]', '', true
= l(:description_selected_columns)
= l :description_selected_columns
br
label
= radio_button_tag 'c[]', 'all_inline'
= l(:description_all_columns)
= l :description_all_columns
hr
- if @query.available_filters.key?('description')
p
label
= check_box_tag 'c[]', 'description', @query.has_column?(:description)
= l(:field_description)
- if defined?(with_last_notes) && with_last_notes
- if query.available_block_columns.any?
fieldset#csv-export-block-columns
legend = toggle_checkboxes_link '#csv-export-block-columns input[type=checkbox]'
- query.available_block_columns.each do |column|
label
= check_box_tag 'c[]', 'last_notes', @query.has_column?(:last_notes)
= l(:label_last_notes)
= check_box_tag 'c[]', column.name, query.has_column?(column), id: nil
= column.caption
= export_csv_encoding_select_tag
- if @issue_count && @issue_count > Setting.issues_export_limit.to_i || \
@query_count && @query_count > Setting.issues_export_limit.to_i
p.icon.icon-warning
= @issue_count ? l(:setting_issues_export_limit) : l(:setting_export_limit)
' :
= Setting.issues_export_limit.to_i
p.buttons
= submit_tag l(:button_export),
name: nil,

View File

@ -1,18 +1,18 @@
fieldset.box
legend = l(:additionals_query_list_defaults)
legend = l :additionals_query_list_defaults
- setting_name_columns = "#{query_type}_list_defaults"
- query = query_class.new(@settings[setting_name_columns.to_sym])
- query = query_class.new @settings[setting_name_columns.to_sym]
.default-query-settings-label
= render_query_columns_selection(query, name: "settings[#{setting_name_columns}][column_names]")
= render_query_columns_selection query, name: "settings[#{setting_name_columns}][column_names]"
- columns = query_class.new.available_totalable_columns
- if columns.count.positive?
fieldset.box
legend = l(:additionals_query_list_default_totals)
legend = l :additionals_query_list_default_totals
.default-query-settings-totals
- setting_name_totals = "#{query_type}_list_default_totals"
= hidden_field_tag("settings[#{setting_name_totals}][]", '')
= hidden_field_tag "settings[#{setting_name_totals}][]", ''
- columns.each do |s|
label.inline
- value = @settings[setting_name_totals.to_sym].present? ? @settings[setting_name_totals.to_sym].include?(s.name.to_s) : false

View File

@ -1,47 +1,47 @@
fieldset.settings
legend = l(:label_content_plural)
legend = l :label_content_plural
p
= additionals_settings_textarea :account_login_bottom
em.info
= l(:account_login_info)
= l :account_login_info
p
= additionals_settings_textarea :global_sidebar
em.info
= l(:global_sidebar_info)
= l :global_sidebar_info
p
= additionals_settings_textarea :global_footer
em.info
= l(:global_footer_info)
= l :global_footer_info
fieldset.settings
legend = l(:label_settings)
legend = l :label_settings
p
= additionals_settings_checkbox :open_external_urls
em.info
= t(:open_external_urls_info)
= t :open_external_urls_info
p
= additionals_settings_checkbox :add_go_to_top
em.info
= t(:add_go_to_top_info)
= t :add_go_to_top_info
p
= additionals_settings_checkbox :legacy_smiley_support
em.info
= t(:legacy_smiley_support_info_html)
= t :legacy_smiley_support_info_html
fieldset.settings
legend = l(:label_disabled_modules)
legend = l :label_disabled_modules
p
= tag.label l(:label_disabled_modules)
= hidden_field_tag('settings[disabled_modules][]', '')
= hidden_field_tag 'settings[disabled_modules][]', ''
- Redmine::AccessControl.available_project_modules_all.sort.each do |m|
label.block
- value = @settings[:disabled_modules].present? ? @settings[:disabled_modules].include?(m.to_s) : false
= check_box_tag('settings[disabled_modules][]', m, value, id: nil)
= l_or_humanize(m, prefix: 'project_module_')
= check_box_tag 'settings[disabled_modules][]', m, value, id: nil
= l_or_humanize m, prefix: 'project_module_'
br
em.info
= l(:disabled_modules_info)
= l :disabled_modules_info

View File

@ -38,7 +38,7 @@ p
options_for_select(rule_status.collect { |column| [column.name, column.id] },
@settings[:issue_status_y]),
multiple: false, style: 'width:150px; vertical-align: top'
em.info = t(:rule_issue_status_change_info)
em.info = t :rule_issue_status_change_info
br
br
@ -51,7 +51,7 @@ p
options_for_select(rule_status.collect { |column| [column.name, column.id] },
@settings[:issue_assign_to_x]),
multiple: true, size: 6, style: 'width:150px'
em.info = t(:rule_issue_current_user_status_info_html)
em.info = t :rule_issue_current_user_status_info_html
br
br
@ -77,7 +77,7 @@ br
p
= additionals_settings_checkbox :issue_timelog_required
span[style="vertical-align: top; margin-left: 15px;"]
= l(:label_tracker_plural)
= l :label_tracker_plural
| :
= select_tag 'settings[issue_timelog_required_tracker]',
options_for_select(Tracker.all.sorted.collect { |column| [column.name, column.id] },

View File

@ -1,15 +1,15 @@
em.info
= l(:hidden_macros_in_toolbar_info)
= l :hidden_macros_in_toolbar_info
br
p
= tag.label l(:label_hidden_macros_in_toolbar)
= hidden_field_tag('settings[hidden_macros_in_toolbar][]', '')
= hidden_field_tag 'settings[hidden_macros_in_toolbar][]', ''
- @available_macros = AdditionalsMacro.all(only_names: true).each do |m|
label.block
- value = @settings[:hidden_macros_in_toolbar].present? ? @settings[:hidden_macros_in_toolbar].include?(m) : false
= check_box_tag('settings[hidden_macros_in_toolbar][]', m, value, id: nil)
= check_box_tag 'settings[hidden_macros_in_toolbar][]', m, value, id: nil
= m
br

View File

@ -20,14 +20,14 @@ h3 = l :label_custom_menu_items
p
label = h l(:label_permissions)
- permission_field = "custom_menu#{i}_roles"
- menu_roles = Struct.new(:id, :name)
- menu_roles = Struct.new :id, :name
= select_tag("settings[#{permission_field}]",
options_from_collection_for_select(Role.sorted.collect { |m| menu_roles.new(m.id, m.name) },
options_from_collection_for_select(Role.sorted.collect { |m| menu_roles.new m.id, m.name },
:id,
:name,
@settings[permission_field]),
multiple: true, style: 'height: 100px;')
em.info = l(:menu_roles_info)
em.info = l :menu_roles_info
br

View File

@ -1,5 +1,5 @@
p
= additionals_settings_textfield :google_maps_api_key, size: 60
em.info = t(:google_maps_embed_api_html)
em.info = t :google_maps_embed_api_html
= call_hook :additionals_settings_web_apis, settings: @settings

View File

@ -1,23 +1,23 @@
em.info = t(:top_wiki_help)
em.info = t :top_wiki_help
br
fieldset.settings
legend = l(:label_content_plural)
legend = l :label_content_plural
p
= additionals_settings_textarea :global_wiki_sidebar
em.info
= l(:global_wiki_sidebar_info)
= l :global_wiki_sidebar_info
fieldset.settings
legend = l(:label_pdf_wiki_settings)
legend = l :label_pdf_wiki_settings
p
= additionals_settings_checkbox :wiki_pdf_remove_title
em.info
= l(:wiki_pdf_remove_title_info)
= l :wiki_pdf_remove_title_info
p
= additionals_settings_checkbox :wiki_pdf_remove_attachments
em.info
= l(:wiki_pdf_remove_attachments_info)
= l :wiki_pdf_remove_attachments_info

View File

@ -1,6 +1,6 @@
= call_hook :view_dashboard_top, dashboard: dashboard, project: @project
#my-page.splitcontent class="#{dashboard_css_classes(dashboard)}"
#my-page.splitcontent class="#{dashboard_css_classes dashboard}"
- dashboard.available_groups.each do |group|
.block-receiver id="list-#{group}" class="splitcontent#{group}"
= render_dashboard_blocks dashboard.layout[group], dashboard

View File

@ -12,6 +12,7 @@
= hidden_field_tag 'dashboard[dashboard_type]', @dashboard.dashboard_type if @dashboard.new_record?
- if @project && @allowed_projects.present? && @allowed_projects.count > 1
p
= hidden_field_tag 'dashboard[content_project_id]', @project&.id
= f.select :project_id,
project_tree_options_for_select(@allowed_projects,
selected: @dashboard.project,
@ -19,7 +20,7 @@
{},
disabled: !@dashboard.project_id_can_change?
em.info
= l(:info_dashboard_project_select)
= l :info_dashboard_project_select
- else
= hidden_field_tag 'dashboard[project_id]', @project&.id
@ -27,19 +28,19 @@
User.current.allowed_to?(:set_system_dashboards, @project, global: true)
p
label = l(:field_visible)
label = l :field_visible
label.block
= radio_button 'dashboard', 'visibility', Dashboard::VISIBILITY_PRIVATE
'
= l(:label_visibility_private)
= l :label_visibility_private
label.block
= radio_button 'dashboard', 'visibility', Dashboard::VISIBILITY_PUBLIC
'
= l(:label_visibility_public)
= l :label_visibility_public
label.block
= radio_button 'dashboard', 'visibility', Dashboard::VISIBILITY_ROLES
'
= l(:label_visibility_roles)
= l :label_visibility_roles
' :
- Role.givable.sorted.each do |role|
label.block.role-visibility

View File

@ -11,7 +11,7 @@ h3 = block_definition[:label]
= l :field_homepage
' :
= link_to_if uri_with_safe_scheme?(@project.homepage), @project.homepage, @project.homepage, class: 'external'
- render_custom_field_values(@project) do |custom_field, formatted|
- render_custom_field_values @project do |custom_field, formatted|
li class="#{custom_field.css_classes}"
span.label
= custom_field.name

View File

@ -1,6 +1,6 @@
- dashboard_async_cache dashboard, block, async, settings do
- query = klass.visible.find_by(id: settings[:query_id])
- query = klass.visible.find_by id: settings[:query_id]
- if query
ruby:
query.project = @project if query_block[:with_project]

View File

@ -1,4 +1,4 @@
h2 = l(:button_dashboard_edit)
h2 = l :button_dashboard_edit
= labelled_form_for :dashboard,
@dashboard,
html: { multipart: true, id: 'dashboard-form' } do |f|

View File

@ -1,4 +1,4 @@
h2 = l(:label_new_additional_dashboard)
h2 = l :label_new_additional_dashboard
= labelled_form_for :dashboard,
@dashboard,
url: { action: 'create', project_id: @project },

View File

@ -1,5 +1,5 @@
- if show_issue_change_author?(issue) && issue.safe_attribute?('author_id')
- author_options = author_options_for_select(issue.project, issue)
- author_options = author_options_for_select issue.project, issue
- if author_options.present?
p#change_author
= form.label_for_field :author_id

View File

@ -1,7 +1,7 @@
- if @project && User.current.allowed_to?(:edit_issue_author, @project)
- author_options = author_options_for_select(@project)
- author_options = author_options_for_select @project
- if author_options.present?
p#change_author
= label_tag('issue[author_id]', l(:field_author))
= select_tag('issue[author_id]',
tag.option(l(:label_no_change_option), value: '') + author_options)
= label_tag 'issue[author_id]', l(:field_author)
= select_tag 'issue[author_id]',
tag.option(l(:label_no_change_option), value: '') + author_options

View File

@ -1,3 +1,3 @@
- if @issue.new_ticket_message.present?
.nodata.nodata-left
.new-ticket-message.nodata.nodata-left
= textilizable @issue, :new_ticket_message, inline_attachments: false

View File

@ -0,0 +1,45 @@
fieldset.box.tabular
legend
= l :label_new_ticket_message
- if User.current.admin?
.contextual
= link_to l(:label_administration), plugin_settings_path(id: 'additionals', tab: 'rules'), class: 'icon icon-settings'
p
= f.select :enable_new_ticket_message,
options_for_select({ l(:label_system_setting) => '1',
l(:label_disabled) => '0',
l(:label_project_setting) => '2' }, project.enable_new_ticket_message.to_s)
p#project-ticket-message style="#{'display: none' if project.enable_new_ticket_message != 2}"
= f.text_area :new_ticket_message,
rows: addtionals_textarea_cols(project.new_ticket_message),
label: l(:label_additionals_message)
p#system-ticket-message style="#{'display: none' if project.enable_new_ticket_message != 1}"
= additionals_settings_textarea :new_ticket_message_system,
value: Additionals.setting(:new_ticket_message).to_s,
label: l(:label_additionals_message),
for_project: true,
disabled: true
javascript:
$(function() {
$("#project_enable_new_ticket_message").change(function () {
if ( this.value == '1')
{
$("#system-ticket-message").show();
$("#project-ticket-message").hide();
}
else if ( this.value == '2')
{
$("#system-ticket-message").hide();
$("#project-ticket-message").show();
}
else
{
$("#system-ticket-message").hide();
$("#project-ticket-message").hide();
}
});
});

View File

@ -16,7 +16,7 @@
= actions_dropdown do
- if User.current.allowed_to? :add_subprojects, @project
= link_to l(:label_subproject_new), new_project_path(parent_id: @project), class: 'icon icon-add'
- if User.current.allowed_to?(:close_project, @project)
- if User.current.allowed_to? :close_project, @project
- if @project.active?
= link_to l(:button_close),
close_project_path(@project),
@ -46,14 +46,13 @@
class: 'icon icon-add new-additionals-dashboard'
- if @dashboard&.destroyable?
= delete_dashboard_link project_dashboard_path(@project, @dashboard),
class: 'icon icon-del'
= delete_dashboard_link project_dashboard_path(@project, @dashboard)
= sidebar_action_toggle @dashboard_sidebar, @dashboard, @project
= render_dashboard_actionlist @dashboard, @project unless @dashboard_sidebar
= call_hook :view_project_actions_dropdown, project: @project
- if User.current.allowed_to?(:edit_project, @project)
- if User.current.allowed_to? :edit_project, @project
= link_to_if_authorized l(:label_settings),
{ controller: 'projects', action: 'settings', id: @project },
class: 'icon icon-settings'
@ -63,7 +62,7 @@ h2 = project_overview_name @project, @dashboard
- unless @project.active?
p.warning
span.icon.icon-lock
= l(:text_project_closed)
= l :text_project_closed
= render partial: 'common/dashboard', locals: { dashboard: @dashboard }

View File

@ -1,3 +0,0 @@
- if @query.description?
.query-description
= textilizable @query, :description

View File

@ -9,5 +9,5 @@ tr.group.open
span.badge.badge-count.count = group_count
'
span.totals = group_totals
= link_to_function("#{l :button_collapse_all}/#{l :button_expand_all}",
'toggleAllRowGroups(this)', class: 'toggle-all')
= link_to_function "#{l :button_collapse_all}/#{l :button_expand_all}",
'toggleAllRowGroups(this)', class: 'toggle-all'

View File

@ -1,7 +1,7 @@
p
= f.check_box :hide, disabled: @role.users_visibility != 'members_of_visible_projects'
em.info
= t(:info_hidden_roles_html)
= t :info_hidden_roles_html
javascript:
$(function() {

View File

@ -18,8 +18,7 @@
new_dashboard_path,
class: 'icon icon-add new-additionals-dashboard'
- if @dashboard&.destroyable?
= delete_dashboard_link dashboard_path(@dashboard),
class: 'icon icon-del'
= delete_dashboard_link dashboard_path(@dashboard)
= sidebar_action_toggle @dashboard_sidebar, @dashboard
= render_dashboard_actionlist @dashboard unless @dashboard_sidebar

View File

@ -3,7 +3,7 @@
h3 = list_title
table.list.projects
- project_tree(@projects, init_level: false) do |project, level|
- project_tree @projects, init_level: false do |project, level|
tr id="project-#{project.id}" class="#{project_list_css_classes project, level}"
td.name
span[style='font-weight: bold;']
@ -13,7 +13,7 @@
= link_to_project project
- if project.homepage?
' :
= link_to(project.homepage, project.homepage, @html_options)
= link_to project.homepage, project.homepage, @html_options
- if with_create_issue && User.current.allowed_to?(:add_issues, project)
= link_to '',
new_project_issue_path(project_id: project),

View File

@ -4,7 +4,7 @@
- users.each do |user|
.user.box class="#{cycle 'odd', 'even'}"
div[style="float: left; display: block; margin-right: 5px;"]
= avatar(user, size: 50)
= avatar user, size: 50
.user.line[style="font-weight: bold;"]
= link_to_user user

View File

@ -16,7 +16,8 @@ jsToolBar.prototype.elements.macros = {
jsToolBar.prototype.macroMenu = function(fn){
var menu = $('<ul style="position:absolute;"></ul>');
for (var i = 0; i < this.macroList.length; i++) {
$('<li></li>').text(this.macroList[i]).appendTo(menu).mousedown(function(){
var macroItem = $('<div></div>').text(this.macroList[i]);
$('<li></li>').html(macroItem).appendTo(menu).mousedown(function(){
fn($(this).text());
});
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -9,8 +9,13 @@ $(function() {
} else {
var mermaidThemeVariables = { 'fontSize': '12px' };
}
mermaid.initialize({
startOnLoad: true,
maxTextSize: 500000,
flowchart:{
useMaxWidth: false
},
theme: mermaidTheme,
themeVariables: mermaidThemeVariables });
});

View File

@ -377,6 +377,24 @@ select.dashboard-block-select {
div.drdn-items a.disabled,
div.drdn-items a.disabled:hover { color: #aaa; }
table.entity-list td.block_column {
color: #777;
font-size: 90%;
padding: 4px 4px 4px 24px;
text-align: left;
white-space: normal;
}
table.entity-list td.block_column span {
font-weight: bold;
display: block;
margin-bottom: 4px;
}
table.entity-list td.block_column pre {
white-space: normal;
}
@media screen and (max-width: 899px) {
.gototop { display: none; }

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -2,11 +2,11 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
<metadata>
Created by FontForge 20201107 at Tue Mar 16 10:15:04 2021
Created by FontForge 20201107 at Wed Aug 4 12:25:29 2021
By Robert Madole
Copyright (c) Font Awesome
</metadata>
<!-- Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><defs>
<!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><defs>
<font id="FontAwesome5Brands-Regular" horiz-adv-x="448" >
<font-face
font-family="Font Awesome 5 Brands Regular"
@ -957,14 +957,6 @@ l19.2002 -19.2002l128 128l-128 128l-51.5 -51.5l77.1006 -76.5l-25.6006 -25.5996l-
<glyph glyph-name="gg-circle" unicode="&#xf261;" horiz-adv-x="512"
d="M257 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM207.5 65.2002l75 75.2002l-77.2002 77.1992l-24.3994 -24.3994l53.0996 -52.9004l-26.5996 -26.5996l-77.2002 77.2002l77.2002 77.1992l11.0996 -11.0996l24.2002 24.2002
l-35.2002 35.3994l-125.7 -125.699zM306.5 67.4004l125.7 125.6l-125.7 125.7l-75 -75l77.2002 -77.2002l24.3994 24.4004l-53.0996 52.8994l26.5 26.5l77.2002 -77.2002l-77.2002 -77.1992l-11.0996 11.0996l-24.1006 -24.4004z" />
<glyph glyph-name="tripadvisor" unicode="&#xf262;" horiz-adv-x="576"
d="M528.91 269.18c28.8652 -26.2666 46.9404 -64.1113 46.9404 -106.176c0 -0.0615234 -0.000976562 -0.12207 -0.000976562 -0.183594h0.0302734c-0.00195312 -79.4414 -64.5479 -143.928 -143.989 -143.928c-37.7363 0 -72.0996 14.5527 -97.7803 38.3477
l-46.1104 -50.1699l-46.1396 50.1992c-25.6064 -23.4443 -59.8145 -37.7793 -97.2334 -37.7793c-79.4268 0 -143.911 64.4844 -143.911 143.911c0 41.8076 17.8662 79.4756 46.374 105.778l-47.0898 51.2402h104.66c52.2266 35.5498 115.938 56.3369 183.822 56.3369
s130.95 -20.7871 183.178 -56.3369h104.34zM144.06 65.4297c53.751 0 97.3906 43.6396 97.3906 97.3906s-43.6396 97.3896 -97.3906 97.3896s-97.3896 -43.6387 -97.3896 -97.3896s43.6387 -97.3906 97.3896 -97.3906zM288 165.63c0 64.0801 46.6104 119.07 108.08 142.59
c-33.2285 13.8467 -70.0527 21.4941 -108.272 21.4941c-38.2207 0 -74.6689 -7.64746 -107.897 -21.4941c61.4697 -23.5098 108.09 -78.5 108.09 -142.59zM431.88 65.4297c53.7568 0.00292969 97.4004 43.6475 97.4004 97.4053c0 53.7598 -43.6455 97.4053 -97.4053 97.4053
c-53.7588 0 -97.4053 -43.6455 -97.4053 -97.4053c0 -53.7578 43.6436 -97.4023 97.4004 -97.4053h0.00976562zM144.06 213.88c0.0175781 0 0.0332031 -0.000976562 0.0507812 -0.000976562c28.1299 0 50.9717 -22.8242 51 -50.9492v-0.109375
c0 -28.1807 -22.8799 -51.0605 -51.0605 -51.0605s-51.0596 22.8799 -51.0596 51.0605s22.8789 51.0596 51.0596 51.0596h0.00976562zM431.88 213.88c28.1807 0 51.0605 -22.8789 51.0605 -51.0596s-22.8799 -51.0605 -51.0605 -51.0605s-51.0596 22.8799 -51.0596 51.0605
s22.8789 51.0596 51.0596 51.0596z" />
<glyph glyph-name="odnoklassniki" unicode="&#xf263;" horiz-adv-x="320"
d="M275.1 114c-27.3994 -17.4004 -65.0996 -24.2998 -90 -26.9004l20.9004 -20.5996l76.2998 -76.2998c27.9004 -28.6006 -17.5 -73.2998 -45.7002 -45.7002c-19.0996 19.4004 -47.0996 47.4004 -76.2998 76.5996l-76.2998 -76.5
c-28.2002 -27.5 -73.5996 17.6006 -45.4004 45.7002c19.4004 19.4004 47.1006 47.4004 76.3008 76.2998l20.5996 20.6006c-24.5996 2.59961 -62.9004 9.09961 -90.5996 26.8994c-32.6006 21 -46.9004 33.3008 -34.3008 59c7.40039 14.6006 27.7002 26.9004 54.6006 5.7002
@ -1241,11 +1233,10 @@ d="M256 440c136.9 0 248 -111.1 248 -248s-111.1 -248 -248 -248s-248 111.1 -248 24
h-116.9v-42.6006h70.1006c-5.2002 -34.2002 -37.5 -53.2998 -70.1006 -53.2998c-43 0 -77.2002 35.5 -77.2002 78.0996c0 42.6006 34.3008 78.1006 77.2002 78.1006c18.1006 0 36.2002 -6.2002 49.4004 -19.1006l33.5996 32.6006
c-22.8994 21.2998 -51.7002 32.2998 -83 32.2998c-68.4375 0 -124 -55.5625 -124 -124s55.5625 -124 124 -124zM415.5 174.2h35.2002v35.5h-35.2002v35.5h-35.5v-35.5h-35.5v-35.5h35.5v-35.5h35.5v35.5z" />
<glyph glyph-name="font-awesome" unicode="&#xf2b4;"
d="M397.8 416c27.5 0 50.2002 -22.7002 50.2002 -50.2002v-347.6c0 -27.5 -22.7002 -50.2002 -50.2002 -50.2002h-347.6c-27.5 0 -50.2002 22.7002 -50.2002 50.2002v347.6c0 27.5 22.7002 50.2002 50.2002 50.2002h347.6zM352.4 131.7h0.0996094v140.3
c0 4.2002 -4.2002 7.7998 -9 7.7998c-6 0 -31.0996 -16.0996 -53.7998 -16.0996c-4.7002 0 -8.90039 0.599609 -13.1006 2.39941c-20.2998 7.7002 -38.1992 13.7002 -60.8994 13.7002c-20.9004 0 -43 -6.5 -61.5 -14.2998
c-1.7998 -1.2002 -3.60059 -1.7998 -5.40039 -2.40039v18.5c8.2998 6 13.1006 15.5 13.1006 26.3008c0 18.5996 -15 33.5 -33.5 33.5c-18.6006 0 -33.5 -15 -33.5 -33.5c0 -10.8008 5.2998 -20.3008 13.0996 -26.3008v-218.6c0 -11.2998 9 -20.2998 20.2998 -20.2998
c8.90039 0 16.7002 5.89941 19.1006 14.2998v1.2002c0.599609 1.2002 0.599609 3 0.599609 4.7998v45.4004c1.2002 0.599609 2.40039 0.599609 3.59961 1.19922c19.7002 8.90039 44.2002 17.3008 67.5 17.3008c32.3008 0 44.8008 -16.7002 71.7002 -16.7002
c19.2002 0 37.1006 6.5 53.7998 13.7002c4.2002 1.7998 7.80078 3.59961 7.80078 7.7998z" />
d="M400 416c26.4922 0 48 -21.5078 48 -48v-352c0 -26.4922 -21.5078 -48 -48 -48h-352c-26.4922 0 -48 21.5078 -48 48v352c0 26.4922 21.5078 48 48 48h352zM336 136v160c-31.5996 -11.2002 -41.2002 -16 -59.7998 -16c-31.4004 0 -43.4004 16 -74.6006 16
c-25.3994 0 -37.3994 -10.4004 -57.5996 -14.4004v6.40039c0 8.83105 -7.16895 16 -16 16s-16 -7.16895 -16 -16v-192c0 -8.83105 7.16895 -16 16 -16s16 7.16895 16 16v153.6c20.2002 4 32.2002 14.4004 57.5996 14.4004c31.4004 0 43.2002 -16 74.6006 -16
c10.2002 0 17.7998 1.40039 27.7998 4.59961v-96c-10 -3.19922 -17.5996 -4.59961 -27.7998 -4.59961c-31.4004 0 -43.4004 16 -74.6006 16c-8.91309 -0.0322266 -17.5195 -1.44336 -25.5996 -4v-32c7.86035 2.58398 16.2559 4.00195 24.9756 4.00195
c0.208008 0 0.416016 0 0.624023 -0.00195312c31.4004 0 43.2002 -16 74.6006 -16c18.5996 0 28.2002 4.7998 59.7998 16z" />
<glyph glyph-name="linode" unicode="&#xf2b8;"
d="M437.4 221.7c0.599609 -2 -8.80078 -66.2998 -9.7002 -72.7998c0 -0.900391 -0.5 -1.7002 -1.10059 -2l-54.5996 -43.7002c-1.09961 -0.900391 -2.59961 -0.900391 -3.7002 0l-20.2998 14l-2.2998 -33.4004c0 -0.899414 -0.200195 -1.7002 -1.10059 -2.2998
l-66.8994 -53.4004c-1.10059 -0.899414 -2.90039 -0.899414 -4 0l-28 23.7002l2 -46c0 -0.899414 -0.200195 -1.7002 -1.10059 -2.2998l-83.6992 -66.9004c-0.600586 -0.299805 -1.10059 -0.599609 -1.7002 -0.599609c-0.900391 0.299805 -1.7002 0.299805 -2.2998 0.900391
@ -1340,12 +1331,11 @@ c5.7002 -3.09961 6.90039 -9.40039 6 -15.0996c-1.09961 -9.7002 -28 -70.9004 -28.8
c3.10059 0 8.30078 -0.900391 7.10059 -10.9004c-1.40039 -9.39941 -35.1006 -72.2998 -38.9004 -87.6992c-4.59961 -20.6006 6.60059 -41.4004 24.9004 -50.6006c11.3994 -5.7002 62.5 -15.7002 58.5 11.1006zM376.4 3.09961c10.5996 7.5 24.8994 4.60059 32.2998 -6
c7.09961 -10.5996 4.59961 -25.1992 -6 -32.5996c-10.6006 -7.09961 -24.9004 -4.59961 -32 6c-7.2002 10.5996 -4.60059 25.2002 5.7002 32.5996z" />
<glyph glyph-name="font-awesome-alt" unicode="&#xf35c;"
d="M339.3 276.8c5.40039 0 9.5 -3 7.7002 -7.09961v-134.4c0 -4.2002 -3 -6 -7.2002 -7.7998c-15.5996 -7.09961 -33.5 -13.7002 -52 -13.7002c-26.2998 0 -38.2002 16.1006 -69.2998 16.1006c-22.7002 0 -46 -8.30078 -65.7002 -16.7002
c-0.599609 -0.600586 -1.7998 -1.2002 -3 -1.2002v-44.2002c0 -1.7998 0 -3 -0.599609 -4.7998v-1.2998c-2.40039 -7.7002 -9.5 -13.7002 -18.5 -13.7002c-10.7002 0 -19.7002 8.90039 -19.7002 19.7002v212.1c-7.7002 6 -12.5 15.5 -12.5 25.7002
c0 18 14.2998 32.2998 32.2998 32.2998s32.2998 -14.3994 32.2998 -32.2998c0 -10.7998 -4.69922 -19.7002 -12.5 -25.7002v-17.8994c1.2002 0.599609 3 1.19922 4.80078 1.7998c17.8994 7.09961 39.3994 13.7002 59.6992 13.7002
c22.1006 0 39.4004 -5.90039 59.1006 -13.7002c4.09961 -1.7998 8.2998 -2.40039 12.5 -2.40039c22.7002 0 46.5996 15.5 52.5996 15.5zM397.8 416c27.5 0 50.2002 -22.7002 50.2002 -50.2002v-347.6c0 -27.5 -22.7002 -50.2002 -50.2002 -50.2002h-347.6
c-27.5 0 -50.2002 22.7002 -50.2002 50.2002v347.6c0 27.5 22.7002 50.2002 50.2002 50.2002h347.6zM412.1 18.2998v347.601c0 7.69922 -6.5 14.2998 -14.2998 14.2998v-0.100586h-347.6c-7.7002 0 -14.2998 -6.5 -14.2998 -14.2998v-347.5
c0 -7.7002 6.5 -14.2998 14.2998 -14.2998h347.6c7.7002 0 14.2998 6.5 14.2998 14.2998z" />
d="M400 416c26.4922 0 48 -21.5078 48 -48v-352c0 -26.4922 -21.5078 -48 -48 -48h-352c-26.4922 0 -48 21.5078 -48 48v352c0 26.4922 21.5078 48 48 48h352zM416 16v352c0 8.83105 -7.16895 16 -16 16h-352c-8.83105 0 -16 -7.16895 -16 -16v-352
c0 -8.83105 7.16895 -16 16 -16h352c8.83105 0 16 7.16895 16 16zM201.6 296c31.2002 0 43.2002 -16 74.6006 -16c18.5996 0 28.2002 4.7998 59.7998 16v-160c-31.5996 -11.2002 -41.2002 -16 -59.7998 -16c-31.4004 0 -43.2002 16 -74.6006 16
c-0.208008 0.00195312 -0.415039 -0.0175781 -0.623047 -0.0175781c-8.7207 0 -17.1162 -1.39844 -24.9766 -3.98242v32c8.08008 2.55664 16.6865 3.96777 25.5996 4c31.2002 0 43.2002 -16 74.6006 -16c10.2002 0 17.7998 1.40039 27.7998 4.59961v96
c-10 -3.19922 -17.5996 -4.59961 -27.7998 -4.59961c-31.4004 0 -43.2002 16 -74.6006 16c-25.3994 0 -37.3994 -10.4004 -57.5996 -14.4004v-153.6c0 -8.83105 -7.16895 -16 -16 -16s-16 7.16895 -16 16v192c0 8.83105 7.16895 16 16 16s16 -7.16895 16 -16v-6.40039
c20.2002 4 32.2002 14.4004 57.5996 14.4004z" />
<glyph glyph-name="accessible-icon" unicode="&#xf368;"
d="M423.9 192.2l-12.9004 -157.3c-3.2998 -40.7002 -63.9004 -35.1006 -60.5996 4.89941l10 122.5l-41.1006 -2.2998c10.1006 -20.7002 15.7998 -43.9004 15.7998 -68.5c0 -41.2002 -16.0996 -78.7002 -42.2998 -106.5l-39.2998 39.2998
c57.9004 63.7002 13.0996 167.2 -74 167.2c-25.9004 0 -49.5 -9.90039 -67.2002 -26l-39.2998 39.2998c22 20.7002 50.0996 35.1006 81.4004 40.2002l75.2998 85.7002l-42.6006 24.7998l-51.5996 -46c-30 -26.7998 -70.5996 18.5 -40.5 45.4004l68 60.6992
@ -1590,14 +1580,18 @@ c13.2998 45.5 -42.2002 71.7002 -64 29.2998z" />
d="M87 -33.7998v73.5996h73.7002v-73.5996h-73.7002zM25.4004 101.4h61.5996v-61.6006h-61.5996v61.6006zM491.6 271.1c53.2002 -170.3 -73 -327.1 -235.6 -327.1v95.7998h0.299805v0.299805c101.7 0.200195 180.5 101 141.4 208
c-14.2998 39.6006 -46.1006 71.4004 -85.7998 85.7002c-107.101 38.7998 -208.101 -39.8994 -208.101 -141.7h-95.7998c0 162.2 156.9 288.7 327 235.601c74.2002 -23.2998 133.6 -82.4004 156.6 -156.601zM256.3 40.0996h-0.299805v-0.299805h-95.2998v95.6006h95.5996
v-95.3008z" />
<glyph glyph-name="discord" unicode="&#xf392;"
d="M297.216 204.8c0 -15.6162 -11.5195 -28.416 -26.1123 -28.416c-14.3359 0 -26.1113 12.7998 -26.1113 28.416s11.5195 28.416 26.1113 28.416c14.5928 0 26.1123 -12.7998 26.1123 -28.416zM177.664 233.216c14.5918 0 26.3682 -12.7998 26.1123 -28.416
c0 -15.6162 -11.5205 -28.416 -26.1123 -28.416c-14.3359 0 -26.1123 12.7998 -26.1123 28.416s11.5205 28.416 26.1123 28.416zM448 395.264v-459.264c-64.4941 56.9941 -43.8682 38.1279 -118.784 107.776l13.5684 -47.3604h-290.304
c-28.9287 0 -52.4805 23.5518 -52.4805 52.7363v346.111c0 29.1846 23.5518 52.7363 52.4805 52.7363h343.039c28.9287 0 52.4805 -23.5518 52.4805 -52.7363zM375.04 152.576c0 82.4316 -36.8643 149.248 -36.8643 149.248
c-36.8643 27.6475 -71.9355 26.8799 -71.9355 26.8799l-3.58398 -4.0957c43.5195 -13.3125 63.7441 -32.5127 63.7441 -32.5127c-60.8115 33.3291 -132.244 33.335 -191.232 7.42383c-9.47168 -4.35156 -15.1035 -7.42383 -15.1035 -7.42383
s21.2471 20.2246 67.3271 33.5361l-2.55957 3.07227s-35.0723 0.767578 -71.9355 -26.8799c0 0 -36.8643 -66.8164 -36.8643 -149.248c0 0 21.5039 -37.1201 78.0801 -38.9121c0 0 9.47168 11.5195 17.1514 21.248c-32.5117 9.72754 -44.7998 30.208 -44.7998 30.208
c3.7666 -2.63574 9.97656 -6.05273 10.4961 -6.40039c43.21 -24.1973 104.588 -32.126 159.744 -8.95996c8.95996 3.32812 18.9443 8.19238 29.4395 15.1045c0 0 -12.7998 -20.9922 -46.3359 -30.4639c7.68066 -9.72852 16.8965 -20.7363 16.8965 -20.7363
c56.5762 1.79199 78.3359 38.9121 78.3359 38.9121z" />
<glyph glyph-name="discord" unicode="&#xf392;" horiz-adv-x="640"
d="M524.531 378.164c66.4014 -97.6289 99.1973 -207.758 86.9336 -334.541c-0.0498047 -0.554688 -0.338867 -1.04102 -0.764648 -1.35156c-43.8203 -32.4541 -93.7129 -57.8623 -147.062 -74.1865c-0.171875 -0.0527344 -0.354492 -0.0830078 -0.543945 -0.0830078
c-0.625977 0 -1.18066 0.308594 -1.51855 0.783203c-11.1562 15.4766 -21.1797 31.7598 -30.0146 48.8145c-0.131836 0.256836 -0.208984 0.549805 -0.208984 0.858398c0 0.799805 0.50293 1.48438 1.20898 1.75293c15.916 5.9834 31.3828 13.3604 45.8906 21.8301
c0.550781 0.329102 0.918945 0.928711 0.918945 1.61621c0 0.617188 -0.297852 1.16602 -0.756836 1.50977c-3.10547 2.30859 -6.18848 4.73438 -9.13184 7.16016c-0.3125 0.254883 -0.713867 0.407227 -1.14844 0.407227
c-0.277344 0 -0.541016 -0.0625 -0.776367 -0.174805c-95.0898 -43.917 -199.271 -43.917 -295.5 0c-0.226562 0.101562 -0.480469 0.15918 -0.744141 0.15918c-0.438477 0 -0.84082 -0.15625 -1.15527 -0.415039c-2.94336 -2.42578 -6.02734 -4.82812 -9.10938 -7.13672
c-0.453125 -0.344727 -0.74707 -0.886719 -0.74707 -1.5c0 -0.692383 0.375 -1.29883 0.932617 -1.62598c14.5459 -8.40234 30 -15.7812 45.8672 -21.8525c0.712891 -0.261719 1.21973 -0.946289 1.21973 -1.74902c0 -0.301758 -0.0722656 -0.586914 -0.200195 -0.839844
c-8.69238 -17.1572 -18.7334 -33.4609 -30.0371 -48.8418c-0.34668 -0.459961 -0.896484 -0.755859 -1.5166 -0.755859c-0.19043 0 -0.373047 0.0283203 -0.546875 0.0800781c-53.25 16.3789 -103.055 41.7812 -146.824 74.1895
c-0.419922 0.327148 -0.706055 0.817383 -0.765625 1.375c-10.2441 109.663 10.6387 220.702 86.8672 334.54c0.185547 0.300781 0.459961 0.537109 0.788086 0.676758c37.3066 17.1338 78.0146 29.9219 119.688 37.1064
c0.0957031 0.015625 0.191406 0.0253906 0.292969 0.0253906c0.694336 0 1.30176 -0.375977 1.63086 -0.935547c5.56348 -9.8418 10.6553 -20.126 15.1348 -30.5996c22.0664 3.34961 43.7744 5.08691 66.7705 5.08691c22.9951 0 45.5889 -1.7373 67.6553 -5.08691
c4.44727 10.4414 9.46191 20.7285 14.9004 30.5996c0.308594 0.5625 0.90332 0.941406 1.58887 0.941406c0.114258 0 0.225586 -0.0107422 0.333984 -0.03125c41.666 -7.19922 82.373 -19.9863 119.686 -37.1055c0.331055 -0.135742 0.601562 -0.384766 0.764648 -0.700195z
M222.491 110.42c29.4326 0 52.8428 26.5869 52.8428 59.2412c0.462891 32.4189 -23.1777 59.2393 -52.8428 59.2393c-29.4355 0 -52.8438 -26.5898 -52.8438 -59.2412c0 -32.6523 23.8711 -59.2393 52.8438 -59.2393zM417.871 110.42
c29.667 0 52.8438 26.5869 52.8438 59.2412c0.462891 32.4189 -23.1768 59.2393 -52.8438 59.2393c-29.4346 0 -52.8428 -26.5898 -52.8428 -59.2412c0 -32.6523 23.8721 -59.2393 52.8428 -59.2393z" />
<glyph glyph-name="discourse" unicode="&#xf393;"
d="M225.9 416c122.699 0 222.1 -102.3 222.1 -223.9c0 -121.6 -99.4004 -223.899 -222.1 -223.899l-225.801 -0.200195s-0.0996094 224 -0.0996094 227.9c0 121.6 103.3 220.1 225.9 220.1zM224 64c70.7002 0 128 57.2998 128 128s-57.2998 128 -128 128
s-128 -57.2998 -128 -128c0 -22.0996 5.59961 -42.9004 15.4004 -61l-22.9004 -75l81.0996 20.0996c16.5 -7.7998 35 -12.0996 54.4004 -12.0996z" />
@ -2461,10 +2455,11 @@ c13.7002 9.39941 16.4004 24.3994 9.10059 31.3994c-7.2002 6.90039 -28.2002 -7 -29
c12.5996 33.0996 -3.59961 45.5 -3.59961 45.5s-23.4004 12.9004 -33.3008 -20.2002c-9.89941 -33.0996 -6.39941 -44.8994 -6.39941 -44.8994s30.7002 -13.4004 43.2998 19.5996zM442.1 188.1c0 0 15.7002 -1.09961 26.4004 14.2002s1.2998 25.5 1.2998 25.5
s-8.59961 11.1006 -19.5996 -9.09961c-11.1006 -20.1006 -8.10059 -30.6006 -8.10059 -30.6006z" />
<glyph glyph-name="font-awesome-flag" unicode="&#xf425;"
d="M444.373 88.5762c0 -7.16797 -6.14453 -10.2402 -13.3125 -13.3125c-28.6719 -12.2881 -59.3916 -23.5518 -92.1592 -23.5518c-46.0801 0 -67.584 28.6719 -122.88 28.6719c-39.9365 0 -81.9209 -14.3359 -115.713 -29.6953
c-2.04785 -1.02441 -4.0957 -1.02441 -6.14355 -2.04883v-77.8232c0 -21.4053 -16.1221 -34.8164 -33.792 -34.8164c-19.4561 0 -34.8164 15.3604 -34.8164 34.8164v374.783c-13.3115 10.2402 -22.5273 26.624 -22.5273 45.0566c0 31.7441 25.5996 57.3438 57.3438 57.3438
s57.3438 -25.5996 57.3438 -57.3438c0 -18.4326 -8.19141 -34.8164 -22.5273 -45.0566v-31.7432c4.12402 1.37402 58.7676 28.6719 114.688 28.6719c65.2705 0 97.6758 -27.6484 126.976 -27.6484c38.9121 0 81.9209 27.6484 92.1602 27.6484
c8.19238 0 15.3604 -6.14453 15.3604 -13.3125v-240.64z" />
d="M448 400v-336c-63 -23 -82 -32 -119 -32c-63 0 -87 32 -150 32c-20 0 -36 -4 -51 -8v64c15 4 31 8 51 8c63 0 87 -32 150 -32c20 0 35 3 55 9v208c-20 -6 -35 -9 -55 -9c-63 0 -87 32 -150 32c-51 0 -75 -21 -115 -29v-307
c0.00195312 -0.136719 0.00292969 -0.273438 0.00292969 -0.410156c0 -17.4404 -14.1602 -31.5996 -31.6006 -31.5996c-0.136719 0 -0.265625 0.0078125 -0.402344 0.00976562c-0.136719 -0.00195312 -0.273438 -0.00292969 -0.410156 -0.00292969
c-17.4404 0 -31.5996 14.1602 -31.5996 31.6006c0 0.136719 0.0078125 0.265625 0.00976562 0.402344v384c-0.00195312 0.136719 -0.00292969 0.273438 -0.00292969 0.410156c0 17.4404 14.1602 31.5996 31.6006 31.5996
c0.136719 0 0.265625 -0.0078125 0.402344 -0.00976562c0.136719 0.00195312 0.273438 0.00292969 0.410156 0.00292969c17.4404 0 31.5996 -14.1602 31.5996 -31.6006c0 -0.136719 -0.0078125 -0.265625 -0.00976562 -0.402344v-13c40 8 64 29 115 29c63 0 87 -32 150 -32
c37 0 56 9 119 32z" />
<glyph glyph-name="gitter" unicode="&#xf426;" horiz-adv-x="384"
d="M66.4004 125.5h-50.4004v322.5h50.4004v-322.5zM166.9 371.9v-435.9h-50.4004v435.9h50.4004zM267.5 371.9v-435.9h-50.4004v435.9h50.4004zM368 372v-247h-50.4004v247h50.4004z" />
<glyph glyph-name="hooli" unicode="&#xf427;" horiz-adv-x="640"
@ -3425,9 +3420,13 @@ M353.9 173.3c3.55273 2.83594 6.87891 5.7998 10.0996 9l-34.9004 35c-3.18457 -3.22
c2.53027 3.79688 4.77832 7.81738 6.7002 12l-39.5 39.7998c-0.374023 -5.3252 -1.63574 -10.4893 -3.59961 -15.2002zM391.6 230.8l-53.0996 53.4004c4.25977 -7.79688 6.82422 -16.7627 7.09961 -26.2002l41.3008 -41.5c1.7959 4.61523 3.39258 9.46387 4.69922 14.2998z
M392.6 236.4c1.25586 5.3623 2.04199 10.9189 2.30078 16.5996l-64.3008 64.7002c-2.61426 -3.74805 -5.95898 -6.85938 -9.89941 -9.2002z" />
<glyph glyph-name="figma" unicode="&#xf799;" horiz-adv-x="384"
d="M277 277.3h-85.4004v-256c-0.0273438 -47.082 -38.2617 -85.2998 -85.3506 -85.2998c-47.1055 0 -85.3496 38.2441 -85.3496 85.3496c0 47.1064 38.2441 85.3506 85.3496 85.3506h0.0507812c-47.1055 0 -85.3496 38.2441 -85.3496 85.3496
c0 47.1064 38.2441 85.3506 85.3496 85.3506c-47.0781 0 -85.2998 38.2217 -85.2998 85.2998s38.2217 85.2998 85.2998 85.2998h170.7c47.1055 0 85.3496 -38.2441 85.3496 -85.3496c0 -47.1064 -38.2441 -85.3506 -85.3496 -85.3506zM277 277.3
c47.0762 -0.00488281 85.2949 -38.2236 85.2998 -85.2998c0 -47.0781 -38.2217 -85.2998 -85.2998 -85.2998s-85.2998 38.2217 -85.2998 85.2998s38.2217 85.2998 85.2998 85.2998z" />
d="M14 352.208c0 52.9043 42.8877 95.792 95.793 95.792h164.368c52.9053 0 95.793 -42.8877 95.793 -95.792c0 -33.5 -17.1963 -62.9844 -43.2432 -80.1055c26.0469 -17.1211 43.2432 -46.6045 43.2432 -80.1045c0 -52.9053 -42.8877 -95.793 -95.793 -95.793h-2.08008
c-24.8018 0 -47.4033 9.42578 -64.415 24.8906v-88.2627c0 -53.6104 -44.0088 -96.833 -97.3574 -96.833c-52.7725 0 -96.3086 42.7568 -96.3086 95.793c0 33.498 17.1943 62.9805 43.2393 80.1016c-26.0449 17.1221 -43.2393 46.6055 -43.2393 80.1035
c0 33.5 17.1963 62.9834 43.2422 80.1045c-26.0459 17.1211 -43.2422 46.6055 -43.2422 80.1055zM176.288 256.413h-66.4951c-35.5762 0 -64.415 -28.8398 -64.415 -64.415c0 -35.4385 28.6172 -64.1924 64.0029 -64.4141
c0.136719 0.000976562 0.274414 0.000976562 0.412109 0.000976562h66.4951v128.828zM207.666 191.998c0 -35.5752 28.8389 -64.415 64.415 -64.415h2.08008c35.5762 0 64.415 28.8398 64.415 64.415s-28.8389 64.415 -64.415 64.415h-2.08008
c-35.5762 0 -64.415 -28.8398 -64.415 -64.415zM109.793 96.2051c-0.137695 0 -0.275391 0.000976562 -0.412109 0.000976562c-35.3857 -0.220703 -64.0029 -28.9746 -64.0029 -64.4131c0 -35.4453 29.2246 -64.415 64.9307 -64.415
c36.2822 0 65.9795 29.4365 65.9795 65.4551v63.3721h-66.4951zM109.793 416.622c-35.5762 0 -64.415 -28.8398 -64.415 -64.4141c0 -35.5762 28.8389 -64.415 64.415 -64.415h66.4951v128.829h-66.4951zM207.666 287.793h66.4951c35.5762 0 64.415 28.8389 64.415 64.415
c0 35.5742 -28.8389 64.4141 -64.415 64.4141h-66.4951v-128.829z" />
<glyph glyph-name="intercom" unicode="&#xf7af;"
d="M392 416c30.9004 0 56 -25.0996 56 -56v-336c0 -30.9004 -25.0996 -56 -56 -56h-336c-30.9004 0 -56 25.0996 -56 56v336c0 30.9004 25.0996 56 56 56h336zM283.7 333.9v-199.5c0 -19.8008 29.8994 -19.8008 29.8994 0v199.5c0 19.7998 -29.8994 19.7998 -29.8994 0z
M209.1 341.4v-216.5c0 -19.8008 29.9004 -19.8008 29.9004 0v216.5c0 19.7998 -29.9004 19.7998 -29.9004 0zM134.4 333.9v-199.5c0 -19.8008 29.8994 -19.8008 29.8994 0v199.5c0 19.7998 -29.8994 19.7998 -29.8994 0zM59.7002 304v-134.3
@ -3553,11 +3552,12 @@ c30.4102 17.9199 81.0498 55.6504 132.75 115.92c14.9697 -9 16.1494 -11.71 16.5098
c-0.860352 -1.67969 -20.0303 -21.6797 -63.2803 -20.4092c5.5 -12.9404 10.9902 -25.0908 16.5 -36.4404zM306.579 337c-1.58008 2.4502 -39.5801 58.8496 -56.4805 54.6104c-16.8994 1.09961 -36.21 -22.9805 -38.21 -75.2803
c21.1104 13.2402 50.1299 22.3301 94.6904 20.6699zM175.929 333.9c-3.7998 6.68945 -8.66992 12.4795 -14.4297 13.5693h-0.0898438c-24.79 1.41016 -24.75 -52.8301 -24.6699 -49.5898c13.6602 -0.00976562 27.8496 -0.410156 42.3994 -1.25977
c-1.62012 12.6602 -2.72949 25.1699 -3.20996 37.2803zM147.869 171.9c-30.7998 -61.5098 -19.8701 -76.6104 -19.6699 -76.8203c7.38965 -15.4902 38.1299 -20.25 84.9199 4.50977c-21.9502 11.7402 -44.4902 32.6104 -65.25 72.3105zM357.929 97.0996z" />
<glyph glyph-name="bootstrap" unicode="&#xf836;"
d="M292.3 136.07c0 -42.4102 -39.7197 -41.4307 -43.9199 -41.4307h-80.8896v81.6904h80.8896c42.5605 0 43.9199 -31.9004 43.9199 -40.2598zM242.15 209.2h-74.6602v72.1797h74.6602c34.9297 0 38.4395 -20.3496 38.4395 -35.8701
c0 -37.3096 -37.7695 -36.3096 -38.4395 -36.3096zM448 341.33v-298.66c-0.121094 -41.1553 -33.5146 -74.5488 -74.6699 -74.6699h-298.66c-41.1553 0.121094 -74.5488 33.5146 -74.6699 74.6699v298.66c0.121094 41.1553 33.5146 74.5488 74.6699 74.6699h298.66
c41.1553 -0.121094 74.5488 -33.5146 74.6699 -74.6699zM338.05 130.14c0 21.5703 -6.64941 58.29 -49.0498 67.3506v0.729492c22.9102 9.78027 37.3398 28.25 37.3398 55.6406c0 7 2 64.7793 -77.5996 64.7793h-127v-261.33c128.229 0 139.87 -1.67969 163.6 5.70996
c14.21 4.4209 52.71 17.9805 52.71 67.1201z" />
<glyph glyph-name="bootstrap" unicode="&#xf836;" horiz-adv-x="576"
d="M333.5 246.6c0 -23.5996 -18.0996 -36.7998 -50.9004 -36.8994h-42.5v71.2002h50.4004c27.4004 0 43 -12.2002 43 -34.3008zM517 259.4c9.5 -31 25.7002 -50.6006 52 -53.1006v-28.5c-26.4004 -2.5 -42.5 -22.0996 -52.0996 -53.0996
c-9.5 -30.9004 -10.8008 -68.7998 -9.80078 -98.1006c1.10059 -30.3994 -22.5996 -58.5 -54.6992 -58.5h-328.7c-32 0 -55.7998 28 -54.7002 58.5c1.09961 29.3008 -0.299805 67.2002 -9.7998 98.1006c-9.60059 31 -25.7998 50.5996 -52.2002 53.0996v28.5
c26.5 2.5 42.5996 22.1006 52.2002 53.1006c9.5 30.8994 10.7998 68.7998 9.7998 98.0996c-1.09961 30.4004 22.5996 58.5 54.7002 58.5h328.8c32 0 55.7998 -28 54.7002 -58.5c-1.10059 -29.2998 0.299805 -67.2002 9.7998 -98.0996zM300.2 72.9004
c51.8994 0 83.2002 25.3994 83.2002 67.5c0 31.6992 -22.3008 54.6992 -55.5 58.2998v1.2998c24.3994 3.90039 43.5 26.5 43.5 51.7998c0 36 -28.4004 59.4004 -71.7002 59.4004h-97.4004v-238.3h97.9004zM290.2 181.6c35.8994 0 54.5 -13.1992 54.5 -38.8994
c0 -25.7998 -18.1006 -39.5 -52.2998 -39.5h-52.3008v78.3994h50.1006z" />
<glyph glyph-name="buffer" unicode="&#xf837;"
d="M427.84 67.3301l-196.5 -97.8203c-2.24707 -0.963867 -4.72266 -1.49805 -7.32129 -1.49805s-5.10156 0.53418 -7.34863 1.49805l-196.51 97.8203c-4 2 -4 5.28027 0 7.29004l47.0596 23.3799c2.25098 0.964844 4.72949 1.49805 7.33203 1.49805
c2.60156 0 5.10742 -0.533203 7.3584 -1.49805l134.76 -67c2.24609 -0.969727 4.72168 -1.50684 7.32129 -1.50684s5.10254 0.537109 7.34863 1.50684l134.76 67c2.24902 0.964844 4.72656 1.49902 7.32715 1.49902s5.10449 -0.53418 7.35352 -1.49902l47.0596 -23.4297

Before

Width:  |  Height:  |  Size: 730 KiB

After

Width:  |  Height:  |  Size: 730 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -2,11 +2,11 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
<metadata>
Created by FontForge 20201107 at Tue Mar 16 10:15:04 2021
Created by FontForge 20201107 at Wed Aug 4 12:25:29 2021
By Robert Madole
Copyright (c) Font Awesome
</metadata>
<!-- Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><defs>
<!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><defs>
<font id="FontAwesome5Free-Regular" horiz-adv-x="512" >
<font-face
font-family="Font Awesome 5 Free Regular"

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -2,11 +2,11 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
<metadata>
Created by FontForge 20201107 at Tue Mar 16 10:15:04 2021
Created by FontForge 20201107 at Wed Aug 4 12:25:29 2021
By Robert Madole
Copyright (c) Font Awesome
</metadata>
<!-- Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><defs>
<!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><defs>
<font id="FontAwesome5Free-Solid" horiz-adv-x="512" >
<font-face
font-family="Font Awesome 5 Free Solid"

Before

Width:  |  Height:  |  Size: 898 KiB

After

Width:  |  Height:  |  Size: 898 KiB

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More