Actualizar plugin Additionals a 3.0.0
This commit is contained in:
parent
3d976f1b3b
commit
a26f5567af
399 changed files with 70374 additions and 4093 deletions
|
@ -1,23 +1,28 @@
|
|||
require:
|
||||
- rubocop-performance
|
||||
- rubocop-rails
|
||||
|
||||
Rails:
|
||||
Enabled: true
|
||||
|
||||
AllCops:
|
||||
TargetRubyVersion: 2.2
|
||||
TargetRubyVersion: 2.4
|
||||
TargetRailsVersion: 5.2
|
||||
NewCops: enable
|
||||
|
||||
Metrics/AbcSize:
|
||||
Max: 65
|
||||
Enabled: false
|
||||
|
||||
Metrics/BlockLength:
|
||||
Max: 60
|
||||
Enabled: false
|
||||
|
||||
Metrics/ClassLength:
|
||||
Enabled: false
|
||||
|
||||
Metrics/CyclomaticComplexity:
|
||||
Max: 20
|
||||
Max: 25
|
||||
|
||||
Metrics/LineLength:
|
||||
Layout/LineLength:
|
||||
Max: 140
|
||||
|
||||
Metrics/MethodLength:
|
||||
|
@ -29,21 +34,39 @@ Metrics/ModuleLength:
|
|||
Metrics/PerceivedComplexity:
|
||||
Max: 25
|
||||
|
||||
Rails/SkipsModelValidations:
|
||||
Rails/ApplicationJob:
|
||||
Enabled: false
|
||||
|
||||
Rails/ApplicationRecord:
|
||||
Enabled: false
|
||||
|
||||
Rails/CreateTableWithTimestamps:
|
||||
Enabled: false
|
||||
|
||||
# app/model/application_record.rb is missing in redmine, we can't use ApplicationRecord
|
||||
Rails/ApplicationRecord:
|
||||
Rails/HelperInstanceVariable:
|
||||
Enabled: false
|
||||
|
||||
Rails/SkipsModelValidations:
|
||||
Enabled: false
|
||||
|
||||
Performance/ChainArrayAllocation:
|
||||
Enabled: true
|
||||
|
||||
Style/AutoResourceCleanup:
|
||||
Enabled: true
|
||||
|
||||
Style/FrozenStringLiteralComment:
|
||||
Enabled: false
|
||||
|
||||
Style/Documentation:
|
||||
Enabled: false
|
||||
|
||||
# required for travis/ci (symbolic links problem)
|
||||
Style/ExpandPathArguments:
|
||||
Enabled: false
|
||||
|
||||
Style/HashTransformKeys:
|
||||
Enabled: false
|
||||
|
||||
Style/HashTransformValues:
|
||||
Enabled: false
|
||||
|
|
|
@ -3,16 +3,16 @@ linters:
|
|||
max: 140
|
||||
RuboCop:
|
||||
ignored_cops:
|
||||
- Layout/AlignArray
|
||||
- Layout/AlignHash
|
||||
- Layout/AlignParameters
|
||||
- Layout/ArgumentAlignment
|
||||
- Layout/ArrayAlignment
|
||||
- Layout/BlockEndNewline
|
||||
- Layout/EmptyLineAfterGuardClause
|
||||
- Layout/FirstParameterIndentation
|
||||
- Layout/IndentArray
|
||||
- Layout/HashAlignment
|
||||
- Layout/IndentationConsistency
|
||||
- Layout/IndentationWidth
|
||||
- Layout/IndentHash
|
||||
- Layout/IndentFirstArgument
|
||||
- Layout/IndentFirstArrayElement
|
||||
- Layout/IndentFirstHashElement
|
||||
- Layout/MultilineArrayBraceLayout
|
||||
- Layout/MultilineAssignmentLayout
|
||||
- Layout/MultilineBlockLayout
|
||||
|
@ -22,6 +22,7 @@ linters:
|
|||
- Layout/MultilineMethodDefinitionBraceLayout
|
||||
- Layout/MultilineOperationIndentation
|
||||
- Layout/TrailingBlankLines
|
||||
- Layout/TrailingEmptyLines
|
||||
- Layout/TrailingWhitespace
|
||||
- Lint/BlockAlignment
|
||||
- Lint/EndAlignment
|
||||
|
|
|
@ -1,26 +1,21 @@
|
|||
language: ruby
|
||||
os: linux
|
||||
dist: xenial
|
||||
|
||||
rvm:
|
||||
- 2.5.3
|
||||
- 2.4.5
|
||||
- 2.3.8
|
||||
- 2.6.6
|
||||
- 2.5.8
|
||||
- 2.4.10
|
||||
|
||||
services:
|
||||
- mysql
|
||||
- postgresql
|
||||
|
||||
env:
|
||||
- REDMINE_VER=4.0-stable DB=postgresql
|
||||
- REDMINE_VER=3.4-stable DB=postgresql
|
||||
- REDMINE_VER=4.0-stable DB=mysql
|
||||
- REDMINE_VER=3.4-stable DB=mysql
|
||||
|
||||
sudo: true
|
||||
|
||||
addons:
|
||||
postgresql: "9.6"
|
||||
apt:
|
||||
sources:
|
||||
- mysql-5.7-trusty
|
||||
packages:
|
||||
- mysql-server
|
||||
- mysql-client
|
||||
- REDMINE_VER=4.1-stable DB=postgresql
|
||||
- REDMINE_VER=master DB=postgresql
|
||||
- REDMINE_VER=4.1-stable DB=mysql
|
||||
- REDMINE_VER=master DB=mysql
|
||||
|
||||
before_install:
|
||||
- export PLUGIN_NAME=additionals
|
||||
|
@ -31,19 +26,18 @@ before_install:
|
|||
- git clone $REDMINE_GIT_REPO $REDMINE_PATH
|
||||
- cd $REDMINE_PATH
|
||||
- if [[ "$REDMINE_VER" != "master" ]]; then git checkout -b $REDMINE_VER origin/$REDMINE_VER; fi
|
||||
- sed -i '/rubocop/d' $REDMINE_PATH/Gemfile
|
||||
- rm -f $REDMINE_PATH/.rubocop*
|
||||
- cp $TRAVIS_BUILD_DIR/test/support/Gemfile.local $REDMINE_PATH
|
||||
- ln -s $TRAVIS_BUILD_DIR $REDMINE_PATH/plugins/$PLUGIN_NAME
|
||||
- cp $TRAVIS_BUILD_DIR/test/support/additional_environment.rb $REDMINE_PATH/config/
|
||||
- cp $TRAVIS_BUILD_DIR/test/support/database-$DB-travis.yml $REDMINE_PATH/config/database.yml
|
||||
|
||||
before_script:
|
||||
- if [[ "$DB" == "mysql" ]]; then mysql -e "use mysql; update user set authentication_string=PASSWORD('travis_ci_test') where User='root'; update user set plugin='mysql_native_password';FLUSH PRIVILEGES;"; fi
|
||||
- if [[ "$DB" == "mysql" ]]; then mysql_upgrade -ptravis_ci_test; fi
|
||||
- if [[ "$DB" == "mysql" ]]; then service mysql restart; fi
|
||||
- bundle exec rake db:create db:migrate redmine:plugins:migrate
|
||||
|
||||
script:
|
||||
- export SKIP_COVERAGE=1
|
||||
- if [[ "$REDMINE_VER" == "master" ]]; then bundle exec rake redmine:plugins:test:units NAME=$PLUGIN_NAME; fi
|
||||
- if [[ "$REDMINE_VER" == "master" ]]; then bundle exec rake redmine:plugins:test:functionals NAME=$PLUGIN_NAME; fi
|
||||
- if [[ "$REDMINE_VER" == "master" ]]; then bundle exec rake redmine:plugins:test:integration NAME=$PLUGIN_NAME; fi
|
||||
- if [[ "$REDMINE_VER" != "master" ]]; then bundle exec rake redmine:plugins:test NAME=$PLUGIN_NAME RUBYOPT="-W0"; fi
|
||||
- if [[ "$REDMINE_VER" != "master" ]] && [[ "$DB" == "postgresql" ]]; then brakeman plugins/$PLUGIN_NAME; fi
|
||||
- if [[ "$REDMINE_VER" != "master" ]] && [[ "$DB" == "postgresql" ]]; then rubocop plugins/$PLUGIN_NAME; fi
|
||||
- bundle exec rake redmine:plugins:test NAME=$PLUGIN_NAME RUBYOPT="-W0"
|
||||
- bundle exec rake redmine:plugins:migrate NAME=$PLUGIN_NAME VERSION=0
|
||||
|
|
|
@ -1,6 +1,74 @@
|
|||
Changelog
|
||||
=========
|
||||
|
||||
3.0.0
|
||||
+++++
|
||||
|
||||
- Introduce dashboards
|
||||
- Redmine 4.1 or newer is required
|
||||
- FontAwesome 5.14.0 support
|
||||
- D3 6.1.1 support
|
||||
- Mermaid 8.8.0 support
|
||||
- d3plus to v2.0.0-alpha.29 support
|
||||
- drop wiki header and footer settings
|
||||
|
||||
2.0.24
|
||||
++++++
|
||||
|
||||
- FontAwesome 5.13.0 support
|
||||
- Mermaid 8.4.8 support
|
||||
- clipboard.js updated to v2.0.6
|
||||
- fix for spam protection with invisible_captcha
|
||||
- D3 5.16.0 support
|
||||
- Ruby 2.4 is required
|
||||
|
||||
2.0.23
|
||||
++++++
|
||||
|
||||
- members macro now supports with_sum option
|
||||
- FontAwesome 5.12 support
|
||||
- FontAwesome ajax search has been added
|
||||
- Mermaid 8.4.6 support
|
||||
- D3 5.15.0 support
|
||||
- Drop nvd3 library
|
||||
- Drop Chartjs stacked100 library
|
||||
- Drop d3plus-hierarchy library
|
||||
- Drop calendar macro
|
||||
- Support private comments with issue macro
|
||||
- Google Docs macro has been added
|
||||
- Fix bug with Rack 2.0.8 or newer
|
||||
- Drop Redmine 3.4 support
|
||||
- Add Redmine 4.1 support
|
||||
- Use view_layouts_base_body_top hook, which is available since Redmine 3.4
|
||||
- Refactoring new hooks (without template)
|
||||
- asciinema.org macro has been added - thanks to @kotashiratsuka
|
||||
- Select2 4.0.13 support
|
||||
|
||||
2.0.22
|
||||
++++++
|
||||
|
||||
- FontAwesome 5.11.2 support
|
||||
- Mermaid 8.4.2 support
|
||||
- Select2 4.0.12 support
|
||||
- Chart.js 2.9.3 support
|
||||
- Chart.js Plugin datalabels 0.7.0 support
|
||||
- d3plus to v2.0.0-alpha.25
|
||||
- Fix user visibility for members macro
|
||||
- Fix user visibility for issue reports
|
||||
- Drop ZeroClipboard library
|
||||
|
||||
2.0.21
|
||||
++++++
|
||||
|
||||
- fix mail notification if issue author changed
|
||||
- fix permission bug for closed issues with freezed mode
|
||||
- Ruby 2.2.x support has been dropped. Use 2.3.x or newer ruby verion
|
||||
- FontAwesome 5.9.0 support
|
||||
- remove issue_close_with_open_children functionality, because this is included in Redmine 3.4.x #47 (thanks to @pva)
|
||||
- add hierarchy support for projects macro #45
|
||||
- select2 support
|
||||
- bootstrap-datepicker 1.9.0 support
|
||||
|
||||
2.0.20
|
||||
++++++
|
||||
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
gem 'deface', '>= 1.1.0'
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gem 'deface', '1.5.3'
|
||||
gem 'gemoji', '~> 3.0.0'
|
||||
gem 'invisible_captcha'
|
||||
gem 'render_async'
|
||||
gem 'rss'
|
||||
gem 'slim-rails'
|
||||
|
||||
group :test do
|
||||
group :development, :test do
|
||||
gem 'brakeman', require: false
|
||||
gem 'rubocop', require: false
|
||||
gem 'slim_lint', require: false
|
||||
end
|
||||
|
||||
#group :development do
|
||||
# gem 'awesome_print', require: 'ap' # https://github.com/awesome-print/awesome_print
|
||||
# gem 'better_errors' # https://github.com/BetterErrors/better_errors
|
||||
# gem 'binding_of_caller' # better output of with variables for better_errors
|
||||
#end
|
||||
|
|
1
plugins/additionals/README.rst
Normal file
1
plugins/additionals/README.rst
Normal file
|
@ -0,0 +1 @@
|
|||
docs/index.rst
|
|
@ -23,7 +23,7 @@ class AdditionalsAssignToMeController < ApplicationController
|
|||
return redirect_to(issue_path(@issue)) if last_journal.nil?
|
||||
|
||||
last_journal = @issue.journals.visible.order(:created_on).last
|
||||
redirect_to "#{issue_path(@issue)}#change-#{last_journal.id}"
|
||||
redirect_to "#{issue_path @issue}#change-#{last_journal.id}"
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -27,7 +27,7 @@ class AdditionalsChangeStatusController < ApplicationController
|
|||
return redirect_to(issue_path(@issue)) if last_journal.nil?
|
||||
|
||||
last_journal = @issue.journals.visible.order(:created_on).last
|
||||
redirect_to "#{issue_path(@issue)}#change-#{last_journal.id}"
|
||||
redirect_to "#{issue_path @issue}#change-#{last_journal.id}"
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
require 'open-uri'
|
||||
|
||||
class DashboardAsyncBlocksController < ApplicationController
|
||||
before_action :find_dashboard
|
||||
before_action :find_block
|
||||
|
||||
helper :additionals_routes
|
||||
helper :additionals_queries
|
||||
helper :additionals_tag
|
||||
helper :queries
|
||||
helper :issues
|
||||
helper :activities
|
||||
helper :dashboards
|
||||
|
||||
include DashboardsHelper
|
||||
|
||||
rescue_from Query::StatementInvalid, with: :query_statement_invalid
|
||||
rescue_from StandardError, with: :dashboard_with_invalid_block
|
||||
|
||||
def show
|
||||
@settings[:sort] = params[:sort] if params[:sort].present?
|
||||
partial_locals = build_dashboard_partial_locals @block, @block_definition, @settings, @dashboard
|
||||
|
||||
respond_to do |format|
|
||||
format.js do
|
||||
render partial: partial_locals[:async][:partial],
|
||||
content_type: 'text/html',
|
||||
locals: partial_locals
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# abuse create for query list sort order support
|
||||
def create
|
||||
return render_403 if params[:sort].blank?
|
||||
|
||||
partial_locals = build_dashboard_partial_locals @block, @block_definition, @settings, @dashboard
|
||||
partial_locals[:sort_options] = { sort: params[:sort] }
|
||||
|
||||
respond_to do |format|
|
||||
format.js do
|
||||
render partial: 'update_order_by',
|
||||
locals: partial_locals
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_dashboard
|
||||
@dashboard = Dashboard.find params[:dashboard_id]
|
||||
raise ::Unauthorized unless @dashboard.visible?
|
||||
|
||||
if @dashboard.dashboard_type == DashboardContentProject::TYPE_NAME && @dashboard.project.nil?
|
||||
@dashboard.content_project = find_project_by_project_id
|
||||
else
|
||||
@project = @dashboard.project
|
||||
deny_access if @project.present? && !User.current.allowed_to?(:view_project, @project)
|
||||
end
|
||||
|
||||
@can_edit = @dashboard&.editable?
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
|
||||
def find_block
|
||||
@block = params['block']
|
||||
@block_definition = @dashboard.content.find_block @block
|
||||
|
||||
render_404 if @block.blank?
|
||||
render_403 if @block_definition.blank?
|
||||
|
||||
@settings = @dashboard.layout_settings @block
|
||||
end
|
||||
|
||||
def find_project_by_project_id
|
||||
begin
|
||||
@project = Project.find params[:project_id]
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
deny_access unless User.current.allowed_to?(:view_project, @project)
|
||||
|
||||
@project
|
||||
end
|
||||
|
||||
def dashboard_with_invalid_block(exception)
|
||||
logger&.error "Invalid dashboard block for #{@block}: #{exception.message}"
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
render template: 'dashboards/block_error', layout: false
|
||||
end
|
||||
format.any { head @status }
|
||||
end
|
||||
end
|
||||
end
|
217
plugins/additionals/app/controllers/dashboards_controller.rb
Normal file
217
plugins/additionals/app/controllers/dashboards_controller.rb
Normal file
|
@ -0,0 +1,217 @@
|
|||
class DashboardsController < ApplicationController
|
||||
menu_item :dashboards
|
||||
|
||||
before_action :find_dashboard, except: %i[index new create]
|
||||
before_action :find_optional_project, only: %i[new create index]
|
||||
|
||||
accept_rss_auth :index, :show
|
||||
accept_api_auth :index, :show, :create, :update, :destroy
|
||||
|
||||
rescue_from Query::StatementInvalid, with: :query_statement_invalid
|
||||
|
||||
helper :queries
|
||||
helper :issues
|
||||
helper :activities
|
||||
helper :watchers
|
||||
helper :additionals_routes
|
||||
helper :dashboards
|
||||
helper :additionals_issues
|
||||
helper :additionals_queries
|
||||
helper :additionals_tag
|
||||
|
||||
include AdditionalsRoutesHelper
|
||||
include AdditionalsQueriesHelper
|
||||
include QueriesHelper
|
||||
include WatchersHelper
|
||||
include SortHelper
|
||||
|
||||
def index
|
||||
case params[:format]
|
||||
when 'xml', 'json'
|
||||
@offset, @limit = api_offset_and_limit
|
||||
else
|
||||
@limit = per_page_option
|
||||
end
|
||||
|
||||
scope = Dashboard.visible
|
||||
@query_count = scope.count
|
||||
@query_pages = Paginator.new @query_count, @limit, params['page']
|
||||
@dashboards = scope.sorted
|
||||
.limit(@limit)
|
||||
.offset(@offset)
|
||||
.to_a
|
||||
|
||||
respond_to do |format|
|
||||
format.html { render_error status: 406 }
|
||||
format.api
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
respond_to do |format|
|
||||
format.html { head 406 }
|
||||
format.js if request.xhr?
|
||||
format.api
|
||||
end
|
||||
end
|
||||
|
||||
def new
|
||||
@dashboard = Dashboard.new(project: @project,
|
||||
author: User.current)
|
||||
@dashboard.dashboard_type = assign_dashboard_type
|
||||
@allowed_projects = @dashboard.allowed_target_projects
|
||||
end
|
||||
|
||||
def create
|
||||
@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?
|
||||
|
||||
@allowed_projects = @dashboard.allowed_target_projects
|
||||
|
||||
if @dashboard.save
|
||||
flash[:notice] = l(:notice_successful_create)
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to dashboard_link_path(@project, @dashboard) }
|
||||
format.api { render action: 'show', status: :created, location: dashboard_url(@dashboard, project_id: @project) }
|
||||
end
|
||||
else
|
||||
respond_to do |format|
|
||||
format.html { render action: 'new' }
|
||||
format.api { render_validation_errors(@dashboard) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
return render_403 unless @dashboard.editable_by?(User.current)
|
||||
|
||||
@allowed_projects = @dashboard.allowed_target_projects
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.xml {}
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
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?
|
||||
|
||||
@project = @dashboard.project if @project && @dashboard.project_id.present? && @dashboard.project != @project
|
||||
@allowed_projects = @dashboard.allowed_target_projects
|
||||
|
||||
if @dashboard.save
|
||||
flash[:notice] = l(:notice_successful_update)
|
||||
respond_to do |format|
|
||||
format.html { redirect_to dashboard_link_path @project, @dashboard }
|
||||
format.api { head :ok }
|
||||
end
|
||||
else
|
||||
respond_to do |format|
|
||||
format.html { render action: 'edit' }
|
||||
format.api { render_validation_errors @dashboard }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
return render_403 unless @dashboard.destroyable_by? User.current
|
||||
|
||||
begin
|
||||
@dashboard.destroy
|
||||
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)
|
||||
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'))
|
||||
render_error l(:error_query_statement_invalid)
|
||||
end
|
||||
|
||||
def update_layout_setting
|
||||
block_settings = params[:settings] || {}
|
||||
|
||||
block_settings.each do |block, settings|
|
||||
@dashboard.update_block_settings(block, settings.to_unsafe_hash)
|
||||
end
|
||||
@dashboard.save
|
||||
@updated_blocks = block_settings.keys
|
||||
end
|
||||
|
||||
# The block is added on top of the page
|
||||
# params[:block] : id of the block to add
|
||||
def add_block
|
||||
@block = params[:block]
|
||||
if @dashboard.add_block @block
|
||||
@dashboard.save
|
||||
respond_to do |format|
|
||||
format.html { redirect_to dashboard_link_path(@project, @dashboard) }
|
||||
format.js
|
||||
end
|
||||
else
|
||||
render_error status: 422
|
||||
end
|
||||
end
|
||||
|
||||
# params[:block] : id of the block to remove
|
||||
def remove_block
|
||||
@block = params[:block]
|
||||
@dashboard.remove_block @block
|
||||
@dashboard.save
|
||||
respond_to do |format|
|
||||
format.html { redirect_to dashboard_link_path(@project, @dashboard) }
|
||||
format.js
|
||||
end
|
||||
end
|
||||
|
||||
# Change blocks order
|
||||
# params[:group] : group to order (top, left or right)
|
||||
# params[:blocks] : array of block ids of the group
|
||||
def order_blocks
|
||||
@dashboard.order_blocks params[:group], params[:blocks]
|
||||
@dashboard.save
|
||||
head 200
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assign_dashboard_type
|
||||
if params['dashboard_type'].present?
|
||||
params['dashboard_type']
|
||||
elsif params['dashboard'].present? && params['dashboard']['dashboard_type'].present?
|
||||
params['dashboard']['dashboard_type']
|
||||
elsif @project.nil?
|
||||
DashboardContentWelcome::TYPE_NAME
|
||||
else
|
||||
DashboardContentProject::TYPE_NAME
|
||||
end
|
||||
end
|
||||
|
||||
def find_dashboard
|
||||
@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
|
||||
else
|
||||
@project = @dashboard.project
|
||||
end
|
||||
|
||||
@can_edit = @dashboard&.editable?
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
end
|
|
@ -0,0 +1,12 @@
|
|||
module AdditionalsChartjsHelper
|
||||
def chartjs_colorschemes_info_url
|
||||
link_to(l(:label_chartjs_colorscheme_info),
|
||||
'https://nagix.github.io/chartjs-plugin-colorschemes/colorchart.html',
|
||||
class: 'external')
|
||||
end
|
||||
|
||||
def select_options_for_chartjs_colorscheme(selected)
|
||||
data = YAML.safe_load(ERB.new(IO.read(Rails.root.join('plugins/additionals/config/colorschemes.yml'))).result) || {}
|
||||
grouped_options_for_select(data, selected)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,25 @@
|
|||
module AdditionalsClipboardjsHelper
|
||||
def clipboardjs_button_for(target, clipboard_text_from_button = nil)
|
||||
render_clipboardjs_button(target, clipboard_text_from_button) + render_clipboardjs_javascript(target)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def render_clipboardjs_button(target, clipboard_text_from_button)
|
||||
data = { 'clipboard-target' => "##{target}",
|
||||
'label-copied' => l(:label_copied_to_clipboard),
|
||||
'label-to-copy' => l(:label_copy_to_clipboard) }
|
||||
|
||||
data['clipboard-text'] = clipboard_text_from_button if clipboard_text_from_button.present?
|
||||
|
||||
tag.button id: "zc_#{target}",
|
||||
class: 'clipboard_button far fa-copy',
|
||||
type: 'button',
|
||||
title: l(:label_copy_to_clipboard),
|
||||
data: data
|
||||
end
|
||||
|
||||
def render_clipboardjs_javascript(target)
|
||||
javascript_tag("setClipboardJS('#zc_#{target}');")
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
module AdditionalsCustomFieldsHelper
|
||||
def custom_fields_with_full_with_layout
|
||||
['IssueCustomField']
|
||||
end
|
||||
end
|
|
@ -1,11 +1,4 @@
|
|||
module AdditionalsFontawesomeHelper
|
||||
def fontawesome_info_url
|
||||
s = []
|
||||
s << l(:label_set_icon_from)
|
||||
s << link_to('https://fontawesome.com/icons?m=free', 'https://fontawesome.com/icons?m=free', class: 'external')
|
||||
safe_join(s, ' ')
|
||||
end
|
||||
|
||||
# name = TYPE-FA_NAME, eg. fas_car
|
||||
# fas_cloud-upload-alt
|
||||
# far_id-card
|
||||
|
@ -15,13 +8,13 @@ module AdditionalsFontawesomeHelper
|
|||
# post_text
|
||||
# title
|
||||
def font_awesome_icon(name, options = {})
|
||||
info = AdditionalsFontAwesome.value_info(name)
|
||||
info = AdditionalsFontAwesome.value_info name
|
||||
return '' if info.blank?
|
||||
|
||||
post_text = ''
|
||||
options['aria-hidden'] = 'true'
|
||||
options[:'aria-hidden'] = 'true'
|
||||
options[:class] = if options[:class].present?
|
||||
info[:classes] + ' ' + options[:class]
|
||||
"#{info[:classes]} #{options[:class]}"
|
||||
else
|
||||
info[:classes]
|
||||
end
|
||||
|
@ -30,17 +23,68 @@ module AdditionalsFontawesomeHelper
|
|||
if options[:pre_text].present?
|
||||
s << options[:pre_text]
|
||||
s << ' '
|
||||
options.delete(:pre_text)
|
||||
options.delete :pre_text
|
||||
end
|
||||
if options[:post_text].present?
|
||||
post_text = options[:post_text]
|
||||
options.delete(:post_text)
|
||||
options.delete :post_text
|
||||
end
|
||||
s << content_tag('span', '', options)
|
||||
s << tag.span(options)
|
||||
if post_text.present?
|
||||
s << ' '
|
||||
s << post_text
|
||||
end
|
||||
safe_join(s)
|
||||
safe_join s
|
||||
end
|
||||
|
||||
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)
|
||||
|
||||
s = []
|
||||
s << form.select(name,
|
||||
options_for_select(AdditionalsFontAwesome.active_option_for_select(selected), selected),
|
||||
options,
|
||||
html_options)
|
||||
|
||||
s << additionals_fontawesome_loader(options, html_options) if loader
|
||||
|
||||
safe_join s
|
||||
end
|
||||
|
||||
def additionals_fontawesome_add_selected(selected)
|
||||
@selected_store ||= []
|
||||
return if selected.blank?
|
||||
|
||||
@selected_store << selected
|
||||
end
|
||||
|
||||
def additionals_fontawesome_default_select_width
|
||||
'250px'
|
||||
end
|
||||
|
||||
def additionals_fontawesome_loader(options, html_options = {})
|
||||
html_options[:class] ||= 'select2-fontawesome-field'
|
||||
options[:template_selection] = 'formatFontawesomeText'
|
||||
options[:template_result] = 'formatFontawesomeText'
|
||||
if options[:include_blank]
|
||||
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',
|
||||
formats: [:js],
|
||||
locals: { field_class: html_options[:class],
|
||||
ajax_url: fontawesome_auto_completes_path(selected: @selected_store.join(',')),
|
||||
options: options })
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,18 +1,29 @@
|
|||
module AdditionalsIssuesHelper
|
||||
def issue_author_options_for_select(project, issue = nil)
|
||||
authors = project.users.sorted
|
||||
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?
|
||||
authors = scope.sorted.to_a
|
||||
|
||||
unless entity.nil?
|
||||
current_author_found = authors.detect { |u| u.id == entity.author_id_was }
|
||||
if current_author_found.blank?
|
||||
current_author = User.find_by id: entity.author_id_was
|
||||
authors << current_author if current_author
|
||||
end
|
||||
end
|
||||
|
||||
s = []
|
||||
return s unless authors.any?
|
||||
|
||||
s << content_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 issue.nil?
|
||||
if entity.nil?
|
||||
s << options_from_collection_for_select(authors, 'id', 'name')
|
||||
else
|
||||
s << content_tag('option', issue.author, value: issue.author_id, selected: true) if issue.author && !authors.include?(issue.author)
|
||||
s << options_from_collection_for_select(authors, 'id', 'name', issue.author_id)
|
||||
s << tag.option(entity.author, value: entity.author_id, selected: true) if entity.author && authors.exclude?(entity.author)
|
||||
s << options_from_collection_for_select(authors, 'id', 'name', entity.author_id)
|
||||
end
|
||||
safe_join(s)
|
||||
safe_join s
|
||||
end
|
||||
|
||||
def show_issue_change_author?(issue)
|
||||
|
|
159
plugins/additionals/app/helpers/additionals_journals_helper.rb
Normal file
159
plugins/additionals/app/helpers/additionals_journals_helper.rb
Normal file
|
@ -0,0 +1,159 @@
|
|||
module AdditionalsJournalsHelper
|
||||
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 = {})
|
||||
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)
|
||||
strings = []
|
||||
values_by_field = {}
|
||||
|
||||
details.each do |detail|
|
||||
if detail.property == 'cf'
|
||||
field = detail.custom_field
|
||||
if field&.multiple?
|
||||
values_by_field[field] ||= { added: [], deleted: [] }
|
||||
values_by_field[field][:deleted] << detail.old_value if detail.old_value
|
||||
values_by_field[field][:added] << detail.value if detail.value
|
||||
next
|
||||
end
|
||||
end
|
||||
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.value = changes[:added]
|
||||
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.old_value = changes[:deleted]
|
||||
strings << send(show_detail_method, detail, no_html, options)
|
||||
end
|
||||
end
|
||||
strings
|
||||
end
|
||||
|
||||
# taken from Redmine 4
|
||||
# Returns the action links for an issue journal
|
||||
def render_entity_journal_actions(entity, journal)
|
||||
return '' unless journal.notes.present? && journal.editable_by?(User.current)
|
||||
|
||||
entity_type = entity.model_name.param_key
|
||||
|
||||
safe_join [link_to(l(:button_edit),
|
||||
send("edit_#{entity_type}_journal_path", journal),
|
||||
remote: true,
|
||||
method: 'get',
|
||||
title: l(:button_edit),
|
||||
class: 'icon-only icon-edit'),
|
||||
link_to(l(:button_delete),
|
||||
send("#{entity_type}_journal_path", journal, journal: { notes: '' }),
|
||||
remote: true,
|
||||
method: 'put', data: { confirm: l(:text_are_you_sure) },
|
||||
title: l(:button_delete),
|
||||
class: 'icon-only icon-del')], ' '
|
||||
end
|
||||
|
||||
# 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:
|
||||
multiple = false
|
||||
no_detail = false
|
||||
show_diff = false
|
||||
label = nil
|
||||
diff_url_method = "diff_#{entity.name.underscore}_journal_url"
|
||||
entity_prop = entity_show_detail_prop detail, options
|
||||
|
||||
if entity_prop.present?
|
||||
label = entity_prop[:label] if entity_prop.key? :label
|
||||
value = entity_prop[:value] if entity_prop.key? :value
|
||||
old_value = entity_prop[:old_value] if entity_prop.key? :old_value
|
||||
show_diff = entity_prop[:show_diff] if entity_prop.key? :show_diff
|
||||
no_detail = entity_prop[:no_detail] if entity_prop.key? :no_detail
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
html =
|
||||
if no_detail
|
||||
l(:text_journal_changed_no_detail, label: label)
|
||||
elsif show_diff
|
||||
s = l(:text_journal_changed_no_detail, label: label)
|
||||
unless no_html
|
||||
diff_link = link_to l(:label_diff),
|
||||
send(diff_url_method,
|
||||
detail.journal_id,
|
||||
detail_id: detail.id,
|
||||
only_path: options[:only_path]),
|
||||
title: l(:label_view_diff)
|
||||
s << " (#{diff_link})"
|
||||
end
|
||||
s.html_safe
|
||||
elsif detail.value.present?
|
||||
if detail.old_value.present?
|
||||
l(:text_journal_changed, label: label, old: old_value, new: value)
|
||||
elsif multiple
|
||||
l(:text_journal_added, label: label, value: value)
|
||||
else
|
||||
l(:text_journal_set_to, label: label, value: value)
|
||||
end
|
||||
else
|
||||
l(:text_journal_deleted, label: label, old: old_value).html_safe
|
||||
end
|
||||
html.html_safe
|
||||
else
|
||||
# default implementation for journal detail rendering
|
||||
show_detail detail, no_html, options
|
||||
end
|
||||
end
|
||||
# rubocop: enable Rails/OutputSafety
|
||||
|
||||
private
|
||||
|
||||
def entity_show_detail_prop(detail, options)
|
||||
return options[:entity_prop] if options.key? :entity_prop
|
||||
return unless detail.property == 'cf'
|
||||
|
||||
custom_field = detail.custom_field
|
||||
return unless custom_field
|
||||
|
||||
return { show_diff: true, label: l(:field_description) } if custom_field.format.class.change_as_diff
|
||||
|
||||
case custom_field.format.name
|
||||
when 'project_relation'
|
||||
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?
|
||||
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?
|
||||
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?
|
||||
end
|
||||
|
||||
prop
|
||||
end
|
||||
end
|
|
@ -1,28 +1,37 @@
|
|||
module AdditionalsMenuHelper
|
||||
def additionals_top_menu_setup
|
||||
return unless User.current.try(:hrm_user_type_id).nil?
|
||||
return if Redmine::Plugin.installed? 'redmine_hrm'
|
||||
|
||||
if Additionals.setting?(:remove_mypage)
|
||||
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, if: proc { User.current.logged? } })
|
||||
end
|
||||
|
||||
if Additionals.setting?(:remove_help)
|
||||
if Additionals.setting? :remove_help
|
||||
Redmine::MenuManager.map(:top_menu).delete(:help) if Redmine::MenuManager.map(:top_menu).exists?(:help)
|
||||
elsif User.current.logged?
|
||||
handle_top_menu_item(:help, url: '#', symbol: 'fas_question', last: true)
|
||||
handle_top_submenu_item :help, url: '#', symbol: 'fas_question', last: true
|
||||
@additionals_help_items = additionals_help_menu_items
|
||||
else
|
||||
handle_top_menu_item(:help, url: Redmine::Info.help_url, symbol: 'fas_question', last: true)
|
||||
handle_top_menu_item :help, url: Redmine::Info.help_url, symbol: 'fas_question', last: true
|
||||
end
|
||||
end
|
||||
|
||||
def handle_top_menu_item(menu_name, item)
|
||||
def handle_top_submenu_item(menu_name, item)
|
||||
handle_top_menu_item menu_name, item, with_submenu: true
|
||||
end
|
||||
|
||||
def handle_top_menu_item(menu_name, item, with_submenu: false)
|
||||
Redmine::MenuManager.map(:top_menu).delete(menu_name.to_sym) if Redmine::MenuManager.map(:top_menu).exists?(menu_name.to_sym)
|
||||
|
||||
html_options = {}
|
||||
html_options[:class] = 'external' if item[:url].include? '://'
|
||||
|
||||
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?
|
||||
|
||||
html_options[:title] = item[:title] if item[:title].present?
|
||||
|
||||
menu_options = { parent: item[:parent].present? ? item[:parent].to_sym : nil,
|
||||
|
@ -48,7 +57,7 @@ module AdditionalsMenuHelper
|
|||
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, item[:url], menu_options
|
||||
end
|
||||
|
||||
def render_custom_top_menu_item
|
||||
|
@ -56,14 +65,15 @@ module AdditionalsMenuHelper
|
|||
return if items.empty?
|
||||
|
||||
user_roles = Role.givable
|
||||
.joins(:members).where(members: { user_id: User.current.id })
|
||||
.joins(members: :project).where(projects: { status: Project::STATUS_ACTIVE })
|
||||
.joins(members: :project)
|
||||
.where(members: { user_id: User.current.id },
|
||||
projects: { status: Project::STATUS_ACTIVE })
|
||||
.distinct
|
||||
.reorder(nil)
|
||||
.pluck(:id)
|
||||
.ids
|
||||
|
||||
items.each do |item|
|
||||
additionals_custom_top_menu_item(item, user_roles)
|
||||
additionals_custom_top_menu_item item, user_roles
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -72,10 +82,10 @@ module AdditionalsMenuHelper
|
|||
Additionals::MAX_CUSTOM_MENU_ITEMS.times do |num|
|
||||
menu_name = "custom_menu#{num}"
|
||||
item = { menu_name: menu_name.to_sym,
|
||||
url: Additionals.settings[menu_name + '_url'],
|
||||
name: Additionals.settings[menu_name + '_name'],
|
||||
title: Additionals.settings[menu_name + '_title'],
|
||||
roles: Additionals.settings[menu_name + '_roles'] }
|
||||
url: Additionals.setting("#{menu_name}_url"),
|
||||
name: Additionals.setting("#{menu_name}_name"),
|
||||
title: Additionals.setting("#{menu_name}_title"),
|
||||
roles: Additionals.setting("#{menu_name}_roles") }
|
||||
|
||||
if item[:name].present? && item[:url].present? && item[:roles].present?
|
||||
items << item
|
||||
|
@ -110,7 +120,7 @@ module AdditionalsMenuHelper
|
|||
end
|
||||
|
||||
if show_entry
|
||||
handle_top_menu_item(item[:menu_name], item)
|
||||
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])
|
||||
end
|
||||
|
@ -118,12 +128,16 @@ module AdditionalsMenuHelper
|
|||
|
||||
def addtionals_help_plugin_items
|
||||
user_items = [{ title: 'Redmine Guide', url: Redmine::Info.help_url },
|
||||
{ title: "Redmine #{l(:label_macro_plural)}", url: additionals_macros_path }]
|
||||
{ title: "Redmine #{l :label_macro_plural}", url: additionals_macros_path }]
|
||||
|
||||
admin_items = [{ title: 'Additionals', url: 'https://additionals.readthedocs.io/en/latest/manual/', manual: true },
|
||||
{ title: 'Redmine Changelog', url: 'https://www.redmine.org/projects/redmine/wiki/Changelog_3_4' },
|
||||
{ title: 'Redmine Upgrade', url: 'https://www.redmine.org/projects/redmine/wiki/RedmineUpgrade' },
|
||||
{ title: 'Redmine Security Advisories', url: 'https://www.redmine.org/projects/redmine/wiki/Security_Advisories' }]
|
||||
admin_items = [{ title: 'Additionals',
|
||||
url: 'https://additionals.readthedocs.io/en/latest/manual/', manual: true },
|
||||
{ title: 'Redmine Changelog',
|
||||
url: "https://www.redmine.org/projects/redmine/wiki/Changelog_#{Redmine::VERSION::MAJOR}_#{Redmine::VERSION::MINOR}" },
|
||||
{ title: 'Redmine Upgrade',
|
||||
url: 'https://www.redmine.org/projects/redmine/wiki/RedmineUpgrade' },
|
||||
{ title: 'Redmine Security Advisories',
|
||||
url: 'https://www.redmine.org/projects/redmine/wiki/Security_Advisories' }]
|
||||
|
||||
Redmine::Plugin.all.each do |plugin|
|
||||
next if plugin.id == :additionals
|
||||
|
@ -145,7 +159,7 @@ module AdditionalsMenuHelper
|
|||
|
||||
plugin_item.each do |temp_item|
|
||||
u_items = if !temp_item[:manual].nil? && temp_item[:manual]
|
||||
{ title: "#{temp_item[:title]} #{l(:label_help_manual)}", url: temp_item[:url] }
|
||||
{ title: "#{temp_item[:title]} #{l :label_help_manual}", url: temp_item[:url] }
|
||||
else
|
||||
{ title: temp_item[:title], url: temp_item[:url] }
|
||||
end
|
||||
|
@ -173,18 +187,17 @@ module AdditionalsMenuHelper
|
|||
s = []
|
||||
pages.each_with_index do |item, idx|
|
||||
s << if item[:title] == '-'
|
||||
content_tag(:li, tag(:hr))
|
||||
tag.li tag.hr
|
||||
else
|
||||
html_options = { class: 'help_item_' + idx.to_s }
|
||||
html_options = { class: "help_item_#{idx}" }
|
||||
if item[:url].include? '://'
|
||||
html_options[:class] << ' external'
|
||||
html_options[:target] = '_blank'
|
||||
end
|
||||
content_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)
|
||||
safe_join s
|
||||
end
|
||||
|
||||
# Plugin help items definition for plugins,
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
module AdditionalsProjectsHelper
|
||||
def project_overview_name(_project, dashboard = nil)
|
||||
name = [l(:label_overview)]
|
||||
name << dashboard.name if dashboard.present? && (dashboard.always_expose? || !dashboard.system_default)
|
||||
|
||||
safe_join name, Additionals::LIST_SEPARATOR
|
||||
end
|
||||
end
|
|
@ -52,9 +52,9 @@ module AdditionalsQueriesHelper
|
|||
end
|
||||
|
||||
def additionals_load_query_id(query_class, session_key, query_id, options, object_type)
|
||||
cond = 'project_id IS NULL'
|
||||
cond << " OR project_id = #{@project.id}" if @project
|
||||
@query = query_class.where(cond).find(query_id)
|
||||
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
|
||||
|
@ -72,19 +72,20 @@ module AdditionalsQueriesHelper
|
|||
end
|
||||
|
||||
def additionals_query_cache_key(object_type)
|
||||
project_id = @project.nil? ? 0 : @project.id
|
||||
project_id = @project ? @project.id : 0
|
||||
"#{object_type}_query_data_#{session.id}_#{project_id}"
|
||||
end
|
||||
|
||||
def additionals_select2_search_users(where_filter = '', where_params = {})
|
||||
def additionals_select2_search_users(options = {})
|
||||
q = params[:q].to_s.strip
|
||||
exclude_id = params[:user_id].to_i
|
||||
scope = User.active.where(type: 'User')
|
||||
scope = scope.where.not(id: exclude_id) if exclude_id > 0
|
||||
scope = scope.where(where_filter, where_params) if where_filter.present?
|
||||
scope = scope.like(q) if q.present?
|
||||
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.order(last_login_on: :desc)
|
||||
.limit(params[:limit] || Additionals::SELECT2_INIT_ENTRIES)
|
||||
.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
|
||||
|
@ -97,9 +98,10 @@ module AdditionalsQueriesHelper
|
|||
query.columns
|
||||
end
|
||||
|
||||
stream = StringIO.new('')
|
||||
export_to_xlsx(items, columns, filename: stream)
|
||||
stream.string
|
||||
options[:filename] = StringIO.new('')
|
||||
|
||||
export_to_xlsx(items, columns, options)
|
||||
options[:filename].string
|
||||
end
|
||||
|
||||
def additionals_result_to_xlsx(items, columns, options = {})
|
||||
|
@ -161,10 +163,18 @@ module AdditionalsQueriesHelper
|
|||
columns.each_with_index do |c, column_index|
|
||||
value = csv_content(c, line)
|
||||
if c.name == :id # ID
|
||||
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)
|
||||
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)
|
||||
else
|
||||
# id without link
|
||||
worksheet.write(line_index + 1,
|
||||
column_index,
|
||||
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, hyperlink_format, 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")
|
||||
|
@ -188,7 +198,7 @@ module AdditionalsQueriesHelper
|
|||
value_str = value.to_s
|
||||
|
||||
# 1.1: margin
|
||||
width = (value_str.length + value_str.chars.reject(&:ascii_only?).length) * 1.1 + 1
|
||||
width = (value_str.length + value_str.chars.count { |e| !e.ascii_only? }) * 1.1 + 1
|
||||
# 30: max width
|
||||
width > 30 ? 30 : width
|
||||
end
|
||||
|
@ -206,7 +216,7 @@ module AdditionalsQueriesHelper
|
|||
format[:bg_color] = 'silver' unless index.even?
|
||||
else
|
||||
format[:bg_color] = 'silver' unless index.even?
|
||||
format[:color] = 'red' if value.is_a?(Numeric) && value < 0
|
||||
format[:color] = 'red' if value.is_a?(Numeric) && value.negative?
|
||||
end
|
||||
|
||||
format
|
||||
|
@ -214,13 +224,13 @@ module AdditionalsQueriesHelper
|
|||
|
||||
def xlsx_hyperlink_cell?(token)
|
||||
# Match http, https or ftp URL
|
||||
if token =~ %r{\A[fh]tt?ps?://}
|
||||
if %r{\A[fh]tt?ps?://}.match?(token)
|
||||
true
|
||||
# Match mailto:
|
||||
elsif token.present? && token.start_with?('mailto:')
|
||||
true
|
||||
# Match internal or external sheet link
|
||||
elsif token =~ /\A(?:in|ex)ternal:/
|
||||
elsif /\A(?:in|ex)ternal:/.match?(token)
|
||||
true
|
||||
end
|
||||
end
|
||||
|
@ -229,7 +239,7 @@ module AdditionalsQueriesHelper
|
|||
# columns in ignored_column_names are skipped (names as symbols)
|
||||
# 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, ignored_column_names = nil)
|
||||
def query_as_hidden_field_tags(query)
|
||||
tags = hidden_field_tag('set_filter', '1', id: nil)
|
||||
|
||||
if query.filters.present?
|
||||
|
@ -243,8 +253,10 @@ module AdditionalsQueriesHelper
|
|||
else
|
||||
tags << hidden_field_tag('f[]', '', id: nil)
|
||||
end
|
||||
|
||||
ignored_block_columns = query.block_columns.map(&:name)
|
||||
query.columns.each do |column|
|
||||
next if ignored_column_names.present? && ignored_column_names.include?(column.name)
|
||||
next if ignored_block_columns.include?(column.name)
|
||||
|
||||
tags << hidden_field_tag('c[]', column.name, id: nil)
|
||||
end
|
||||
|
@ -258,4 +270,11 @@ 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
|
||||
|
|
82
plugins/additionals/app/helpers/additionals_routes_helper.rb
Normal file
82
plugins/additionals/app/helpers/additionals_routes_helper.rb
Normal file
|
@ -0,0 +1,82 @@
|
|||
module AdditionalsRoutesHelper
|
||||
def _dashboards_path(project, *args)
|
||||
if project
|
||||
project_dashboards_path(project, *args)
|
||||
else
|
||||
dashboards_path(*args)
|
||||
end
|
||||
end
|
||||
|
||||
def _dashboard_path(project, *args)
|
||||
if project
|
||||
project_dashboard_path(project, *args)
|
||||
else
|
||||
dashboard_path(*args)
|
||||
end
|
||||
end
|
||||
|
||||
def _dashboard_async_blocks_path(project, *args)
|
||||
if project
|
||||
project_dashboard_async_blocks_path(project, *args)
|
||||
else
|
||||
dashboard_async_blocks_path(*args)
|
||||
end
|
||||
end
|
||||
|
||||
def _edit_dashboard_path(project, *args)
|
||||
if project
|
||||
edit_project_dashboard_path(project, *args)
|
||||
else
|
||||
edit_dashboard_path(*args)
|
||||
end
|
||||
end
|
||||
|
||||
def _new_dashboard_path(project, *args)
|
||||
if project
|
||||
new_project_dashboard_path(project, *args)
|
||||
else
|
||||
new_dashboard_path(*args)
|
||||
end
|
||||
end
|
||||
|
||||
def _update_layout_setting_dashboard_path(project, *args)
|
||||
if project
|
||||
update_layout_setting_project_dashboard_path(project, *args)
|
||||
else
|
||||
update_layout_setting_dashboard_path(*args)
|
||||
end
|
||||
end
|
||||
|
||||
def _add_block_dashboard_path(project, *args)
|
||||
if project
|
||||
add_block_project_dashboard_path(project, *args)
|
||||
else
|
||||
add_block_dashboard_path(*args)
|
||||
end
|
||||
end
|
||||
|
||||
def _remove_block_dashboard_path(project, *args)
|
||||
if project
|
||||
remove_block_project_dashboard_path(project, *args)
|
||||
else
|
||||
remove_block_dashboard_path(*args)
|
||||
end
|
||||
end
|
||||
|
||||
def _order_blocks_dashboard_path(project, *args)
|
||||
if project
|
||||
order_blocks_project_dashboard_path(project, *args)
|
||||
else
|
||||
order_blocks_dashboard_path(*args)
|
||||
end
|
||||
end
|
||||
|
||||
def dashboard_link_path(project, dashboard, options = {})
|
||||
options[:dashboard_id] = dashboard.id
|
||||
if dashboard.dashboard_type == DashboardContentProject::TYPE_NAME
|
||||
project_path project, options
|
||||
else
|
||||
home_path options
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,13 @@
|
|||
module AdditionalsSelect2Helper
|
||||
def additionals_select2_tag(name, option_tags = nil, options = {})
|
||||
s = select_tag(name, option_tags, options)
|
||||
s << hidden_field_tag("#{name}[]", '') if options[:multiple] && options.fetch(:include_hidden, true)
|
||||
|
||||
s + javascript_tag("select2Tag('#{sanitize_to_id name}', #{options.to_json});")
|
||||
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?
|
||||
end
|
||||
end
|
|
@ -0,0 +1,59 @@
|
|||
module AdditionalsSettingsHelper
|
||||
def additionals_settings_tabs
|
||||
tabs = [{ name: 'general', partial: 'additionals/settings/general', label: :label_general },
|
||||
{ name: 'wiki', partial: 'additionals/settings/wiki', label: :label_wiki },
|
||||
{ name: 'macros', partial: 'additionals/settings/macros', label: :label_macro_plural },
|
||||
{ name: 'rules', partial: 'additionals/settings/issues', label: :label_issue_plural },
|
||||
{ name: 'users', partial: 'additionals/settings/users', label: :label_user_plural },
|
||||
{ name: 'web', partial: 'additionals/settings/web_apis', label: :label_web_apis }]
|
||||
|
||||
unless Redmine::Plugin.installed? 'redmine_hrm'
|
||||
tabs << { name: 'menu', partial: 'additionals/settings/menu', label: :label_settings_menu }
|
||||
end
|
||||
|
||||
tabs
|
||||
end
|
||||
|
||||
def additionals_settings_checkbox(name, options = {})
|
||||
label_title = options.delete(:label).presence || l("label_#{name}")
|
||||
value = options.delete :value
|
||||
value_is_bool = options.delete :value_is_bool
|
||||
custom_value = if value.nil?
|
||||
value = 1
|
||||
false
|
||||
else
|
||||
value = 1 if value_is_bool
|
||||
true
|
||||
end
|
||||
|
||||
checked = if custom_value && !value_is_bool
|
||||
@settings[name]
|
||||
else
|
||||
Additionals.true? @settings[name]
|
||||
end
|
||||
|
||||
s = [label_tag("settings[#{name}]", label_title)]
|
||||
s << hidden_field_tag("settings[#{name}]", 0, id: nil) if !custom_value || value_is_bool
|
||||
s << check_box_tag("settings[#{name}]", value, checked, options)
|
||||
safe_join s
|
||||
end
|
||||
|
||||
def additionals_settings_textfield(name, options = {})
|
||||
label_title = options.delete(:label).presence || l("label_#{name}")
|
||||
value = options.delete(:value).presence || @settings[name]
|
||||
|
||||
safe_join [label_tag("settings[#{name}]", label_title),
|
||||
text_field_tag("settings[#{name}]", value, options)]
|
||||
end
|
||||
|
||||
def additionals_settings_textarea(name, options = {})
|
||||
label_title = options.delete(:label).presence || l("label_#{name}")
|
||||
value = options.delete(:value).presence || @settings[name]
|
||||
|
||||
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)]
|
||||
end
|
||||
end
|
|
@ -1,16 +1,6 @@
|
|||
require 'digest/md5'
|
||||
|
||||
module AdditionalsTagHelper
|
||||
# deprecated: this will removed after a while
|
||||
def render_additionals_tags_list(tags, options = {})
|
||||
additionals_tag_cloud(tags, options)
|
||||
end
|
||||
|
||||
# deprecated: this will removed after a while
|
||||
def render_additionals_tag_link_line(tag_list)
|
||||
additionals_tag_links(tag_list)
|
||||
end
|
||||
|
||||
def additionals_tag_cloud(tags, options = {})
|
||||
return if tags.blank?
|
||||
|
||||
|
@ -31,7 +21,7 @@ module AdditionalsTagHelper
|
|||
' '
|
||||
end
|
||||
|
||||
content_tag(:div, safe_join(s, sep), class: 'tags')
|
||||
tag.div safe_join(s, sep), class: 'tags'
|
||||
end
|
||||
|
||||
# plain list of tags
|
||||
|
@ -54,33 +44,31 @@ module AdditionalsTagHelper
|
|||
end
|
||||
|
||||
safe_join(tag_list.map do |tag|
|
||||
additionals_tag_link(tag, options)
|
||||
additionals_tag_link tag, options
|
||||
end, sep)
|
||||
end
|
||||
|
||||
def additionals_tag_link(tag, options = {})
|
||||
def additionals_tag_link(tag_object, options = {})
|
||||
tag_name = []
|
||||
tag_name << tag.name
|
||||
tag_name << tag_object.name
|
||||
|
||||
unless options[:tags_without_color]
|
||||
tag_bg_color = additionals_tag_color(tag.name)
|
||||
tag_fg_color = additionals_tag_fg_color(tag_bg_color)
|
||||
tag_bg_color = additionals_tag_color tag_object.name
|
||||
tag_fg_color = additionals_tag_fg_color tag_bg_color
|
||||
tag_style = "background-color: #{tag_bg_color}; color: #{tag_fg_color}"
|
||||
end
|
||||
|
||||
tag_name << content_tag('span', "(#{tag.count})", class: 'tag-count') if options[:show_count]
|
||||
tag_name << tag.span("(#{tag_object.count})", class: 'tag-count') if options[:show_count]
|
||||
|
||||
if options[:tags_without_color]
|
||||
content_tag('span',
|
||||
link_to(safe_join(tag_name), additionals_tag_url(tag.name, options)),
|
||||
class: 'tag-label')
|
||||
tag.span link_to(safe_join(tag_name), additionals_tag_url(tag_object.name, options)),
|
||||
class: 'tag-label'
|
||||
else
|
||||
content_tag('span',
|
||||
link_to(safe_join(tag_name),
|
||||
additionals_tag_url(tag.name, options),
|
||||
style: tag_style),
|
||||
class: 'additionals-tag-label-color',
|
||||
style: tag_style)
|
||||
tag.span link_to(safe_join(tag_name),
|
||||
additionals_tag_url(tag_object.name, options),
|
||||
style: tag_style),
|
||||
class: 'additionals-tag-label-color',
|
||||
style: tag_style
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -90,7 +78,7 @@ module AdditionalsTagHelper
|
|||
{ controller: options[:tag_controller].presence || controller_name,
|
||||
action: action,
|
||||
set_filter: 1,
|
||||
project_id: @project,
|
||||
project_id: options[:project],
|
||||
fields: [:tags],
|
||||
values: { tags: [tag_name] },
|
||||
operators: { tags: '=' } }
|
||||
|
|
459
plugins/additionals/app/helpers/dashboards_helper.rb
Normal file
459
plugins/additionals/app/helpers/dashboards_helper.rb
Normal file
|
@ -0,0 +1,459 @@
|
|||
module DashboardsHelper
|
||||
def dashboard_sidebar?(dashboard, params)
|
||||
if params['enable_sidebar'].blank?
|
||||
if dashboard.blank?
|
||||
# defaults without dashboard
|
||||
!@project.nil?
|
||||
else
|
||||
dashboard.enable_sidebar?
|
||||
end
|
||||
else
|
||||
Additionals.true? params['enable_sidebar']
|
||||
end
|
||||
end
|
||||
|
||||
def welcome_overview_name(dashboard = nil)
|
||||
name = [l(:label_home)]
|
||||
name << dashboard.name if dashboard&.always_expose? || dashboard.present? && !dashboard.system_default?
|
||||
|
||||
safe_join name, Additionals::LIST_SEPARATOR
|
||||
end
|
||||
|
||||
def dashboard_css_classes(dashboard)
|
||||
classes = ['dashboard', dashboard.dashboard_type.underscore, "dashboard-#{dashboard.id}"]
|
||||
safe_join classes, ' '
|
||||
end
|
||||
|
||||
def sidebar_dashboards(dashboard, project = nil, user = nil)
|
||||
user ||= User.current
|
||||
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))
|
||||
else
|
||||
scope.where dashboard_type: dashboard.dashboard_type
|
||||
end
|
||||
|
||||
scope.sorted.to_a
|
||||
end
|
||||
|
||||
def render_dashboard_actionlist(active_dashboard, project = nil)
|
||||
dashboards = sidebar_dashboards active_dashboard, project
|
||||
base_css = 'icon icon-dashboard'
|
||||
out = []
|
||||
|
||||
dashboards.select!(&:public?) unless User.current.allowed_to? :save_dashboards, project, global: true
|
||||
dashboards.each do |dashboard|
|
||||
css_class = base_css
|
||||
dashboard_name = "#{l :label_dashboard}: #{dashboard.name}"
|
||||
out << if dashboard.id == active_dashboard.id
|
||||
link_to dashboard_name, '#',
|
||||
onclick: 'return false;',
|
||||
class: "#{base_css} disabled"
|
||||
else
|
||||
dashboard_link dashboard, project,
|
||||
class: css_class,
|
||||
title: l(:label_change_to_dashboard),
|
||||
name: dashboard_name
|
||||
end
|
||||
end
|
||||
|
||||
safe_join out
|
||||
end
|
||||
|
||||
def render_sidebar_dashboards(dashboard, project = nil)
|
||||
dashboards = sidebar_dashboards dashboard, project
|
||||
out = [dashboard_links(l(:label_my_dashboard_plural),
|
||||
dashboard,
|
||||
User.current.allowed_to?(:save_dashboards, project, global: true) ? dashboards.select(&:private?) : [],
|
||||
project),
|
||||
dashboard_links(l(:label_shared_dashboard_plural),
|
||||
dashboard,
|
||||
dashboards.select(&:public?),
|
||||
project)]
|
||||
|
||||
out << dashboard_info(dashboard) if dashboard.always_expose? || !dashboard.system_default
|
||||
|
||||
safe_join out
|
||||
end
|
||||
|
||||
def dashboard_info(dashboard)
|
||||
tag.div class: 'active-dashboards' do
|
||||
out = [tag.h3(l(:label_active_dashboard)),
|
||||
tag.ul do
|
||||
concat tag.ul "#{l :field_name}: #{dashboard.name}"
|
||||
concat tag.ul safe_join([l(:field_author), link_to_user(dashboard.author)], ': ')
|
||||
concat tag.ul "#{l :field_created_on}: #{format_time dashboard.created_at}"
|
||||
concat tag.ul "#{l :field_updated_on}: #{format_time dashboard.updated_at}"
|
||||
end]
|
||||
|
||||
if dashboard.description.present?
|
||||
out << tag.div(textilizable(dashboard, :description, inline_attachments: false),
|
||||
class: 'dashboard-description')
|
||||
end
|
||||
|
||||
safe_join out
|
||||
end
|
||||
end
|
||||
|
||||
def dashboard_links(title, active_dashboard, dashboards, project)
|
||||
return '' unless dashboards.any?
|
||||
|
||||
tag.h3(title, class: 'dashboards') +
|
||||
tag.ul do
|
||||
dashboards.each do |dashboard|
|
||||
selected = dashboard.id == if params[:dashboard_id].present?
|
||||
params[:dashboard_id].to_i
|
||||
else
|
||||
active_dashboard.id
|
||||
end
|
||||
|
||||
css = 'dashboard'
|
||||
css << ' selected' if selected
|
||||
link = [dashboard_link(dashboard, project, class: css)]
|
||||
if dashboard.system_default?
|
||||
link << if dashboard.project_id.nil?
|
||||
font_awesome_icon('fas_cube',
|
||||
title: l(:field_system_default),
|
||||
class: 'dashboard-system-default global')
|
||||
else
|
||||
font_awesome_icon('fas_cube',
|
||||
title: l(:field_project_system_default),
|
||||
class: 'dashboard-system-default project')
|
||||
end
|
||||
end
|
||||
concat tag.li safe_join(link)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
name = options.delete(:name) || dashboard.name
|
||||
link_to name, dashboard_link_path(project, dashboard), options
|
||||
end
|
||||
|
||||
def sidebar_action_toggle(enabled, dashboard, project = nil)
|
||||
return if dashboard.nil?
|
||||
|
||||
if enabled
|
||||
link_to l(:label_disable_sidebar),
|
||||
dashboard_link_path(project, dashboard, enable_sidebar: 0),
|
||||
class: 'icon icon-sidebar'
|
||||
else
|
||||
link_to l(:label_enable_sidebar),
|
||||
dashboard_link_path(project, dashboard, enable_sidebar: 1),
|
||||
class: 'icon icon-sidebar'
|
||||
end
|
||||
end
|
||||
|
||||
def delete_dashboard_link(url, options = {})
|
||||
options = { method: :delete,
|
||||
data: { confirm: l(:text_are_you_sure) },
|
||||
class: 'icon icon-del' }.merge(options)
|
||||
|
||||
link_to l(:button_dashboard_delete), url, options
|
||||
end
|
||||
|
||||
# Returns the select tag used to add or remove a block
|
||||
def dashboard_block_select_tag(dashboard)
|
||||
blocks_in_use = dashboard.layout.values.flatten
|
||||
options = tag.option "<< #{l :label_add_dashboard_block} >>", value: ''
|
||||
dashboard.content.block_options(blocks_in_use).each do |label, block|
|
||||
options << tag.option(label, value: block, disabled: block.blank?)
|
||||
end
|
||||
select_tag 'block',
|
||||
options,
|
||||
id: 'block-select',
|
||||
class: 'dashboard-block-select',
|
||||
onchange: "$('#block-form').submit();"
|
||||
end
|
||||
|
||||
# Renders the blocks
|
||||
def render_dashboard_blocks(blocks, dashboard, _options = {})
|
||||
s = ''.html_safe
|
||||
|
||||
if blocks.present?
|
||||
blocks.each do |block|
|
||||
s << render_dashboard_block(block, dashboard).to_s
|
||||
end
|
||||
end
|
||||
s
|
||||
end
|
||||
|
||||
# Renders a single block
|
||||
def render_dashboard_block(block, dashboard, overwritten_settings = {})
|
||||
block_definition = dashboard.content.find_block block
|
||||
unless block_definition
|
||||
Rails.logger.info "Unknown block \"#{block}\" found in #{dashboard.name} (id=#{dashboard.id})"
|
||||
return
|
||||
end
|
||||
|
||||
content = render_dashboard_block_content block, block_definition, dashboard, overwritten_settings
|
||||
return if content.blank?
|
||||
|
||||
if dashboard.editable?
|
||||
icons = []
|
||||
if block_definition[:no_settings].blank? &&
|
||||
(!block_definition.key?(:with_settings_if) || block_definition[:with_settings_if].call(@project))
|
||||
icons << link_to_function(l(:label_options),
|
||||
"$('##{block}-settings').toggle();",
|
||||
class: 'icon-only icon-settings',
|
||||
title: l(:label_options))
|
||||
end
|
||||
icons << tag.span('', class: 'icon-only icon-sort-handle sort-handle', title: l(:button_move))
|
||||
icons << link_to(l(:button_delete),
|
||||
_remove_block_dashboard_path(@project, dashboard, block: block),
|
||||
remote: true, method: 'post',
|
||||
class: 'icon-only icon-close', title: l(:button_delete))
|
||||
|
||||
content = tag.div(safe_join(icons), class: 'contextual') + content
|
||||
end
|
||||
|
||||
tag.div content, class: 'mypage-box', id: "block-#{block}"
|
||||
end
|
||||
|
||||
def build_dashboard_partial_locals(block, block_definition, settings, dashboard)
|
||||
partial_locals = { dashboard: dashboard,
|
||||
settings: settings,
|
||||
block: block,
|
||||
block_definition: block_definition,
|
||||
user: User.current }
|
||||
|
||||
if block_definition[:query_block]
|
||||
partial_locals[:query_block] = block_definition[:query_block]
|
||||
partial_locals[:klass] = block_definition[:query_block][:class]
|
||||
partial_locals[:async] = { required_settings: %i[query_id],
|
||||
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]
|
||||
elsif block_definition[:async]
|
||||
partial_locals[:async] = block_definition[:async]
|
||||
end
|
||||
|
||||
partial_locals
|
||||
end
|
||||
|
||||
def dashboard_async_required_settings?(settings, async)
|
||||
return true if async[:required_settings].blank?
|
||||
return false if settings.blank?
|
||||
|
||||
async[:required_settings].each do |required_setting|
|
||||
return false if settings.exclude?(required_setting) || settings[required_setting].blank?
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def dashboard_query_list_block_title(query, query_block, project)
|
||||
title = []
|
||||
title << query.project if project.nil? && query.project
|
||||
title << query_block[:label]
|
||||
|
||||
title << if query_block[:with_project]
|
||||
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))
|
||||
end
|
||||
|
||||
safe_join title, Additionals::LIST_SEPARATOR
|
||||
end
|
||||
|
||||
def dashboard_query_list_block_alerts(dashboard, query, block_definition)
|
||||
return if dashboard.visibility == Dashboard::VISIBILITY_PRIVATE
|
||||
|
||||
title = if query.visibility == Query::VISIBILITY_PRIVATE
|
||||
l(:alert_only_visible_by_yourself)
|
||||
elsif block_definition.key?(:admin_only) && block_definition[:admin_only]
|
||||
l(:alert_only_visible_by_admins)
|
||||
end
|
||||
|
||||
return if title.nil?
|
||||
|
||||
font_awesome_icon('fas_info-circle',
|
||||
title: title,
|
||||
class: 'dashboard-block-alert')
|
||||
end
|
||||
|
||||
def render_legacy_left_block(_block, _block_definition, _settings, _dashboard)
|
||||
if @project
|
||||
call_hook :view_projects_show_left, project: @project
|
||||
else
|
||||
call_hook :view_welcome_index_left
|
||||
end
|
||||
end
|
||||
|
||||
def render_legacy_right_block(_block, _block_definition, _settings, _dashboard)
|
||||
if @project
|
||||
call_hook :view_projects_show_right, project: @project
|
||||
else
|
||||
call_hook :view_welcome_index_right
|
||||
end
|
||||
end
|
||||
|
||||
# copied from my_helper
|
||||
def render_documents_block(block, _block_definition, settings, dashboard)
|
||||
max_entries = settings[:max_entries] || DashboardContent::DEFAULT_MAX_ENTRIES
|
||||
|
||||
scope = Document.visible
|
||||
scope = scope.where(project: dashboard.project) if dashboard.project
|
||||
|
||||
documents = scope.order(created_on: :desc)
|
||||
.limit(max_entries)
|
||||
.to_a
|
||||
|
||||
render partial: 'dashboards/blocks/documents', locals: { block: block,
|
||||
max_entries: max_entries,
|
||||
documents: documents }
|
||||
end
|
||||
|
||||
def render_news_block(block, _block_definition, settings, dashboard)
|
||||
max_entries = settings[:max_entries] || DashboardContent::DEFAULT_MAX_ENTRIES
|
||||
|
||||
news = if dashboard.content_project.nil?
|
||||
News.latest User.current
|
||||
else
|
||||
dashboard.content_project
|
||||
.news
|
||||
.limit(max_entries)
|
||||
.includes(:author, :project)
|
||||
.reorder(created_on: :desc)
|
||||
.to_a
|
||||
end
|
||||
|
||||
render partial: 'dashboards/blocks/news', locals: { block: block,
|
||||
max_entries: max_entries,
|
||||
news: news }
|
||||
end
|
||||
|
||||
def render_my_spent_time_block(block, block_definition, settings, dashboard)
|
||||
days = settings[:days].to_i
|
||||
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?
|
||||
|
||||
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,
|
||||
block_definition: block_definition,
|
||||
entries_today: entries_today,
|
||||
entries_days: entries_days,
|
||||
days: days }
|
||||
end
|
||||
|
||||
def activity_dashboard_data(settings, dashboard)
|
||||
max_entries = (settings[:max_entries] || DashboardContent::DEFAULT_MAX_ENTRIES).to_i
|
||||
user = User.current
|
||||
options = {}
|
||||
options[:author] = user if Additionals.true? settings[:me_only]
|
||||
options[:project] = dashboard.content_project if dashboard.content_project.present?
|
||||
|
||||
Redmine::Activity::Fetcher.new(user, options)
|
||||
.events(nil, nil, limit: max_entries)
|
||||
.group_by { |event| user.time_to_date(event.event_datetime) }
|
||||
end
|
||||
|
||||
def dashboard_feed_catcher(url, max_entries)
|
||||
feed = { items: [], valid: false }
|
||||
return feed if url.blank?
|
||||
|
||||
cnt = 0
|
||||
max_entries = max_entries.present? ? max_entries.to_i : 10
|
||||
|
||||
begin
|
||||
URI.open(url) do |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,
|
||||
link: item.link.try(:href)&.presence || item.link }
|
||||
break if cnt >= max_entries
|
||||
end
|
||||
end
|
||||
rescue StandardError => e
|
||||
Rails.logger.info "dashboard_feed_catcher error for #{url}: #{e}"
|
||||
return feed
|
||||
end
|
||||
|
||||
feed[:valid] = true
|
||||
|
||||
feed
|
||||
end
|
||||
|
||||
def dashboard_feed_title(title, block_definition)
|
||||
title.presence || block_definition[:label]
|
||||
end
|
||||
|
||||
def options_for_query_select(klass, project)
|
||||
# sidebar_queries cannot be use because descendants classes are included
|
||||
# this changes on class loading
|
||||
# queries = klass.visible.global_or_on_project(@project).sorted.to_a
|
||||
queries = klass.visible
|
||||
.global_or_on_project(project)
|
||||
.where(type: klass.to_s)
|
||||
.sorted.to_a
|
||||
|
||||
tag.option + options_from_collection_for_select(queries, :id, :name)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# 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?
|
||||
|
||||
partial = block_definition[:partial]
|
||||
partial_locals = build_dashboard_partial_locals block, block_definition, settings, dashboard
|
||||
|
||||
if block_definition[:query_block] || block_definition[:async]
|
||||
render partial: 'dashboards/blocks/async', locals: partial_locals
|
||||
elsif partial
|
||||
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})")
|
||||
nil
|
||||
end
|
||||
else
|
||||
send "render_#{block_definition[:name]}_block",
|
||||
block,
|
||||
block_definition,
|
||||
settings,
|
||||
dashboard
|
||||
end
|
||||
end
|
||||
|
||||
def resently_used_dashboard_save(dashboard, project = nil)
|
||||
user = User.current
|
||||
dashboard_type = dashboard.dashboard_type
|
||||
recently_id = user.pref.recently_used_dashboard dashboard_type, project
|
||||
return if recently_id == dashboard.id || user.anonymous?
|
||||
|
||||
if dashboard_type == DashboardContentProject::TYPE_NAME
|
||||
user.pref.recently_used_dashboards[dashboard_type] = {} if user.pref.recently_used_dashboards[dashboard_type].nil?
|
||||
user.pref.recently_used_dashboards[dashboard_type][project.id] = dashboard.id
|
||||
else
|
||||
user.pref.recently_used_dashboards[dashboard_type] = dashboard.id
|
||||
end
|
||||
|
||||
user.pref.save
|
||||
end
|
||||
end
|
7
plugins/additionals/app/jobs/additionals_job.rb
Normal file
7
plugins/additionals/app/jobs/additionals_job.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
class AdditionalsJob < ActiveJob::Base
|
||||
# Automatically retry jobs that encountered a deadlock
|
||||
# retry_on ActiveRecord::Deadlocked
|
||||
|
||||
# Most jobs are safe to ignore if the underlying records are no longer available
|
||||
# discard_on ActiveJob::DeserializationError
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
class AdditionalsRemoveUnusedTagJob < AdditionalsJob
|
||||
def perform
|
||||
AdditionalsTag.remove_unused_tags
|
||||
end
|
||||
end
|
64
plugins/additionals/app/models/additionals_chart.rb
Normal file
64
plugins/additionals/app/models/additionals_chart.rb
Normal file
|
@ -0,0 +1,64 @@
|
|||
class AdditionalsChart < ActiveRecord::Base
|
||||
include Redmine::I18n
|
||||
|
||||
CHART_DEFAULT_HEIGHT = 350
|
||||
CHART_DEFAULT_WIDTH = 400
|
||||
|
||||
class << self
|
||||
def color_schema
|
||||
Redmine::Plugin.installed?('redmine_reporting') ? RedmineReporting.setting(:chart_color_schema) : 'tableau.Classic20'
|
||||
end
|
||||
|
||||
def data
|
||||
raise 'overwrite it!'
|
||||
end
|
||||
|
||||
# build return value
|
||||
def build_chart_data(datasets, options = {})
|
||||
cached_labels = labels
|
||||
data = { datasets: datasets.to_json,
|
||||
labels: cached_labels.keys,
|
||||
label_ids: cached_labels.values }
|
||||
|
||||
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[:color_schema] = color_schema
|
||||
|
||||
data.merge(options)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_values_without_gaps(data, gap_value = 0)
|
||||
values = []
|
||||
labels.each do |label, _label_id|
|
||||
values << if data.key?(label)
|
||||
data[label]
|
||||
else
|
||||
gap_value
|
||||
end
|
||||
end
|
||||
|
||||
values
|
||||
end
|
||||
|
||||
def init_labels
|
||||
@labels = {}
|
||||
end
|
||||
|
||||
def labels
|
||||
# NOTE: do not sort it, because color changes if user switch language
|
||||
@labels.to_h
|
||||
end
|
||||
|
||||
def add_label(label, id)
|
||||
return if @labels.key? label
|
||||
|
||||
@labels[label] = id
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,12 +1,12 @@
|
|||
class AdditionalsFontAwesome
|
||||
include Redmine::I18n
|
||||
|
||||
FORMAT_REGEXP = /\Afa[rsb]_[a-zA-Z0-9]+[a-zA-Z0-9\-]*\z/.freeze
|
||||
SEARCH_LIMIT = 50
|
||||
|
||||
class << self
|
||||
def load_icons(type)
|
||||
data = YAML.safe_load(ERB.new(IO.read(Rails.root.join('plugins',
|
||||
'additionals',
|
||||
'config',
|
||||
'fontawesome_icons.yml'))).result) || {}
|
||||
data = YAML.safe_load(ERB.new(IO.read(Rails.root.join('plugins/additionals/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))
|
||||
|
@ -62,12 +62,6 @@ class AdditionalsFontAwesome
|
|||
FONTAWESOME_ICONS[type].collect { |fa_symbol, values| [values[:label], key2value(fa_symbol, type[-1])] }
|
||||
end
|
||||
|
||||
def json_for_select
|
||||
[{ text: l(:label_fontawesome_regular), children: json_values(:far) },
|
||||
{ text: l(:label_fontawesome_solid), children: json_values(:fas) },
|
||||
{ text: l(:label_fontawesome_brands), children: json_values(:fab) }].to_json
|
||||
end
|
||||
|
||||
# show only one value as current selected
|
||||
# (all other options are retrieved by select2
|
||||
def active_option_for_select(selected)
|
||||
|
@ -77,12 +71,6 @@ class AdditionalsFontAwesome
|
|||
[[info[:label], selected]]
|
||||
end
|
||||
|
||||
def options_for_select
|
||||
[[l(:label_fontawesome_regular), select_values(:far)],
|
||||
[l(:label_fontawesome_solid), select_values(:fas)],
|
||||
[l(:label_fontawesome_brands), select_values(:fab)]]
|
||||
end
|
||||
|
||||
def value_info(value, options = {})
|
||||
return {} if value.blank?
|
||||
|
||||
|
@ -104,8 +92,60 @@ class AdditionalsFontAwesome
|
|||
info
|
||||
end
|
||||
|
||||
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)
|
||||
cnt = icons.count
|
||||
return icons if cnt >= SEARCH_LIMIT
|
||||
|
||||
icons += search_in_type(:fas, search, selected_store, cnt)
|
||||
cnt = icons.count
|
||||
return icons if cnt >= SEARCH_LIMIT
|
||||
|
||||
icons + search_in_type(:fab, search, selected_store, cnt)
|
||||
end
|
||||
|
||||
def convert2mermaid(icon)
|
||||
return if icon.blank?
|
||||
|
||||
parts = icon.split('_')
|
||||
return unless parts.count == 2
|
||||
|
||||
"#{parts.first}:fa-#{parts.last}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def search_in_type(type, search, selected_store, cnt = 0)
|
||||
icons = []
|
||||
|
||||
search_length = search.to_s.length
|
||||
first_letter_search = if search_length == 1
|
||||
search[0].downcase
|
||||
elsif search_length.zero? && selected_store.any?
|
||||
selected = selected_store.first
|
||||
fa = selected.split('_')
|
||||
search = fa[1][0] if fa.count > 1
|
||||
search
|
||||
end
|
||||
|
||||
FONTAWESOME_ICONS[type].each do |fa_symbol, values|
|
||||
break if SEARCH_LIMIT == cnt
|
||||
|
||||
id = key2value(fa_symbol, type[-1])
|
||||
next if selected_store.exclude?(id) &&
|
||||
search.present? &&
|
||||
(first_letter_search.present? && !values[:label].downcase.start_with?(first_letter_search) ||
|
||||
first_letter_search.blank? && values[:label] !~ /#{search}/i)
|
||||
|
||||
icons << { id: id, text: values[:label] }
|
||||
cnt += 1
|
||||
end
|
||||
|
||||
icons
|
||||
end
|
||||
|
||||
def load_details(type, name)
|
||||
return {} unless FONTAWESOME_ICONS.key?(type)
|
||||
|
||||
|
|
|
@ -25,8 +25,6 @@ class AdditionalsImport < Import
|
|||
value = case v.custom_field.field_format
|
||||
when 'date'
|
||||
row_date(row, "cf_#{v.custom_field.id}")
|
||||
when 'list'
|
||||
row_value(row, "cf_#{v.custom_field.id}").try(:split, ',')
|
||||
else
|
||||
row_value(row, "cf_#{v.custom_field.id}")
|
||||
end
|
||||
|
@ -34,7 +32,7 @@ class AdditionalsImport < Import
|
|||
|
||||
h[v.custom_field.id.to_s] =
|
||||
if value.is_a?(Array)
|
||||
value.map { |val| v.custom_field.value_from_keyword(val.strip, object) }.compact.flatten
|
||||
value.map { |val| v.custom_field.value_from_keyword(val.strip, object) }.flatten!&.compact
|
||||
else
|
||||
v.custom_field.value_from_keyword(value, object)
|
||||
end
|
||||
|
|
71
plugins/additionals/app/models/additionals_journal.rb
Normal file
71
plugins/additionals/app/models/additionals_journal.rb
Normal file
|
@ -0,0 +1,71 @@
|
|||
class AdditionalsJournal
|
||||
class << self
|
||||
def save_journal_history(journal, prop_key, ids_old, ids)
|
||||
ids_all = (ids_old + ids).uniq
|
||||
|
||||
ids_all.each do |id|
|
||||
next if ids_old.include?(id) && ids.include?(id)
|
||||
|
||||
if ids.include?(id)
|
||||
value = id
|
||||
old_value = nil
|
||||
else
|
||||
old_value = id
|
||||
value = nil
|
||||
end
|
||||
|
||||
journal.details << JournalDetail.new(property: 'attr',
|
||||
prop_key: prop_key,
|
||||
old_value: old_value,
|
||||
value: value)
|
||||
journal.save
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def validate_relation(entries, entry_id)
|
||||
old_entries = entries.select { |entry| entry.id.present? }
|
||||
new_entries = entries.select { |entry| entry.id.blank? }
|
||||
return true if new_entries.blank?
|
||||
|
||||
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) }
|
||||
return false unless (old_entries & new_entries).count.zero?
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
# Preloads visible last notes for a collection of entity
|
||||
# this is a copy of Issue.load_visible_last_notes, but usable for all entities
|
||||
# @see https://www.redmine.org/projects/redmine/repository/entry/trunk/app/models/issue.rb#L1214
|
||||
def load_visible_last_notes(entries, entity, user = User.current)
|
||||
return unless entries.any?
|
||||
|
||||
ids = entries.map(&:id)
|
||||
|
||||
journal_class = (entity == Issue ? Journal : "#{entity}Journal").constantize
|
||||
journal_ids = journal_class.joins(entity.name.underscore.to_sym => :project)
|
||||
.where(journalized_type: entity.to_s, journalized_id: ids)
|
||||
.where(journal_class.visible_notes_condition(user, skip_pre_condition: true))
|
||||
.where.not(notes: '')
|
||||
.group(:journalized_id)
|
||||
.maximum(:id)
|
||||
.values
|
||||
|
||||
journals = Journal.where(id: journal_ids).to_a
|
||||
|
||||
entries.each do |entry|
|
||||
journal = journals.detect { |j| j.journalized_id == entry.id }
|
||||
entry.instance_variable_set('@last_notes', journal.try(:notes) || '')
|
||||
end
|
||||
end
|
||||
|
||||
def set_relation_detail(entity, detail, value_key)
|
||||
value = detail.send value_key
|
||||
detail[value_key] = (entity.find_by(id: value) || value) if value.present?
|
||||
end
|
||||
end
|
||||
end
|
|
@ -62,7 +62,7 @@ class AdditionalsMacro
|
|||
permission: :view_contacts },
|
||||
{ list: %i[db db_query db_tag db_tag_count],
|
||||
permission: :view_db_entries },
|
||||
{ list: %i[child_pages calendar last_updated_at last_updated_by lastupdated_at lastupdated_by
|
||||
{ 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,
|
||||
|
|
|
@ -1,150 +1,215 @@
|
|||
module AdditionalsQuery
|
||||
def self.included(base)
|
||||
base.send :include, InstanceMethods
|
||||
def column_with_prefix?(prefix)
|
||||
columns.detect { |c| c.name.to_s.start_with?("#{prefix}.") }.present?
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
def initialize_ids_filter(options = {})
|
||||
if options[:label]
|
||||
add_available_filter 'ids', type: :integer, label: options[:label]
|
||||
def available_column_names(options = {})
|
||||
names = available_columns.dup
|
||||
names.flatten!
|
||||
names.select! { |col| col.sortable.present? } if options[:only_sortable]
|
||||
names.map(&:name)
|
||||
end
|
||||
|
||||
def sql_for_enabled_module(table_field, module_names)
|
||||
module_names = Array(module_names)
|
||||
|
||||
sql = []
|
||||
module_names.each do |module_name|
|
||||
sql << "EXISTS(SELECT 1 FROM #{EnabledModule.table_name} WHERE #{EnabledModule.table_name}.project_id=#{table_field}" \
|
||||
" AND #{EnabledModule.table_name}.name='#{module_name}')"
|
||||
end
|
||||
|
||||
sql.join(' AND ')
|
||||
end
|
||||
|
||||
def initialize_ids_filter(options = {})
|
||||
if options[:label]
|
||||
add_available_filter 'ids', type: :integer, label: options[:label]
|
||||
else
|
||||
add_available_filter 'ids', type: :integer, name: '#'
|
||||
end
|
||||
end
|
||||
|
||||
def sql_for_ids_field(_field, operator, value)
|
||||
if operator == '='
|
||||
# accepts a comma separated list of ids
|
||||
ids = value.first.to_s.scan(/\d+/).map(&:to_i)
|
||||
if ids.present?
|
||||
"#{queried_table_name}.id IN (#{ids.join ','})"
|
||||
else
|
||||
add_available_filter 'ids', type: :integer, name: '#'
|
||||
'1=0'
|
||||
end
|
||||
else
|
||||
sql_for_field 'id', operator, value, queried_table_name, 'id'
|
||||
end
|
||||
end
|
||||
|
||||
def sql_for_ids_field(_field, operator, value)
|
||||
if operator == '='
|
||||
# accepts a comma separated list of ids
|
||||
ids = value.first.to_s.scan(/\d+/).map(&:to_i)
|
||||
if ids.present?
|
||||
"#{queried_table_name}.id IN (#{ids.join(',')})"
|
||||
else
|
||||
'1=0'
|
||||
end
|
||||
else
|
||||
sql_for_field('id', operator, value, queried_table_name, 'id')
|
||||
end
|
||||
end
|
||||
def sql_for_project_status_field(field, operator, value)
|
||||
sql_for_field field, operator, value, Project.table_name, 'status'
|
||||
end
|
||||
|
||||
def initialize_project_filter(options = {})
|
||||
if project.nil?
|
||||
add_available_filter('project_id', order: options[:position],
|
||||
type: :list,
|
||||
values: -> { project_values })
|
||||
end
|
||||
return if project.nil? || project.leaf? || subproject_values.empty?
|
||||
def initialize_project_status_filter
|
||||
return if project&.leaf?
|
||||
|
||||
add_available_filter('subproject_id', order: options[:position],
|
||||
type: :list_subprojects,
|
||||
values: -> { subproject_values })
|
||||
end
|
||||
add_available_filter('project.status',
|
||||
type: :list,
|
||||
name: l(:label_attribute_of_project, name: l(:field_status)),
|
||||
values: -> { project_statuses_values })
|
||||
end
|
||||
|
||||
def initialize_created_filter(options = {})
|
||||
add_available_filter 'created_on', order: options[:position],
|
||||
type: :date_past,
|
||||
label: options[:label].presence
|
||||
end
|
||||
|
||||
def initialize_updated_filter(options = {})
|
||||
add_available_filter 'updated_on', order: options[:position],
|
||||
type: :date_past,
|
||||
label: options[:label].presence
|
||||
end
|
||||
|
||||
def initialize_tags_filter(options = {})
|
||||
values = if project
|
||||
queried_class.available_tags(project: project.id)
|
||||
else
|
||||
queried_class.available_tags
|
||||
end
|
||||
return if values.blank?
|
||||
|
||||
add_available_filter 'tags', order: options[:position],
|
||||
type: :list,
|
||||
values: values.collect { |t| [t.name, t.name] }
|
||||
end
|
||||
|
||||
def initialize_author_filter(options = {})
|
||||
return if author_values.empty?
|
||||
|
||||
add_available_filter('author_id', order: options[:position],
|
||||
type: :list_optional,
|
||||
values: options[:no_lambda].nil? ? author_values : -> { author_values })
|
||||
end
|
||||
|
||||
def initialize_assignee_filter(options = {})
|
||||
return if author_values.empty?
|
||||
|
||||
add_available_filter('assigned_to_id', order: options[:position],
|
||||
type: :list_optional,
|
||||
values: options[:no_lambda] ? author_values : -> { author_values })
|
||||
end
|
||||
|
||||
def initialize_watcher_filter(options = {})
|
||||
return if watcher_values.empty? || !User.current.logged?
|
||||
|
||||
add_available_filter('watcher_id', order: options[:position],
|
||||
def initialize_project_filter(options = {})
|
||||
if project.nil? || options[:always]
|
||||
add_available_filter('project_id', order: options[:position],
|
||||
type: :list,
|
||||
values: options[:no_lambda] ? watcher_values : -> { watcher_values })
|
||||
values: -> { project_values })
|
||||
end
|
||||
return if project.nil? || project.leaf? || subproject_values.empty?
|
||||
|
||||
def watcher_values
|
||||
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
|
||||
add_available_filter('subproject_id', order: options[:position],
|
||||
type: :list_subprojects,
|
||||
values: -> { subproject_values })
|
||||
end
|
||||
|
||||
def initialize_created_filter(options = {})
|
||||
add_available_filter 'created_on', order: options[:position],
|
||||
type: :date_past,
|
||||
label: options[:label].presence
|
||||
end
|
||||
|
||||
def initialize_updated_filter(options = {})
|
||||
add_available_filter 'updated_on', order: options[:position],
|
||||
type: :date_past,
|
||||
label: options[:label].presence
|
||||
end
|
||||
|
||||
def initialize_tags_filter(options = {})
|
||||
values = if project
|
||||
queried_class.available_tags(project: project.id)
|
||||
else
|
||||
queried_class.available_tags
|
||||
end
|
||||
return if values.blank?
|
||||
|
||||
add_available_filter 'tags', order: options[:position],
|
||||
type: :list,
|
||||
values: values.collect { |t| [t.name, t.name] }
|
||||
end
|
||||
|
||||
def initialize_approved_filter
|
||||
add_available_filter 'approved',
|
||||
type: :list,
|
||||
values: [[l(:label_hrm_approved), '1'],
|
||||
[l(:label_hrm_not_approved), '0'],
|
||||
[l(:label_hrm_to_approval), '2'],
|
||||
[l(:label_hrm_without_approval), '3']],
|
||||
label: :field_approved
|
||||
end
|
||||
|
||||
def initialize_author_filter(options = {})
|
||||
return if author_values.empty?
|
||||
|
||||
add_available_filter('author_id', order: options[:position],
|
||||
type: :list_optional,
|
||||
values: options[:no_lambda].nil? ? author_values : -> { author_values })
|
||||
end
|
||||
|
||||
def initialize_assignee_filter(options = {})
|
||||
return if author_values.empty?
|
||||
|
||||
add_available_filter('assigned_to_id', order: options[:position],
|
||||
type: :list_optional,
|
||||
values: options[:no_lambda] ? assigned_to_all_values : -> { assigned_to_all_values })
|
||||
end
|
||||
|
||||
def initialize_watcher_filter(options = {})
|
||||
return if watcher_values.empty? || !User.current.logged?
|
||||
|
||||
add_available_filter('watcher_id', order: options[:position],
|
||||
type: :list,
|
||||
values: options[:no_lambda] ? watcher_values : -> { watcher_values })
|
||||
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 = []
|
||||
assigned_to_values << ["<< #{l :label_me} >>", 'me'] if User.current.logged?
|
||||
assigned_to_values += principals.sort_by(&:status).collect { |s| [s.name, s.id.to_s, l("status_#{User::LABEL_BY_STATUS[s.status]}")] }
|
||||
|
||||
assigned_to_values
|
||||
end
|
||||
|
||||
def watcher_values
|
||||
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
|
||||
end
|
||||
|
||||
def sql_for_watcher_id_field(field, operator, value)
|
||||
watchable_type = queried_class == User ? 'Principal' : queried_class.to_s
|
||||
|
||||
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)
|
||||
AdditionalsTag.sql_for_tags_field(queried_class, operator_for(field), value)
|
||||
end
|
||||
|
||||
def sql_for_is_private_field(_field, operator, value)
|
||||
if bool_operator(operator, value)
|
||||
return '' if value.count > 1
|
||||
|
||||
"#{queried_table_name}.is_private = #{self.class.connection.quoted_true}"
|
||||
else
|
||||
return '1=0' if value.count > 1
|
||||
|
||||
"#{queried_table_name}.is_private = #{self.class.connection.quoted_false}"
|
||||
end
|
||||
end
|
||||
|
||||
def sql_for_watcher_id_field(field, operator, value)
|
||||
watchable_type = queried_class == User ? 'Principal' : queried_class.to_s
|
||||
# use for list fields with to values 1 (true) and 0 (false)
|
||||
def bool_operator(operator, values)
|
||||
operator == '=' && values.first == '1' || operator != '=' && values.first != '1'
|
||||
end
|
||||
|
||||
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
|
||||
# use for list
|
||||
def bool_values
|
||||
[[l(:general_text_yes), '1'], [l(:general_text_no), '0']]
|
||||
end
|
||||
|
||||
def sql_for_tags_field(field, _operator, value)
|
||||
AdditionalsTag.sql_for_tags_field(queried_class, operator_for(field), value)
|
||||
end
|
||||
def query_count
|
||||
objects_scope.count
|
||||
rescue ::ActiveRecord::StatementInvalid => e
|
||||
raise queried_class::StatementInvalid, e.message if defined? queried_class::StatementInvalid
|
||||
|
||||
def sql_for_is_private_field(_field, operator, value)
|
||||
if bool_operator(operator, value)
|
||||
return '1=1' if value.count > 1
|
||||
raise ::Query::StatementInvalid, e.message
|
||||
end
|
||||
|
||||
"#{queried_table_name}.is_private = #{self.class.connection.quoted_true}"
|
||||
else
|
||||
return '1=0' if value.count > 1
|
||||
def results_scope(options = {})
|
||||
order_option = [group_by_sort_order, (options[:order] || sort_clause)].flatten!.to_a.reject(&:blank?)
|
||||
|
||||
"#{queried_table_name}.is_private = #{self.class.connection.quoted_false}"
|
||||
objects_scope(options)
|
||||
.order(order_option)
|
||||
.joins(joins_for_order_statement(order_option.join(',')))
|
||||
.limit(options[:limit])
|
||||
.offset(options[:offset])
|
||||
rescue ::ActiveRecord::StatementInvalid => e
|
||||
raise queried_class::StatementInvalid, e.message if defined? queried_class::StatementInvalid
|
||||
|
||||
raise ::Query::StatementInvalid, e.message
|
||||
end
|
||||
|
||||
def grouped_name_for(group_name, replace_fields = {})
|
||||
return unless group_name
|
||||
|
||||
if grouped? && group_by_column.present?
|
||||
replace_fields.each do |field, new_name|
|
||||
return new_name.presence || group_name if group_by_column.name == field
|
||||
end
|
||||
end
|
||||
|
||||
# use for list fields with to values 1 (true) and 0 (false)
|
||||
def bool_operator(operator, values)
|
||||
operator == '=' && values.first == '1' || operator != '=' && values.first != '1'
|
||||
end
|
||||
|
||||
# use for list
|
||||
def bool_values
|
||||
[[l(:general_text_yes), '1'], [l(:general_text_no), '0']]
|
||||
end
|
||||
|
||||
def query_count
|
||||
objects_scope.count
|
||||
rescue ::ActiveRecord::StatementInvalid => e
|
||||
raise StatementInvalid, e.message
|
||||
end
|
||||
|
||||
def results_scope(options = {})
|
||||
order_option = [group_by_sort_order, (options[:order] || sort_clause)].flatten.reject(&:blank?)
|
||||
|
||||
objects_scope(options)
|
||||
.order(order_option)
|
||||
.joins(joins_for_order_statement(order_option.join(',')))
|
||||
.limit(options[:limit])
|
||||
.offset(options[:offset])
|
||||
rescue ::ActiveRecord::StatementInvalid => e
|
||||
raise StatementInvalid, e.message
|
||||
end
|
||||
group_name
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,68 +3,74 @@ class AdditionalsTag
|
|||
TAGGING_TABLE_NAME = RedmineCrm::Tagging.table_name if defined? RedmineCrm
|
||||
PROJECT_TABLE_NAME = Project.table_name
|
||||
|
||||
def self.get_available_tags(klass, options = {})
|
||||
scope = RedmineCrm::Tag.where({})
|
||||
scope = scope.where("#{PROJECT_TABLE_NAME}.id = ?", options[:project]) if options[:project]
|
||||
if options[:permission]
|
||||
scope = scope.where(tag_access(options[:permission]))
|
||||
elsif options[:visible_condition]
|
||||
scope = scope.where(klass.visible_condition(User.current))
|
||||
end
|
||||
scope = scope.where("LOWER(#{TAG_TABLE_NAME}.name) LIKE ?", "%#{options[:name_like].downcase}%") if options[:name_like]
|
||||
scope = scope.where("#{TAG_TABLE_NAME}.name=?", options[:name]) if options[:name]
|
||||
scope = scope.where("#{TAGGING_TABLE_NAME}.taggable_id!=?", options[:exclude_id]) if options[:exclude_id]
|
||||
scope = scope.where(options[:where_field] => options[:where_value]) if options[:where_field].present? && options[:where_value]
|
||||
|
||||
scope = scope.select("#{TAG_TABLE_NAME}.*, COUNT(DISTINCT #{TAGGING_TABLE_NAME}.taggable_id) AS count")
|
||||
scope = scope.joins(tag_joins(klass, options))
|
||||
scope = scope.group("#{TAG_TABLE_NAME}.id, #{TAG_TABLE_NAME}.name").having('COUNT(*) > 0')
|
||||
scope = scope.order("#{TAG_TABLE_NAME}.name")
|
||||
scope
|
||||
end
|
||||
|
||||
def self.tag_joins(klass, options = {})
|
||||
table_name = klass.table_name
|
||||
|
||||
joins = ["JOIN #{TAGGING_TABLE_NAME} ON #{TAGGING_TABLE_NAME}.tag_id = #{TAG_TABLE_NAME}.id"]
|
||||
joins << "JOIN #{table_name} " \
|
||||
"ON #{table_name}.id = #{TAGGING_TABLE_NAME}.taggable_id AND #{TAGGING_TABLE_NAME}.taggable_type = '#{klass}'"
|
||||
|
||||
if options[:project_join]
|
||||
joins << options[:project_join]
|
||||
elsif options[:project] || !options[:without_projects]
|
||||
joins << "JOIN #{PROJECT_TABLE_NAME} ON #{table_name}.project_id = #{PROJECT_TABLE_NAME}.id"
|
||||
class << self
|
||||
def all_type_tags(klass, options = {})
|
||||
RedmineCrm::Tag.where({})
|
||||
.joins(tag_joins(klass, options))
|
||||
.distinct
|
||||
.order("#{TAG_TABLE_NAME}.name")
|
||||
end
|
||||
|
||||
joins
|
||||
end
|
||||
def get_available_tags(klass, options = {})
|
||||
scope = RedmineCrm::Tag.where({})
|
||||
scope = scope.where("#{PROJECT_TABLE_NAME}.id = ?", options[:project]) if options[:project]
|
||||
if options[:permission]
|
||||
scope = scope.where(tag_access(options[:permission]))
|
||||
elsif options[:visible_condition]
|
||||
scope = scope.where(klass.visible_condition(User.current))
|
||||
end
|
||||
scope = scope.where("LOWER(#{TAG_TABLE_NAME}.name) LIKE ?", "%#{options[:name_like].downcase}%") if options[:name_like]
|
||||
scope = scope.where("#{TAG_TABLE_NAME}.name=?", options[:name]) if options[:name]
|
||||
scope = scope.where("#{TAGGING_TABLE_NAME}.taggable_id!=?", options[:exclude_id]) if options[:exclude_id]
|
||||
scope = scope.where(options[:where_field] => options[:where_value]) if options[:where_field].present? && options[:where_value]
|
||||
|
||||
def self.tag_access(permission)
|
||||
projects_allowed = if permission.nil?
|
||||
Project.visible.pluck(:id)
|
||||
else
|
||||
Project.where(Project.allowed_to_condition(User.current, permission)).pluck(:id)
|
||||
end
|
||||
|
||||
if projects_allowed.present?
|
||||
"#{PROJECT_TABLE_NAME}.id IN (#{projects_allowed.join(',')})" unless projects_allowed.empty?
|
||||
else
|
||||
'1=0'
|
||||
scope.select("#{TAG_TABLE_NAME}.*, COUNT(DISTINCT #{TAGGING_TABLE_NAME}.taggable_id) AS count")
|
||||
.joins(tag_joins(klass, options))
|
||||
.group("#{TAG_TABLE_NAME}.id, #{TAG_TABLE_NAME}.name").having('COUNT(*) > 0')
|
||||
.order("#{TAG_TABLE_NAME}.name")
|
||||
end
|
||||
end
|
||||
|
||||
def self.remove_unused_tags
|
||||
unused = RedmineCrm::Tag.find_by_sql(<<-SQL)
|
||||
SELECT * FROM tags WHERE id NOT IN (
|
||||
SELECT DISTINCT tag_id FROM taggings
|
||||
)
|
||||
SQL
|
||||
unused.each(&:destroy)
|
||||
end
|
||||
def remove_unused_tags
|
||||
RedmineCrm::Tag.where.not(id: RedmineCrm::Tagging.select(:tag_id).distinct)
|
||||
.each(&:destroy)
|
||||
end
|
||||
|
||||
def self.sql_for_tags_field(klass, operator, value)
|
||||
compare = operator.eql?('=') ? 'IN' : 'NOT IN'
|
||||
ids_list = klass.tagged_with(value).collect(&:id).push(0).join(',')
|
||||
"( #{klass.table_name}.id #{compare} (#{ids_list}) ) "
|
||||
def sql_for_tags_field(klass, operator, value)
|
||||
compare = operator.eql?('=') ? 'IN' : 'NOT IN'
|
||||
ids_list = klass.tagged_with(value).collect(&:id).push(0).join(',')
|
||||
"( #{klass.table_name}.id #{compare} (#{ids_list}) ) "
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def tag_access(permission)
|
||||
projects_allowed = if permission.nil?
|
||||
Project.visible.ids
|
||||
else
|
||||
Project.where(Project.allowed_to_condition(User.current, permission)).ids
|
||||
end
|
||||
|
||||
if projects_allowed.present?
|
||||
"#{PROJECT_TABLE_NAME}.id IN (#{projects_allowed.join ','})" unless projects_allowed.empty?
|
||||
else
|
||||
'1=0'
|
||||
end
|
||||
end
|
||||
|
||||
def tag_joins(klass, options = {})
|
||||
table_name = klass.table_name
|
||||
|
||||
joins = ["JOIN #{TAGGING_TABLE_NAME} ON #{TAGGING_TABLE_NAME}.tag_id = #{TAG_TABLE_NAME}.id"]
|
||||
joins << "JOIN #{table_name} " \
|
||||
"ON #{table_name}.id = #{TAGGING_TABLE_NAME}.taggable_id AND #{TAGGING_TABLE_NAME}.taggable_type = '#{klass}'"
|
||||
|
||||
if options[:project_join]
|
||||
joins << options[:project_join]
|
||||
elsif options[:project] || !options[:without_projects]
|
||||
joins << "JOIN #{PROJECT_TABLE_NAME} ON #{table_name}.project_id = #{PROJECT_TABLE_NAME}.id"
|
||||
end
|
||||
|
||||
joins
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
423
plugins/additionals/app/models/dashboard.rb
Normal file
423
plugins/additionals/app/models/dashboard.rb
Normal file
|
@ -0,0 +1,423 @@
|
|||
class Dashboard < ActiveRecord::Base
|
||||
include Redmine::I18n
|
||||
include Redmine::SafeAttributes
|
||||
include Additionals::EntityMethods
|
||||
|
||||
class SystemDefaultChangeException < StandardError; end
|
||||
class ProjectSystemDefaultChangeException < StandardError; end
|
||||
|
||||
belongs_to :project
|
||||
belongs_to :author, class_name: 'User'
|
||||
|
||||
# current active project (belongs_to :project can be nil, because this is system default)
|
||||
attr_accessor :content_project
|
||||
|
||||
serialize :options
|
||||
|
||||
has_many :dashboard_roles, dependent: :destroy
|
||||
has_many :roles, through: :dashboard_roles
|
||||
|
||||
VISIBILITY_PRIVATE = 0
|
||||
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) })
|
||||
|
||||
safe_attributes 'name', 'description', 'enable_sidebar',
|
||||
'always_expose', 'project_id', 'author_id',
|
||||
if: (lambda do |dashboard, user|
|
||||
dashboard.new_record? ||
|
||||
user.allowed_to?(:save_dashboards, dashboard.project, global: true)
|
||||
end)
|
||||
|
||||
safe_attributes 'dashboard_type',
|
||||
if: (lambda do |dashboard, _user|
|
||||
dashboard.new_record?
|
||||
end)
|
||||
|
||||
safe_attributes 'visibility', 'role_ids',
|
||||
if: (lambda do |dashboard, user|
|
||||
user.allowed_to?(:share_dashboards, dashboard.project, global: true) ||
|
||||
user.allowed_to?(:set_system_dashboards, dashboard.project, global: true)
|
||||
end)
|
||||
|
||||
safe_attributes 'system_default',
|
||||
if: (lambda do |dashboard, user|
|
||||
user.allowed_to?(:set_system_dashboards, dashboard.project, global: true)
|
||||
end)
|
||||
|
||||
before_save :dashboard_type_check, :visibility_check, :set_options_hash, :clear_unused_block_settings
|
||||
|
||||
before_destroy :check_destroy_system_default
|
||||
after_save :update_system_defaults
|
||||
after_save :remove_unused_role_relations
|
||||
|
||||
validates :name, :dashboard_type, :author, :visibility, presence: true
|
||||
validates :visibility, inclusion: { in: [VISIBILITY_PUBLIC, VISIBILITY_ROLES, VISIBILITY_PRIVATE] }
|
||||
validate :validate_roles
|
||||
validate :validate_visibility
|
||||
validate :validate_name
|
||||
validate :validate_system_default
|
||||
validate :validate_project_system_default
|
||||
|
||||
class << self
|
||||
def system_default(dashboard_type)
|
||||
select(:id).find_by(dashboard_type: dashboard_type, system_default: true)
|
||||
.try(:id)
|
||||
end
|
||||
|
||||
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 = 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?
|
||||
|
||||
if dashboard.blank?
|
||||
scope = scope.where(system_default: true).or(scope.where(author_id: user.id))
|
||||
dashboard = scope.order(system_default: :desc, project_id: :desc, id: :asc).first
|
||||
|
||||
if recently_id.present?
|
||||
Rails.logger.debug 'default cleanup required'
|
||||
# Remove invalid recently_id
|
||||
if project.present?
|
||||
User.current.pref.recently_used_dashboards[dashboard_type].delete(project.id)
|
||||
else
|
||||
User.current.pref.recently_used_dashboards[dashboard_type] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
dashboard
|
||||
end
|
||||
|
||||
def fields_for_order_statement(table = nil)
|
||||
table ||= table_name
|
||||
["#{table}.name"]
|
||||
end
|
||||
|
||||
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)))
|
||||
|
||||
if user.admin?
|
||||
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 = ?",
|
||||
VISIBILITY_PUBLIC,
|
||||
VISIBILITY_ROLES,
|
||||
user.id,
|
||||
Project::STATUS_ARCHIVED,
|
||||
user.id)
|
||||
elsif user.logged?
|
||||
scope.where(visibility: VISIBILITY_PUBLIC).or(scope.where(author_id: user.id))
|
||||
else
|
||||
scope.where visibility: VISIBILITY_PUBLIC
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(attributes = nil, *args)
|
||||
super
|
||||
set_options_hash
|
||||
end
|
||||
|
||||
def set_options_hash
|
||||
self.options ||= {}
|
||||
end
|
||||
|
||||
def [](attr_name)
|
||||
if has_attribute? attr_name
|
||||
super
|
||||
else
|
||||
options ? options[attr_name] : nil
|
||||
end
|
||||
end
|
||||
|
||||
def []=(attr_name, value)
|
||||
if has_attribute? attr_name
|
||||
super
|
||||
else
|
||||
h = (self[:options] || {}).dup
|
||||
h.update(attr_name => value)
|
||||
self[:options] = h
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
# Returns true if the dashboard is visible to +user+ or the current user.
|
||||
def visible?(user = User.current)
|
||||
return true if user.admin?
|
||||
return false unless project.nil? || user.allowed_to?(:view_project, project)
|
||||
return true if user == author
|
||||
|
||||
case visibility
|
||||
when VISIBILITY_PUBLIC
|
||||
true
|
||||
when VISIBILITY_ROLES
|
||||
if project
|
||||
(user.roles_for_project(project) & roles).any?
|
||||
else
|
||||
user.memberships.joins(:member_roles).where(member_roles: { role_id: roles.map(&:id) }).any?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def content
|
||||
@content ||= "DashboardContent#{dashboard_type[0..-10]}".constantize.new(project: content_project.presence || project)
|
||||
end
|
||||
|
||||
def available_groups
|
||||
content.groups
|
||||
end
|
||||
|
||||
def layout
|
||||
self[:layout] ||= content.default_layout.deep_dup
|
||||
end
|
||||
|
||||
def layout=(arg)
|
||||
self[:layout] = arg
|
||||
end
|
||||
|
||||
def layout_settings(block = nil)
|
||||
s = self[:layout_settings] ||= {}
|
||||
if block
|
||||
s[block] ||= {}
|
||||
else
|
||||
s
|
||||
end
|
||||
end
|
||||
|
||||
def layout_settings=(arg)
|
||||
self[:layout_settings] = arg
|
||||
end
|
||||
|
||||
def remove_block(block)
|
||||
block = block.to_s.underscore
|
||||
layout.each_key do |group|
|
||||
layout[group].delete block
|
||||
end
|
||||
layout
|
||||
end
|
||||
|
||||
# Adds block to the user page layout
|
||||
# Returns nil if block is not valid or if it's already
|
||||
# present in the user page layout
|
||||
def add_block(block)
|
||||
block = block.to_s.underscore
|
||||
return unless content.valid_block? block, layout.values.flatten
|
||||
|
||||
remove_block block
|
||||
# add it to the first group
|
||||
# add it to the first group
|
||||
group = available_groups.first
|
||||
layout[group] ||= []
|
||||
layout[group].unshift block
|
||||
end
|
||||
|
||||
# Sets the block order for the given group.
|
||||
# Example:
|
||||
# preferences.order_blocks('left', ['issueswatched', 'news'])
|
||||
def order_blocks(group, blocks)
|
||||
group = group.to_s
|
||||
return if content.groups.exclude?(group) || blocks.blank?
|
||||
|
||||
blocks = blocks.map(&:underscore) & layout.values.flatten
|
||||
blocks.each { |block| remove_block(block) }
|
||||
layout[group] = blocks
|
||||
end
|
||||
|
||||
def update_block_settings(block, settings)
|
||||
block = block.to_s
|
||||
block_settings = layout_settings(block).merge(settings.symbolize_keys)
|
||||
layout_settings[block] = block_settings
|
||||
end
|
||||
|
||||
def private?(user = User.current)
|
||||
author_id == user.id && visibility == VISIBILITY_PRIVATE
|
||||
end
|
||||
|
||||
def public?
|
||||
visibility != VISIBILITY_PRIVATE
|
||||
end
|
||||
|
||||
def editable_by?(usr = User.current, prj = nil)
|
||||
prj ||= project
|
||||
usr && (usr.admin? ||
|
||||
(author == usr && usr.allowed_to?(:save_dashboards, prj, global: true)))
|
||||
end
|
||||
|
||||
def editable?(usr = User.current)
|
||||
@editable ||= editable_by?(usr)
|
||||
end
|
||||
|
||||
def destroyable_by?(usr = User.current)
|
||||
return unless editable_by? usr, project
|
||||
|
||||
return !system_default_was if dashboard_type != DashboardContentProject::TYPE_NAME
|
||||
|
||||
# project dashboards needs special care
|
||||
project.present? || !system_default_was
|
||||
end
|
||||
|
||||
def destroyable?
|
||||
@destroyable ||= destroyable_by?(User.current)
|
||||
end
|
||||
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
|
||||
# Returns a string of css classes that apply to the entry
|
||||
def css_classes(user = User.current)
|
||||
s = ['dashboard']
|
||||
s << 'created-by-me' if author_id == user.id
|
||||
s.join(' ')
|
||||
end
|
||||
|
||||
def allowed_target_projects(user = User.current)
|
||||
Project.where Project.allowed_to_condition(user, :save_dashboards)
|
||||
end
|
||||
|
||||
# this is used to get unique cache for blocks
|
||||
def async_params(block, options, settings = {})
|
||||
if block.blank?
|
||||
msg = 'block is missing for dashboard_async'
|
||||
Rails.log.error msg
|
||||
raise msg
|
||||
end
|
||||
|
||||
config = { dashboard_id: id,
|
||||
block: block }
|
||||
|
||||
settings[:user_id] = User.current.id if !options.key?(:skip_user_id) || !options[:skip_user_id]
|
||||
|
||||
if settings.present?
|
||||
settings.each do |key, setting|
|
||||
settings[key] = setting.reject(&:blank?).join(',') if setting.is_a? Array
|
||||
|
||||
next if options[:exposed_params].blank?
|
||||
|
||||
options[:exposed_params].each do |exposed_param|
|
||||
if key == exposed_param
|
||||
config[key] = settings[key]
|
||||
settings.delete key
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
unique_params = settings.flatten
|
||||
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('_'))
|
||||
end
|
||||
|
||||
Rails.logger.debug "debug async_params for #{block}: config=#{config.inspect}"
|
||||
|
||||
config
|
||||
end
|
||||
|
||||
def project_id_can_change?
|
||||
return true if new_record? ||
|
||||
dashboard_type != DashboardContentProject::TYPE_NAME ||
|
||||
!system_default_was ||
|
||||
project_id_was.present?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def clear_unused_block_settings
|
||||
blocks = layout.values.flatten
|
||||
layout_settings.keep_if { |block, _settings| blocks.include?(block) }
|
||||
end
|
||||
|
||||
def remove_unused_role_relations
|
||||
return if !saved_change_to_visibility? || visibility == VISIBILITY_ROLES
|
||||
|
||||
roles.clear
|
||||
end
|
||||
|
||||
def validate_roles
|
||||
return if visibility != VISIBILITY_ROLES || roles.present?
|
||||
|
||||
errors.add(:base,
|
||||
[l(:label_role_plural), l('activerecord.errors.messages.blank')].join(' '))
|
||||
end
|
||||
|
||||
def validate_system_default
|
||||
return if new_record? || system_default_was == system_default || system_default?
|
||||
|
||||
raise SystemDefaultChangeException
|
||||
end
|
||||
|
||||
def validate_project_system_default
|
||||
return if project_id_can_change?
|
||||
|
||||
raise ProjectSystemDefaultChangeException if project_id.present?
|
||||
end
|
||||
|
||||
def check_destroy_system_default
|
||||
raise 'It is not allowed to delete dashboard, which is system default' unless destroyable?
|
||||
end
|
||||
|
||||
def dashboard_type_check
|
||||
self.project_id = nil if dashboard_type == DashboardContentWelcome::TYPE_NAME
|
||||
end
|
||||
|
||||
def update_system_defaults
|
||||
return unless system_default? && User.current.allowed_to?(:set_system_dashboards, project, global: true)
|
||||
|
||||
scope = self.class
|
||||
.where(dashboard_type: dashboard_type)
|
||||
.where.not(id: id)
|
||||
|
||||
scope = scope.where(project: project) if dashboard_type == DashboardContentProject::TYPE_NAME
|
||||
|
||||
scope.update_all system_default: false
|
||||
end
|
||||
|
||||
# check if permissions changed and dashboard settings have to be corrected
|
||||
def visibility_check
|
||||
user = User.current
|
||||
|
||||
return if system_default? ||
|
||||
user.allowed_to?(:share_dashboards, project, global: true) ||
|
||||
user.allowed_to?(:set_system_dashboards, project, global: true)
|
||||
|
||||
# change to private
|
||||
self.visibility = VISIBILITY_PRIVATE
|
||||
end
|
||||
|
||||
def validate_visibility
|
||||
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)
|
||||
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?
|
||||
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?
|
||||
end
|
||||
end
|
111
plugins/additionals/app/models/dashboard_content.rb
Normal file
111
plugins/additionals/app/models/dashboard_content.rb
Normal file
|
@ -0,0 +1,111 @@
|
|||
class DashboardContent
|
||||
include Redmine::I18n
|
||||
|
||||
attr_accessor :user, :project
|
||||
|
||||
MAX_MULTIPLE_OCCURS = 8
|
||||
DEFAULT_MAX_ENTRIES = 10
|
||||
RENDER_ASYNC_CACHE_EXPIRES_IN = 30
|
||||
|
||||
class << self
|
||||
def types
|
||||
descendants.map { |dc| dc::TYPE_NAME }
|
||||
end
|
||||
end
|
||||
|
||||
def with_chartjs?
|
||||
false
|
||||
end
|
||||
|
||||
def initialize(attr = {})
|
||||
self.user = attr[:user].presence || User.current
|
||||
self.project = attr[:project].presence
|
||||
end
|
||||
|
||||
def groups
|
||||
%w[top left right bottom]
|
||||
end
|
||||
|
||||
def block_definitions
|
||||
{
|
||||
'issuequery' => { label: l(:label_query_with_name, l(:label_issue_plural)),
|
||||
permission: :view_issues,
|
||||
query_block: {
|
||||
label: l(:label_issue_plural),
|
||||
list_partial: 'issues/list',
|
||||
class: IssueQuery,
|
||||
link_helper: '_project_issues_path',
|
||||
count_method: 'issue_count',
|
||||
entries_method: 'issues',
|
||||
entities_var: :issues,
|
||||
with_project: true
|
||||
},
|
||||
max_occurs: DashboardContent::MAX_MULTIPLE_OCCURS },
|
||||
'text' => { label: l(:label_text),
|
||||
max_occurs: MAX_MULTIPLE_OCCURS,
|
||||
partial: 'dashboards/blocks/text' },
|
||||
'news' => { label: l(:label_news_latest),
|
||||
permission: :view_news },
|
||||
'documents' => { label: l(:label_document_plural),
|
||||
permission: :view_documents },
|
||||
'my_spent_time' => { label: l(:label_my_spent_time),
|
||||
permission: :log_time },
|
||||
'feed' => { label: l(:label_additionals_feed),
|
||||
max_occurs: DashboardContent::MAX_MULTIPLE_OCCURS,
|
||||
async: { required_settings: %i[url],
|
||||
cache_expires_in: 600,
|
||||
skip_user_id: true,
|
||||
partial: 'dashboards/blocks/feed' } }
|
||||
}
|
||||
end
|
||||
|
||||
# Returns the available blocks
|
||||
def available_blocks
|
||||
return @available_blocks if defined? @available_blocks
|
||||
|
||||
available_blocks = begin block_definitions.reject do |_block_name, block_specs|
|
||||
block_specs.key?(:permission) && !user.allowed_to?(block_specs[:permission], project, global: true) ||
|
||||
block_specs.key?(:admin_only) && block_specs[:admin_only] && !user.admin? ||
|
||||
block_specs.key?(:if) && !block_specs[:if].call(project)
|
||||
end
|
||||
end
|
||||
|
||||
@available_blocks = available_blocks.sort_by { |_k, v| v[:label] }.to_h
|
||||
end
|
||||
|
||||
def block_options(blocks_in_use = [])
|
||||
options = []
|
||||
available_blocks.each do |block, block_options|
|
||||
indexes = blocks_in_use.map do |n|
|
||||
Regexp.last_match(2).to_i if n =~ /\A#{block}(__(\d+))?\z/
|
||||
end
|
||||
indexes.compact!
|
||||
|
||||
occurs = indexes.size
|
||||
block_id = indexes.any? ? "#{block}__#{indexes.max + 1}" : block
|
||||
disabled = (occurs >= (available_blocks[block][:max_occurs] || 1))
|
||||
block_id = nil if disabled
|
||||
|
||||
options << [block_options[:label], block_id]
|
||||
end
|
||||
options
|
||||
end
|
||||
|
||||
def valid_block?(block, blocks_in_use = [])
|
||||
block.present? && block_options(blocks_in_use).map(&:last).include?(block)
|
||||
end
|
||||
|
||||
def find_block(block)
|
||||
block.to_s =~ /\A(.*?)(__\d+)?\z/
|
||||
name = Regexp.last_match(1)
|
||||
available_blocks.key?(name) ? available_blocks[name].merge(name: name) : nil
|
||||
end
|
||||
|
||||
# Returns the default layout for a new dashboard
|
||||
def default_layout
|
||||
{
|
||||
'left' => ['legacy_left'],
|
||||
'right' => ['legacy_right']
|
||||
}
|
||||
end
|
||||
end
|
52
plugins/additionals/app/models/dashboard_content_project.rb
Normal file
52
plugins/additionals/app/models/dashboard_content_project.rb
Normal file
|
@ -0,0 +1,52 @@
|
|||
class DashboardContentProject < DashboardContent
|
||||
TYPE_NAME = 'ProjectDashboard'.freeze
|
||||
|
||||
def block_definitions
|
||||
blocks = super
|
||||
|
||||
# legacy_left or legacy_right should not be moved to DashboardContent,
|
||||
# because DashboardContent is used for areas in other plugins
|
||||
blocks['legacy_left'] = { label: l(:label_dashboard_legacy_left),
|
||||
no_settings: true }
|
||||
|
||||
blocks['legacy_right'] = { label: l(:label_dashboard_legacy_right),
|
||||
no_settings: true }
|
||||
|
||||
blocks['projectinformation'] = { label: l(:label_project_information),
|
||||
no_settings: true,
|
||||
if: (lambda do |project|
|
||||
project.description.present? ||
|
||||
project.homepage.present? ||
|
||||
project.visible_custom_field_values.any? { |o| o.value.present? }
|
||||
end),
|
||||
partial: 'dashboards/blocks/project_information' }
|
||||
|
||||
blocks['projectissues'] = { label: l(:label_issues_summary),
|
||||
no_settings: true,
|
||||
permission: :view_issues,
|
||||
partial: 'dashboards/blocks/project_issues' }
|
||||
|
||||
blocks['projecttimeentries'] = { label: l(:label_time_tracking),
|
||||
no_settings: true,
|
||||
permission: :view_time_entries,
|
||||
partial: 'dashboards/blocks/project_time_entries' }
|
||||
|
||||
blocks['projectmembers'] = { label: l(:label_member_plural),
|
||||
no_settings: true,
|
||||
partial: 'projects/members_box' }
|
||||
|
||||
blocks['projectsubprojects'] = { label: l(:label_subproject_plural),
|
||||
no_settings: true,
|
||||
partial: 'dashboards/blocks/project_subprojects' }
|
||||
|
||||
blocks
|
||||
end
|
||||
|
||||
# Returns the default layout for a new dashboard
|
||||
def default_layout
|
||||
{
|
||||
'left' => %w[projectinformation projectissues projecttimeentries],
|
||||
'right' => %w[projectmembers projectsubprojects]
|
||||
}
|
||||
end
|
||||
end
|
33
plugins/additionals/app/models/dashboard_content_welcome.rb
Normal file
33
plugins/additionals/app/models/dashboard_content_welcome.rb
Normal file
|
@ -0,0 +1,33 @@
|
|||
class DashboardContentWelcome < DashboardContent
|
||||
TYPE_NAME = 'WelcomeDashboard'.freeze
|
||||
|
||||
def block_definitions
|
||||
blocks = super
|
||||
|
||||
# legacy_left or legacy_right should not be moved to DashboardContent,
|
||||
# because DashboardContent is used for areas in other plugins
|
||||
blocks['legacy_left'] = { label: l(:label_dashboard_legacy_left),
|
||||
no_settings: true }
|
||||
|
||||
blocks['legacy_right'] = { label: l(:label_dashboard_legacy_right),
|
||||
no_settings: true }
|
||||
|
||||
blocks['welcome'] = { label: l(:setting_welcome_text),
|
||||
no_settings: true,
|
||||
partial: 'dashboards/blocks/welcome' }
|
||||
|
||||
blocks['activity'] = { label: l(:label_activity),
|
||||
async: { data_method: 'activity_dashboard_data',
|
||||
partial: 'dashboards/blocks/activity' } }
|
||||
|
||||
blocks
|
||||
end
|
||||
|
||||
# Returns the default layout for a new dashboard
|
||||
def default_layout
|
||||
{
|
||||
'left' => %w[welcome legacy_left],
|
||||
'right' => ['legacy_right']
|
||||
}
|
||||
end
|
||||
end
|
6
plugins/additionals/app/models/dashboard_role.rb
Normal file
6
plugins/additionals/app/models/dashboard_role.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
class DashboardRole < ActiveRecord::Base
|
||||
include Redmine::SafeAttributes
|
||||
|
||||
belongs_to :dashboard
|
||||
belongs_to :role
|
||||
end
|
|
@ -1,5 +1,5 @@
|
|||
Deface::Override.new virtual_path: 'account/register',
|
||||
name: 'add-invisble-captcha',
|
||||
insert_top: 'div.box',
|
||||
original: 'e64d82c46cc3322e4d953aa119d1e71e81854158',
|
||||
original: 'a9c303821376a8d83cba32654629d71cc3926a1d',
|
||||
partial: 'account/invisible_captcha'
|
||||
|
|
8
plugins/additionals/app/overrides/contacts/form.rb
Normal file
8
plugins/additionals/app/overrides/contacts/form.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
unless Redmine::Plugin.installed? 'redmine_servicedesk'
|
||||
# redmine_contacts does not provide hook
|
||||
Deface::Override.new virtual_path: 'contacts/_form',
|
||||
name: 'contacts-form-hook',
|
||||
insert_bottom: 'div#contact_data',
|
||||
original: 'df6cae24cfd26e5299c45c427fbbd4e5f23c313e',
|
||||
partial: 'hooks/view_contacts_form'
|
||||
end
|
|
@ -0,0 +1,6 @@
|
|||
Deface::Override.new virtual_path: 'custom_fields/formats/_text',
|
||||
name: 'custom_fields-formats-text',
|
||||
replace: 'erb[silent]:contains(\'if @custom_field.class.name == "IssueCustomField"\')',
|
||||
original: '5e0fbf8e8156bf1514cbada3dbaca9afc3c19bbb',
|
||||
closing_selector: "erb[silent]:contains('end')",
|
||||
partial: 'custom_fields/formats/additionals_text.html.slim'
|
5
plugins/additionals/app/overrides/issues/edit.rb
Normal file
5
plugins/additionals/app/overrides/issues/edit.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
Deface::Override.new virtual_path: 'issues/_edit',
|
||||
name: 'edit-issue-permission',
|
||||
replace: 'erb[silent]:contains("User.current.allowed_to?(:log_time, @project)")',
|
||||
original: '98560fb12bb71f775f2a7fd1884c97f8cd632cd3',
|
||||
text: '<% if User.current.allowed_to?(:log_time, @project) && @issue.log_time_allowed? %>'
|
5
plugins/additionals/app/overrides/issues/list.rb
Normal file
5
plugins/additionals/app/overrides/issues/list.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
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 %>'
|
|
@ -1,5 +1,10 @@
|
|||
Deface::Override.new virtual_path: 'issues/_action_menu',
|
||||
name: 'show-issue-log-time',
|
||||
replace: 'erb[loud]:contains("User.current.allowed_to?(:log_time, @project)")',
|
||||
original: '4bbf065b9f960687e07f76e7232eb21bf183a981',
|
||||
partial: 'issues/additionals_action_menu_log_time'
|
||||
Deface::Override.new virtual_path: 'issues/_action_menu',
|
||||
name: 'add-issue-assign-to-me',
|
||||
insert_bottom: 'div.contextual',
|
||||
original: 'c0a30490bb9ac5c5644e674319f17e40c57034d8',
|
||||
original: '44ef032156db0dfdb67301fdb9ef8901abeca18a',
|
||||
partial: 'issues/additionals_action_menu'
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
Deface::Override.new virtual_path: 'layouts/base',
|
||||
name: 'add-body-header',
|
||||
insert_before: 'div#wrapper',
|
||||
original: '4af81ed701989727953cea2e376c9d83665d7eb2',
|
||||
partial: 'additionals/global_body_header'
|
5
plugins/additionals/app/overrides/reports/simple.rb
Normal file
5
plugins/additionals/app/overrides/reports/simple.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
Deface::Override.new virtual_path: 'reports/_simple',
|
||||
name: 'report-simple-user-scope',
|
||||
insert_before: 'erb[silent]:contains("rows.empty?")',
|
||||
original: '0c85cc752700d7f2bf08b3b9b30f59d8eddc443b',
|
||||
partial: 'reports/additionals_simple'
|
|
@ -1,5 +1,5 @@
|
|||
Deface::Override.new virtual_path: 'roles/_form',
|
||||
name: 'roles-form-hide',
|
||||
insert_before: 'p.manage_members_shown',
|
||||
original: 'b2a317f49e0b65ae506c8871f0c2bcc3e8098766',
|
||||
original: '7413482e01a07b5615be1900b974fee87224cb47',
|
||||
partial: 'roles/additionals_form'
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
Deface::Override.new virtual_path: 'users/show',
|
||||
name: 'user-show-info-hook',
|
||||
insert_top: 'div.splitcontentleft ul:first-child',
|
||||
original: 'aff8d775275e3f33cc45d72b8e2896144be4beff',
|
||||
partial: 'hooks/view_users_show'
|
||||
Deface::Override.new virtual_path: 'users/show',
|
||||
name: 'user-contextual-hook',
|
||||
insert_bottom: 'div.contextual',
|
||||
original: '9d6a7ad6ba0addc68c6b4f6c3b868511bc8eb542',
|
||||
partial: 'hooks/view_users_contextual'
|
||||
unless Redmine::Plugin.installed? 'redmine_hrm'
|
||||
Deface::Override.new virtual_path: 'users/show',
|
||||
name: 'user-show-info-hook',
|
||||
insert_top: 'div.splitcontentleft ul:first-child',
|
||||
original: '743d616ab7942bb6bc65bd00626b6a5143247a37',
|
||||
partial: 'hooks/view_users_show_info'
|
||||
Deface::Override.new virtual_path: 'users/show',
|
||||
name: 'user-contextual-hook',
|
||||
insert_bottom: 'div.contextual',
|
||||
original: '9d6a7ad6ba0addc68c6b4f6c3b868511bc8eb542',
|
||||
partial: 'hooks/view_users_show_contextual'
|
||||
end
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
Deface::Override.new virtual_path: 'welcome/index',
|
||||
name: 'add-welcome-bottom-content',
|
||||
insert_after: 'div.splitcontentright',
|
||||
original: 'dd470844bcaa4d7c9dc66e70e6c0c843d42969bf',
|
||||
partial: 'welcome/overview_bottom'
|
||||
Deface::Override.new virtual_path: 'welcome/index',
|
||||
name: 'add-welcome-top-content',
|
||||
insert_before: 'div.splitcontentleft',
|
||||
original: 'e7de0a2e88c5ccb4d1feb7abac239e4b669babed',
|
||||
partial: 'welcome/overview_top'
|
||||
Deface::Override.new virtual_path: 'welcome/index',
|
||||
name: 'remove-welcome-news',
|
||||
replace: 'div.news',
|
||||
original: '163f5df8f0cb2d5009d7f57ad38174ed29201a1a',
|
||||
partial: 'welcome/overview_news'
|
|
@ -1,5 +0,0 @@
|
|||
Deface::Override.new virtual_path: 'wiki/show',
|
||||
name: 'addto-wiki-show',
|
||||
insert_before: 'div.contextual',
|
||||
original: '6b0cb1646d5e2cb23feee1805949e266036581e6',
|
||||
partial: 'wiki/show_additionals'
|
|
@ -1,5 +0,0 @@
|
|||
Deface::Override.new virtual_path: 'wiki/_sidebar',
|
||||
name: 'addto-wiki-sidebar',
|
||||
insert_after: 'ul',
|
||||
original: '07a5375c015a7d96826c9977c4d8889c4a98bb49',
|
||||
partial: 'wiki/global_sidebar'
|
|
@ -1,2 +1,2 @@
|
|||
- if Additionals.setting?(:invisible_captcha)
|
||||
= invisible_captcha
|
||||
= f.invisible_captcha :url, autocomplete: 'off'
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
- login_text = Additionals.settings[:account_login_bottom]
|
||||
- login_text = Additionals.setting :account_login_bottom
|
||||
- if login_text.present?
|
||||
br
|
||||
.login-additionals
|
||||
= textilizable(login_text)
|
||||
= textilizable login_text
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
- footer = Additionals.settings[:global_footer]
|
||||
- footer = Additionals.setting(:global_footer)
|
||||
- if footer.present?
|
||||
.additionals-footer
|
||||
= textilizable(footer)
|
||||
|
||||
- if @additionals_help_items.present?
|
||||
javascript:
|
||||
$(function() {
|
||||
$('a.help').parent().append("<ul class=\"menu-children\">#{escape_javascript(@additionals_help_items)}</ul>");
|
||||
$('a.help').parent().append("<ul class=\"menu-children\">#{escape_javascript @additionals_help_items}</ul>");
|
||||
});
|
||||
|
||||
- if Additionals.setting?(:open_external_urls)
|
||||
javascript:
|
||||
$(function() {
|
||||
$('a.external').attr({ 'target': '_blank',
|
||||
'rel': 'noopener noreferrer'});
|
||||
});
|
||||
|
|
0
plugins/additionals/app/views/additionals/_global_body_header.slim → plugins/additionals/app/views/additionals/_body_top.slim
Executable file → Normal file
0
plugins/additionals/app/views/additionals/_global_body_header.slim → plugins/additionals/app/views/additionals/_body_top.slim
Executable file → Normal file
|
@ -0,0 +1,18 @@
|
|||
table.list.issue-report.table-of-values
|
||||
= title_with_fontawesome l(:label_table_of_values), 'far fa-list-alt', 'caption'
|
||||
thead
|
||||
tr
|
||||
th = @chart[:label]
|
||||
th = l :label_total
|
||||
tbody
|
||||
- options = { set_filter: 1 }
|
||||
- @chart[:filters].each do |line|
|
||||
- if line[:filter]
|
||||
- options.merge! line[:filter]
|
||||
tr class="#{cycle 'odd', 'even'}"
|
||||
td.name class="#{line[:id].to_s == '0' ? 'summary' : ''}"
|
||||
- if line[:filter].nil?
|
||||
= line[:name]
|
||||
- else
|
||||
= link_to line[:name], send(@chart[:value_link_method], @project, options)
|
||||
td = line[:count]
|
|
@ -1,5 +0,0 @@
|
|||
- unless (controller_name == 'account' && action_name == 'login') || \
|
||||
(controller_name == 'my') || \
|
||||
(controller_name == 'account' && action_name == 'lost_password')
|
||||
- if Additionals.setting?(:add_go_to_top)
|
||||
a.gototop[href="#gototop"] = l(:label_go_to_top)
|
|
@ -2,25 +2,34 @@ 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
|
||||
- if @query.available_filters.key?('description')
|
||||
= query_as_hidden_field_tags @query, [:description]
|
||||
else
|
||||
= query_as_hidden_field_tags @query
|
||||
p
|
||||
label
|
||||
= radio_button_tag 'c[]', '', true
|
||||
= l(:description_selected_columns)
|
||||
br
|
||||
label
|
||||
= radio_button_tag 'c[]', 'all_inline'
|
||||
= l(:description_all_columns)
|
||||
= 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)
|
||||
br
|
||||
label
|
||||
= radio_button_tag 'c[]', 'all_inline'
|
||||
= 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 Rails.version >= '5.2'
|
||||
= export_csv_encoding_select_tag
|
||||
- if defined?(with_last_notes) && with_last_notes
|
||||
label
|
||||
= check_box_tag 'c[]', 'last_notes', @query.has_column?(:last_notes)
|
||||
= l(:label_last_notes)
|
||||
|
||||
= export_csv_encoding_select_tag
|
||||
|
||||
p.buttons
|
||||
= submit_tag l(:button_export), name: nil, onclick: 'hideModal(this);'
|
||||
'
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
- sidebar = Additionals.settings[:global_sidebar]
|
||||
- if sidebar.present?
|
||||
br
|
||||
.sidebar-additionals
|
||||
= textilizable(sidebar)
|
|
@ -4,4 +4,8 @@
|
|||
h2 class="#{classes}"
|
||||
= @query.new_record? ? l(title) : h(@query.name)
|
||||
span.additionals-live-search
|
||||
= text_field_tag(:search, q, autocomplete: 'off', class: 'live-search-field', placeholder: l(placeholder))
|
||||
= text_field_tag(:search,
|
||||
q,
|
||||
autocomplete: 'off',
|
||||
class: 'live-search-field',
|
||||
placeholder: l(:label_query_name_search))
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
- additionals_top_menu_setup
|
||||
- if Additionals.settings[:external_urls].to_i > 0
|
||||
= javascript_include_tag('redirect', plugin: 'additionals')
|
||||
- if Additionals.settings[:external_urls].to_i == 2
|
||||
= javascript_include_tag('noreferrer', plugin: 'additionals')
|
||||
= additionals_library_load(:font_awesome)
|
||||
= additionals_library_load :font_awesome
|
||||
= stylesheet_link_tag 'additionals', plugin: 'additionals'
|
||||
- if User.current.try(:hrm_user_type_id).nil?
|
||||
= javascript_include_tag 'additionals', plugin: 'additionals'
|
||||
- unless Redmine::Plugin.installed? 'redmine_hrm'
|
||||
- render_custom_top_menu_item
|
||||
|
|
|
@ -1,31 +1,28 @@
|
|||
- options = {} if options.nil?
|
||||
javascript:
|
||||
$("##{field_id}").select2({
|
||||
ajax: {
|
||||
url: "#{ajax_url}",
|
||||
dataType: 'json',
|
||||
delay: 250,
|
||||
data: function(params) {
|
||||
return {
|
||||
q: params.term
|
||||
};
|
||||
$(function() {
|
||||
$("#{defined?(field_id) ? ('#' + field_id) : ('.' + field_class)}").select2({
|
||||
ajax: {
|
||||
url: "#{ajax_url}",
|
||||
dataType: 'json',
|
||||
delay: 250,
|
||||
data: function(params) {
|
||||
return {
|
||||
q: params.term,
|
||||
};
|
||||
},
|
||||
processResults: function(data, params) {
|
||||
return {
|
||||
results: data
|
||||
};
|
||||
},
|
||||
cache: true
|
||||
},
|
||||
processResults: function(data, params) {
|
||||
return {
|
||||
results: data
|
||||
};
|
||||
},
|
||||
cache: true
|
||||
},
|
||||
placeholder: "#{options[:placeholder].presence}",
|
||||
allowClear: #{options[:allow_clear].present? && options[:allow_clear] ? 'true' : 'false'},
|
||||
minimumInputLength: 0,
|
||||
width: '60%',
|
||||
templateResult: formatState
|
||||
});
|
||||
|
||||
function formatState(opt) {
|
||||
if (opt.loading) return opt.name;
|
||||
var $opt = $('<span>' + opt.name_with_icon + '</span>');
|
||||
return $opt;
|
||||
};
|
||||
placeholder: "#{options[:placeholder].presence}",
|
||||
allowClear: #{options[:allow_clear].present? && options[:allow_clear] ? 'true' : 'false'},
|
||||
minimumInputLength: 0,
|
||||
width: "#{options[:width].presence || '60%'}",
|
||||
templateResult: #{options[:template_result].presence || 'formatNameWithIcon'},
|
||||
#{options[:template_selection].present? ? ('templateSelection: ' + options[:template_selection]) : nil}
|
||||
})
|
||||
})
|
||||
|
|
|
@ -2,15 +2,11 @@ fieldset.box
|
|||
legend = l(:additionals_query_list_defaults)
|
||||
- setting_name_columns = "#{query_type}_list_defaults"
|
||||
- query = query_class.new(@settings[setting_name_columns.to_sym])
|
||||
- if Redmine::VERSION.to_s >= '4'
|
||||
.default-query-settings-label-redmine4
|
||||
= render_query_columns_selection(query, name: "settings[#{setting_name_columns}][column_names]")
|
||||
- else
|
||||
.default-query-settings-label
|
||||
= render_query_columns_selection(query, name: "settings[#{setting_name_columns}][column_names]")
|
||||
.default-query-settings-label
|
||||
= render_query_columns_selection(query, name: "settings[#{setting_name_columns}][column_names]")
|
||||
|
||||
- columns = query_class.new.available_totalable_columns
|
||||
- if columns.count > 0
|
||||
- if columns.count.positive?
|
||||
fieldset.box
|
||||
legend = l(:additionals_query_list_default_totals)
|
||||
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
' :
|
||||
- if defined?(editable) && editable
|
||||
#tags-data
|
||||
= additionals_tag_links(entry.tags, tags_without_color: defined?(tags_without_color) ? tags_without_color : false)
|
||||
= additionals_tag_links(entry.tags,
|
||||
project: @project,
|
||||
tags_without_color: defined?(tags_without_color) ? tags_without_color : false)
|
||||
'
|
||||
span.contextual
|
||||
= link_to l(:label_edit_tags),
|
||||
|
@ -15,7 +17,7 @@
|
|||
id: 'edit_tags_link'
|
||||
|
||||
#edit_tags_form style="display: none;"
|
||||
= form_tag(update_url, method: :put, multipart: true ) do
|
||||
= form_tag(update_url, method: :put, multipart: true) do
|
||||
= render partial: 'tags_form'
|
||||
'
|
||||
= submit_tag l(:button_save), class: 'button-small'
|
||||
|
@ -23,4 +25,6 @@
|
|||
= link_to l(:button_cancel), {}, onclick: "$('#edit_tags_form').hide(); $('#tags-data').show(); return false;"
|
||||
|
||||
- else
|
||||
= additionals_tag_links(entry.tags, tags_without_color: defined?(tags_without_color) ? tags_without_color : false)
|
||||
= additionals_tag_links(entry.tags,
|
||||
project: @project,
|
||||
tags_without_color: defined?(tags_without_color) ? tags_without_color : false)
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
.additionals-chart-wrapper
|
||||
.additionals-chart-left
|
||||
canvas id="#{@chart[:id]}" style="width: #{@chart[:width]}px; height: #{@chart[:height]}px;"
|
||||
.additionals-table-of-values
|
||||
= render partial: 'additionals/chart_table_values'
|
||||
|
||||
.clear-both
|
||||
|
||||
javascript:
|
||||
const pie_chart_#{{@chart[:id]}} = new Chart(document.getElementById("#{@chart[:id]}"), {
|
||||
type: 'pie',
|
||||
data: {
|
||||
label_ids: #{raw json_escape(@chart[:label_ids])},
|
||||
labels: #{raw json_escape(@chart[:labels])},
|
||||
datasets: #{raw json_escape(@chart[:datasets])}
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
onClick: function(c, i) {
|
||||
e = i[0];
|
||||
if (e !== undefined && #{{@chart[:filter_path].present? ? 1 : 0}} == 1 ) {
|
||||
var activePoints = pie_chart_#{{@chart[:id]}}.getElementAtEvent(c);
|
||||
var label_id = this.data.label_ids[activePoints[0]._index];
|
||||
window.open("#{{@chart[:filter_path]}}" + label_id);
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
colorschemes: {
|
||||
scheme: "#{@chart[:color_schema]}",
|
||||
fillAlpha: 0.8,
|
||||
},
|
||||
datalabels: {
|
||||
formatter: (value, ctx) => {
|
||||
let sum = 0;
|
||||
let dataArr = ctx.chart.data.datasets[0].data;
|
||||
dataArr.map(data => {
|
||||
sum += data;
|
||||
});
|
||||
let percentage = (value*100 / sum).toFixed(0)+"%";
|
||||
return percentage;
|
||||
},
|
||||
color: '#000',
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,4 +1,3 @@
|
|||
- @settings = ActionController::Parameters.new(@settings) unless Rails.version >= '5.2'
|
||||
' Need Help? :
|
||||
= link_to(l(:label_additionals_doc),
|
||||
'https://additionals.readthedocs.io/en/latest/',
|
||||
|
|
|
@ -1,39 +1,47 @@
|
|||
br
|
||||
h3 = l(:label_content_plural)
|
||||
fieldset.settings
|
||||
legend = l(:label_content_plural)
|
||||
|
||||
p
|
||||
= content_tag(:label, l(:label_account_login))
|
||||
= text_area_tag 'settings[account_login_bottom]', @settings[:account_login_bottom], class: 'wiki-edit', rows: 10
|
||||
em.info
|
||||
= l(:account_login_info)
|
||||
p
|
||||
= content_tag(:label, l(:label_global_sidebar))
|
||||
= text_area_tag 'settings[global_sidebar]', @settings[:global_sidebar], class: 'wiki-edit', rows: 10
|
||||
em.info
|
||||
= l(:global_sidebar_info)
|
||||
p
|
||||
= content_tag(:label, l(:label_global_footer))
|
||||
= text_area_tag 'settings[global_footer]', @settings[:global_footer], class: 'wiki-edit', rows: 5
|
||||
em.info
|
||||
= l(:global_footer_info)
|
||||
p
|
||||
= additionals_settings_textarea :account_login_bottom
|
||||
em.info
|
||||
= l(:account_login_info)
|
||||
p
|
||||
= additionals_settings_textarea :global_sidebar
|
||||
em.info
|
||||
= l(:global_sidebar_info)
|
||||
p
|
||||
= additionals_settings_textarea :global_footer
|
||||
em.info
|
||||
= l(:global_footer_info)
|
||||
|
||||
br
|
||||
h3 = l(:label_setting_plural)
|
||||
p
|
||||
= content_tag(:label, l(:label_external_urls))
|
||||
= select_tag 'settings[external_urls]',
|
||||
options_for_select({ l(:external_url_default) => '0',
|
||||
l(:external_url_new_window) => '1',
|
||||
l(:external_url_noreferrer) => '2' }, @settings['external_urls'])
|
||||
em.info
|
||||
= t(:external_urls_info_html)
|
||||
p
|
||||
= content_tag(:label, l(:label_add_go_to_top))
|
||||
= check_box_tag 'settings[add_go_to_top]', 1, @settings[:add_go_to_top].to_i == 1
|
||||
em.info
|
||||
= t(:add_go_to_top_info)
|
||||
p
|
||||
= content_tag(:label, l(:label_legacy_smiley_support))
|
||||
= check_box_tag 'settings[legacy_smiley_support]', 1, @settings[:legacy_smiley_support].to_i == 1
|
||||
em.info
|
||||
= t(:legacy_smiley_support_info_html)
|
||||
fieldset.settings
|
||||
legend = l(:label_settings)
|
||||
|
||||
p
|
||||
= additionals_settings_checkbox :open_external_urls
|
||||
em.info
|
||||
= t(:open_external_urls_info)
|
||||
p
|
||||
= additionals_settings_checkbox :add_go_to_top
|
||||
em.info
|
||||
= t(:add_go_to_top_info)
|
||||
p
|
||||
= additionals_settings_checkbox :legacy_smiley_support
|
||||
em.info
|
||||
= t(:legacy_smiley_support_info_html)
|
||||
|
||||
fieldset.settings
|
||||
legend = l(:label_disabled_modules)
|
||||
|
||||
p
|
||||
= tag.label l(:label_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_')
|
||||
|
||||
br
|
||||
em.info
|
||||
= l(:disabled_modules_info)
|
||||
|
|
|
@ -1,44 +1,29 @@
|
|||
em.info = t(:top_rules_help)
|
||||
|
||||
br
|
||||
h3 = l(:label_content_plural)
|
||||
|
||||
p
|
||||
= content_tag(:label, l(:label_new_ticket_message))
|
||||
= text_area_tag 'settings[new_ticket_message]', @settings[:new_ticket_message], class: 'wiki-edit', rows: 10
|
||||
= additionals_settings_textarea :new_ticket_message
|
||||
em.info = l(:new_ticket_message_info)
|
||||
|
||||
br
|
||||
hr
|
||||
|
||||
h3 = l(:label_setting_plural)
|
||||
.info = t(:top_rules_help)
|
||||
|
||||
br
|
||||
p
|
||||
= content_tag(:label, l(:label_new_issue_on_profile))
|
||||
= check_box_tag 'settings[new_issue_on_profile]', 1, @settings[:new_issue_on_profile].to_i == 1
|
||||
= additionals_settings_checkbox :new_issue_on_profile
|
||||
p
|
||||
= content_tag(:label, l(:label_issue_assign_to_me))
|
||||
= check_box_tag 'settings[issue_assign_to_me]', 1, @settings[:issue_assign_to_me].to_i == 1
|
||||
= additionals_settings_checkbox :issue_assign_to_me
|
||||
p
|
||||
= content_tag(:label, l(:label_issue_change_status_in_sidebar))
|
||||
= check_box_tag 'settings[issue_change_status_in_sidebar]', 1, @settings[:issue_change_status_in_sidebar].to_i == 1
|
||||
= additionals_settings_checkbox :issue_change_status_in_sidebar
|
||||
p
|
||||
= content_tag(:label, l(:label_issue_autowatch_involved))
|
||||
= check_box_tag 'settings[issue_autowatch_involved]', 1, @settings[:issue_autowatch_involved].to_i == 1
|
||||
= additionals_settings_checkbox :issue_autowatch_involved
|
||||
p
|
||||
= content_tag(:label, l(:label_rule_issue_close_with_open_children))
|
||||
= check_box_tag 'settings[issue_close_with_open_children]', 1, @settings[:issue_close_with_open_children].to_i == 1
|
||||
p
|
||||
= content_tag(:label, l(:label_rule_issue_freezed_with_close))
|
||||
= check_box_tag 'settings[issue_freezed_with_close]', 1, @settings[:issue_freezed_with_close].to_i == 1
|
||||
= additionals_settings_checkbox :issue_freezed_with_close
|
||||
em.info = t(:rule_issue_freezed_with_close_info)
|
||||
|
||||
br
|
||||
|
||||
- rule_status = IssueStatus.sorted
|
||||
p
|
||||
= content_tag(:label, l(:label_rule_issue_status_change))
|
||||
= check_box_tag 'settings[issue_status_change]', 1, @settings[:issue_status_change].to_i == 1
|
||||
= additionals_settings_checkbox :issue_status_change
|
||||
span[style="vertical-align: top; margin-left: 15px;"]
|
||||
= l(:field_status)
|
||||
| x:
|
||||
|
@ -58,8 +43,7 @@ em.info = t(:rule_issue_status_change_info)
|
|||
br
|
||||
br
|
||||
p
|
||||
= content_tag(:label, l(:label_rule_issue_current_user_status))
|
||||
= check_box_tag 'settings[issue_current_user_status]', 1, @settings[:issue_current_user_status].to_i == 1
|
||||
= additionals_settings_checkbox :issue_current_user_status
|
||||
span[style="vertical-align: top; margin-left: 15px;"]
|
||||
= l(:field_status)
|
||||
| x:
|
||||
|
@ -72,8 +56,7 @@ em.info = t(:rule_issue_current_user_status_info_html)
|
|||
br
|
||||
br
|
||||
p
|
||||
= content_tag(:label, l(:label_rule_issue_auto_assign))
|
||||
= check_box_tag 'settings[issue_auto_assign]', 1, @settings[:issue_auto_assign].to_i == 1
|
||||
= additionals_settings_checkbox :issue_auto_assign
|
||||
span[style="vertical-align: top; margin-left: 15px;"]
|
||||
= l(:field_status)
|
||||
| x:
|
||||
|
@ -92,8 +75,7 @@ em.info = t(:rule_issue_auto_assign_info)
|
|||
br
|
||||
br
|
||||
p
|
||||
= content_tag(:label, l(:label_rule_issue_timelog_required))
|
||||
= check_box_tag 'settings[issue_timelog_required]', 1, @settings[:issue_timelog_required].to_i == 1
|
||||
= additionals_settings_checkbox :issue_timelog_required
|
||||
span[style="vertical-align: top; margin-left: 15px;"]
|
||||
= l(:label_tracker_plural)
|
||||
| :
|
||||
|
|
|
@ -4,7 +4,7 @@ em.info
|
|||
br
|
||||
|
||||
p
|
||||
= content_tag(:label, l(:label_hidden_macros_in_toolbar))
|
||||
= tag.label l(:label_hidden_macros_in_toolbar)
|
||||
= hidden_field_tag('settings[hidden_macros_in_toolbar][]', '')
|
||||
- @available_macros = AdditionalsMacro.all(only_names: true).each do |m|
|
||||
label.block
|
||||
|
|
|
@ -1,30 +1,27 @@
|
|||
.info = t(:label_top_menu_help_html)
|
||||
.info = t :label_top_menu_help_html
|
||||
br
|
||||
h3 = l(:label_custom_menu_items)
|
||||
h3 = l :label_custom_menu_items
|
||||
|
||||
- 5.times do |i|
|
||||
fieldset
|
||||
legend
|
||||
b = "#{l(:label_menu_entry)} ##{i + 1}"
|
||||
b = "#{l :label_menu_entry} ##{i + 1}"
|
||||
div
|
||||
p
|
||||
label = h l(:field_name)
|
||||
= text_field_tag('settings[custom_menu' + i.to_s + '_name]', @settings['custom_menu' + i.to_s + '_name'], size: 40)
|
||||
= additionals_settings_textfield "custom_menu#{i}_name".to_sym, label: l(:field_name), size: 40
|
||||
p
|
||||
label = h l(:field_url)
|
||||
= text_field_tag('settings[custom_menu' + i.to_s + '_url]', @settings['custom_menu' + i.to_s + '_url'], size: 80)
|
||||
= additionals_settings_textfield "custom_menu#{i}_url".to_sym, label: l(:field_url), size: 80
|
||||
p
|
||||
label = h l(:field_title)
|
||||
= text_field_tag('settings[custom_menu' + i.to_s + '_title]', @settings['custom_menu' + i.to_s + '_title'], size: 80)
|
||||
= additionals_settings_textfield "custom_menu#{i}_title".to_sym, label: l(:field_title), size: 80
|
||||
i
|
||||
| (
|
||||
= l(:label_optional)
|
||||
= l :label_optional
|
||||
| )
|
||||
p
|
||||
label = h l(:label_permissions)
|
||||
- permission_field = 'custom_menu' + i.to_s + '_roles'
|
||||
- permission_field = "custom_menu#{i}_roles"
|
||||
- menu_roles = Struct.new(:id, :name)
|
||||
= select_tag('settings[' + permission_field + ']',
|
||||
= select_tag("settings[#{permission_field}]",
|
||||
options_from_collection_for_select(Role.sorted.collect { |m| menu_roles.new(m.id, m.name) },
|
||||
:id,
|
||||
:name,
|
||||
|
@ -33,12 +30,12 @@ h3 = l(:label_custom_menu_items)
|
|||
em.info = l(:menu_roles_info)
|
||||
|
||||
br
|
||||
h3 = l(:label_setting_plural)
|
||||
|
||||
h3 = l :label_settings
|
||||
|
||||
p
|
||||
= content_tag(:label, l(:label_remove_help))
|
||||
= check_box_tag 'settings[remove_help]', 1, @settings[:remove_help].to_i == 1
|
||||
em.info = l(:remove_help_info)
|
||||
= additionals_settings_checkbox :remove_help
|
||||
em.info = l :remove_help_info
|
||||
p
|
||||
= content_tag(:label, l(:label_remove_mypage))
|
||||
= check_box_tag 'settings[remove_mypage]', 1, @settings[:remove_mypage].to_i == 1
|
||||
em.info = l(:remove_mypage_info)
|
||||
= additionals_settings_checkbox :remove_mypage
|
||||
em.info = l :remove_mypage_info
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
.info = t(:top_overview_help)
|
||||
|
||||
br
|
||||
h3 = l(:label_content_plural)
|
||||
|
||||
p
|
||||
= content_tag(:label, l(:label_overview_right))
|
||||
= text_area_tag 'settings[overview_right]', @settings[:overview_right], class: 'wiki-edit', rows: 10
|
||||
em.info
|
||||
= l(:overview_right_info)
|
||||
p
|
||||
= content_tag(:label, l(:label_overview_top))
|
||||
= text_area_tag 'settings[overview_top]', @settings[:overview_top], class: 'wiki-edit', rows: 10
|
||||
em.info
|
||||
= l(:overview_top_info)
|
||||
p
|
||||
= content_tag(:label, l(:label_overview_bottom))
|
||||
= text_area_tag 'settings[overview_bottom]', @settings[:overview_bottom], class: 'wiki-edit', rows: 10
|
||||
em.info
|
||||
= l(:overview_bottom_info)
|
||||
|
||||
br
|
||||
h3 = l(:label_setting_plural)
|
||||
|
||||
p
|
||||
= content_tag(:label, l(:label_remove_news))
|
||||
= check_box_tag 'settings[remove_news]', 1, @settings[:remove_news].to_i == 1
|
||||
em.info
|
||||
= l(:remove_news_info)
|
|
@ -1,26 +0,0 @@
|
|||
.info = t(:top_projects_help)
|
||||
br
|
||||
|
||||
p
|
||||
= content_tag(:label, l(:label_project_overview_content))
|
||||
= text_area_tag 'settings[project_overview_content]',
|
||||
@settings[:project_overview_content],
|
||||
class: 'wiki-edit', rows: 10
|
||||
em.info
|
||||
= l(:project_overview_content_info)
|
||||
|
||||
hr
|
||||
|
||||
p
|
||||
= content_tag(:label, l(:label_disabled_modules))
|
||||
= hidden_field_tag('settings[disabled_modules][]', '')
|
||||
- Redmine::AccessControl.available_project_modules_all.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_')
|
||||
|
||||
br
|
||||
|
||||
em.info
|
||||
= l(:disabled_modules_info)
|
|
@ -1,10 +1,5 @@
|
|||
br
|
||||
h3 = l(:label_user_plural)
|
||||
p
|
||||
= content_tag(:label, l(:label_invisible_captcha))
|
||||
= check_box_tag 'settings[invisible_captcha]',
|
||||
1,
|
||||
@settings[:invisible_captcha].to_i == 1,
|
||||
disabled: (true unless Setting.self_registration?)
|
||||
= additionals_settings_checkbox :invisible_captcha,
|
||||
disabled: (true unless Setting.self_registration?)
|
||||
em.info
|
||||
= t(:invisible_captcha_info_html)
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
br
|
||||
h3 = l(:label_web_apis)
|
||||
p
|
||||
= content_tag(:label, l(:label_google_maps_embed_api))
|
||||
= text_field_tag('settings[google_maps_api_key]', @settings[:google_maps_api_key], size: 60)
|
||||
= additionals_settings_textfield :google_maps_api_key, size: 60
|
||||
em.info = t(:google_maps_embed_api_html)
|
||||
= call_hook(:additionals_settings_web_apis, settings: @settings)
|
||||
|
||||
= call_hook :additionals_settings_web_apis, settings: @settings
|
||||
|
|
|
@ -1,34 +1,23 @@
|
|||
.info = t(:top_wiki_help)
|
||||
em.info = t(:top_wiki_help)
|
||||
|
||||
br
|
||||
h3 = l(:label_content_plural)
|
||||
|
||||
p
|
||||
= content_tag(:label, l(:label_global_wiki_sidebar))
|
||||
= text_area_tag 'settings[global_wiki_sidebar]', @settings[:global_wiki_sidebar], class: 'wiki-edit', rows: 10
|
||||
em.info
|
||||
= l(:global_wiki_sidebar_info)
|
||||
p
|
||||
= content_tag(:label, l(:label_global_wiki_header))
|
||||
= text_area_tag 'settings[global_wiki_header]', @settings[:global_wiki_header], class: 'wiki-edit', rows: 5
|
||||
em.info
|
||||
= l(:global_wiki_header_info)
|
||||
p
|
||||
= content_tag(:label, l(:label_global_wiki_footer))
|
||||
= text_area_tag 'settings[global_wiki_footer]', @settings[:global_wiki_footer], class: 'wiki-edit', rows: 5
|
||||
em.info
|
||||
= l(:global_wiki_footer_info)
|
||||
fieldset.settings
|
||||
legend = l(:label_content_plural)
|
||||
|
||||
br
|
||||
h3 = l(:label_pdf_wiki_settings)
|
||||
p
|
||||
= additionals_settings_textarea :global_wiki_sidebar
|
||||
em.info
|
||||
= l(:global_wiki_sidebar_info)
|
||||
|
||||
p
|
||||
= content_tag(:label, l(:label_wiki_pdf_remove_title))
|
||||
= check_box_tag 'settings[wiki_pdf_remove_title]', 1, @settings[:wiki_pdf_remove_title].to_i == 1
|
||||
em.info
|
||||
= l(:wiki_pdf_remove_title_info)
|
||||
p
|
||||
= content_tag(:label, l(:label_wiki_pdf_remove_attachments))
|
||||
= check_box_tag 'settings[wiki_pdf_remove_attachments]', 1, @settings[:wiki_pdf_remove_attachments].to_i == 1
|
||||
em.info
|
||||
= l(:wiki_pdf_remove_attachments_info)
|
||||
fieldset.settings
|
||||
legend = l(:label_pdf_wiki_settings)
|
||||
|
||||
p
|
||||
= additionals_settings_checkbox :wiki_pdf_remove_title
|
||||
em.info
|
||||
= l(:wiki_pdf_remove_title_info)
|
||||
p
|
||||
= additionals_settings_checkbox :wiki_pdf_remove_attachments
|
||||
em.info
|
||||
= l(:wiki_pdf_remove_attachments_info)
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
table.list
|
||||
tr
|
||||
td.name
|
||||
= "#{l(:label_system_info)}:"
|
||||
= l :label_system_info
|
||||
| :
|
||||
td.name
|
||||
= system_info
|
||||
tr
|
||||
td.name
|
||||
= "#{l(:label_uptime)}:"
|
||||
= l :label_uptime
|
||||
| :
|
||||
td.name
|
||||
= system_uptime
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
<%= raw @assignee.map { |principal| {
|
||||
'id' => principal.id,
|
||||
'text' => principal.name,
|
||||
'value' => principal.id
|
||||
}
|
||||
}.to_json
|
||||
%>
|
43
plugins/additionals/app/views/common/_dashboard.html.slim
Normal file
43
plugins/additionals/app/views/common/_dashboard.html.slim
Normal file
|
@ -0,0 +1,43 @@
|
|||
= call_hook :view_dashboard_top, dashboard: dashboard, project: @project
|
||||
|
||||
#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
|
||||
|
||||
= call_hook :view_dashboard_bottom, dashboard: dashboard, project: @project
|
||||
|
||||
= context_menu
|
||||
|
||||
/ required for drap & drop work
|
||||
/ (this should always set, because to support new entries)
|
||||
- include_calendar_headers_tags
|
||||
|
||||
- if dashboard.content.with_chartjs?
|
||||
- content_for :header_tags do
|
||||
= additionals_library_load %i[chartjs chartjs_colorschemes chartjs_datalabels]
|
||||
|
||||
javascript:
|
||||
$(function() {
|
||||
$('#block-select').val('');
|
||||
$('.block-receiver').sortable({
|
||||
connectWith: '.block-receiver',
|
||||
tolerance: 'pointer',
|
||||
handle: '.sort-handle',
|
||||
start: function(event, ui){$(this).parent().addClass('dragging');},
|
||||
stop: function(event, ui){$(this).parent().removeClass('dragging');},
|
||||
update: function(event, ui){
|
||||
// trigger the call on the list that receives the block only
|
||||
if ($(this).find(ui.item).length > 0) {
|
||||
$.ajax({
|
||||
url: "#{escape_javascript _order_blocks_dashboard_path(@project, dashboard)}",
|
||||
type: 'post',
|
||||
data: {
|
||||
'group': $(this).attr('id').replace(/^list-/, ''),
|
||||
'blocks': $.map($(this).children(), function(el){return $(el).attr('id').replace(/^block-/, '');})
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
|
@ -1,11 +1,13 @@
|
|||
- if Additionals.setting?(:issue_freezed_with_close) && !User.current.allowed_to?(:edit_closed_issues, project)
|
||||
- if @issues.detect(&:closed?)
|
||||
ruby:
|
||||
@safe_attributes = []
|
||||
@can[:edit] = false
|
||||
@can[:edit] = false
|
||||
@allowed_statuses = nil
|
||||
@trackers = nil
|
||||
@can[:add_watchers] = nil
|
||||
@can[:delete] = nil
|
||||
@options_by_custom_field = []
|
||||
ruby:
|
||||
if Additionals.setting?(:issue_freezed_with_close) &&
|
||||
!User.current.allowed_to?(:edit_closed_issues, project) &&
|
||||
@issues.detect(&:closed?)
|
||||
@safe_attributes = []
|
||||
@can[:edit] = false
|
||||
@can[:edit] = false
|
||||
@allowed_statuses = nil
|
||||
@trackers = nil
|
||||
@can[:add_watchers] = nil
|
||||
@can[:delete] = nil
|
||||
@options_by_custom_field = []
|
||||
end
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
- if custom_fields_with_full_with_layout.include? @custom_field.class.name
|
||||
p
|
||||
= f.check_box :full_width_layout
|
|
@ -0,0 +1 @@
|
|||
$("#block-<%= block %>").replaceWith("<%= escape_javascript render_dashboard_block(block.to_s, dashboard, sort_options) %>");
|
90
plugins/additionals/app/views/dashboards/_form.html.slim
Normal file
90
plugins/additionals/app/views/dashboards/_form.html.slim
Normal file
|
@ -0,0 +1,90 @@
|
|||
= error_messages_for 'dashboard'
|
||||
|
||||
.box.tabular.attributes
|
||||
p
|
||||
= f.text_field :name, size: 255, required: true
|
||||
|
||||
p
|
||||
= f.text_area :description, rows: addtionals_textarea_cols(@dashboard.description, min: 4), class: 'wiki-edit'
|
||||
|
||||
.splitcontent
|
||||
.splitcontentleft
|
||||
|
||||
- if @dashboard.new_record?
|
||||
= hidden_field_tag 'dashboard[dashboard_type]', @dashboard.dashboard_type
|
||||
|
||||
- if @project && @allowed_projects.present? && @allowed_projects.count > 1
|
||||
p
|
||||
= f.select :project_id,
|
||||
project_tree_options_for_select(@allowed_projects,
|
||||
selected: @dashboard.project,
|
||||
include_blank: true),
|
||||
{},
|
||||
disabled: !@dashboard.project_id_can_change?
|
||||
em.info
|
||||
= l(:info_dashboard_project_select)
|
||||
- else
|
||||
= hidden_field_tag 'dashboard[project_id]', @project&.id
|
||||
|
||||
- if User.current.allowed_to?(:share_dashboards, @project, global: true) || \
|
||||
User.current.allowed_to?(:set_system_dashboards, @project, global: true)
|
||||
|
||||
p
|
||||
label = l(:field_visible)
|
||||
label.block
|
||||
= radio_button 'dashboard', 'visibility', Dashboard::VISIBILITY_PRIVATE
|
||||
'
|
||||
= l(:label_visibility_private)
|
||||
label.block
|
||||
= radio_button 'dashboard', 'visibility', Dashboard::VISIBILITY_PUBLIC
|
||||
'
|
||||
= l(:label_visibility_public)
|
||||
label.block
|
||||
= radio_button 'dashboard', 'visibility', Dashboard::VISIBILITY_ROLES
|
||||
'
|
||||
= l(:label_visibility_roles)
|
||||
' :
|
||||
- Role.givable.sorted.each do |role|
|
||||
label.block.role-visibility
|
||||
= check_box_tag 'dashboard[role_ids][]', role.id, @dashboard.role_ids.include?(role.id), id: nil
|
||||
'
|
||||
= role.name
|
||||
= hidden_field_tag 'dashboard[role_ids][]', ''
|
||||
|
||||
.splitcontentright
|
||||
p
|
||||
= f.check_box :enable_sidebar
|
||||
|
||||
- if User.current.allowed_to? :set_system_dashboards, @project, global: true
|
||||
p = f.check_box :system_default, disabled: !@dashboard.destroyable?
|
||||
p#always-expose = f.check_box :always_expose
|
||||
- elsif @dashboard.system_default?
|
||||
p = f.check_box :system_default, disabled: true
|
||||
p = f.check_box :always_expose
|
||||
|
||||
- if @dashboard.persisted?
|
||||
p.object-select
|
||||
= f.select :author_id,
|
||||
author_options_for_select(@project, @dashboard, :save_dashboards),
|
||||
required: true
|
||||
|
||||
= call_hook :view_dashboard_form_details_bottom, dashboard: @dashboard, form: f
|
||||
|
||||
javascript:
|
||||
$(function() {
|
||||
$("input[name='dashboard[visibility]']").change(function(){
|
||||
var roles_checked = $('#dashboard_visibility_1').is(':checked');
|
||||
var private_checked = $('#dashboard_visibility_0').is(':checked');
|
||||
$("input[name='dashboard[role_ids][]'][type=checkbox]").attr('disabled', !roles_checked);
|
||||
}).trigger('change');
|
||||
|
||||
$("input[name='dashboard[system_default]']").change(function(){
|
||||
var selection = $('#dashboard_system_default').is(':checked');
|
||||
if (selection) {
|
||||
$('#always-expose').show();
|
||||
}
|
||||
else {
|
||||
$('#always-expose').hide();
|
||||
}
|
||||
}).trigger('change');
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
$("#block-<%= escape_javascript @block %>").remove();
|
||||
$("#list-top").prepend("<%= escape_javascript render_dashboard_blocks([@block], @dashboard) %>");
|
||||
$("#block-select").replaceWith("<%= escape_javascript dashboard_block_select_tag(@dashboard) %>");
|
|
@ -0,0 +1,4 @@
|
|||
p#errorExplanation
|
||||
' An error occurred while executing dashboard block
|
||||
= tag.i @block
|
||||
' and has been logged. Please report this error to your Redmine administrator.
|
|
@ -0,0 +1,11 @@
|
|||
- cache render_async_cache_key(_dashboard_async_blocks_path(@project,
|
||||
dashboard.async_params(block, async, settings))),
|
||||
expires_in: async[:cache_expires_in] || DashboardContent::RENDER_ASYNC_CACHE_EXPIRES_IN,
|
||||
skip_digest: true do
|
||||
|
||||
- events_by_day = activity_dashboard_data settings, dashboard
|
||||
- title = Additionals.true?(settings[:me_only]) ? l(:label_my_activity) : l(:label_activity)
|
||||
h3 = link_to title, activity_path(user_id: User.current,
|
||||
from: events_by_day.keys.first)
|
||||
|
||||
= render partial: 'activities/activities', locals: { events_by_day: events_by_day }
|
|
@ -0,0 +1,21 @@
|
|||
- max_entries = settings[:max_entries].presence || DashboardContent::DEFAULT_MAX_ENTRIES
|
||||
div id="#{block}-settings" style="#{'display: none;' if hide}"
|
||||
= form_tag(_update_layout_setting_dashboard_path(@project, @dashboard), remote: true) do
|
||||
= hidden_field_tag "settings[#{block}][me_only]", '0'
|
||||
.box
|
||||
p
|
||||
label
|
||||
= l(:label_max_entries)
|
||||
' :
|
||||
= number_field_tag "settings[#{block}][max_entries]", max_entries, min: 1, max: 1000, required: true
|
||||
|
||||
p
|
||||
label
|
||||
= l(:label_only_my_activities)
|
||||
' :
|
||||
= check_box_tag "settings[#{block}][me_only]", '1', Additionals.true?(settings[:me_only])
|
||||
|
||||
p
|
||||
= submit_tag l(:button_save)
|
||||
'
|
||||
= link_to_function l(:button_cancel), "$('##{block}-settings').toggle();"
|
|
@ -0,0 +1,31 @@
|
|||
- with_async = dashboard_async_required_settings? settings, async
|
||||
|
||||
- unless with_async
|
||||
h3 = settings[:title].presence || block_definition[:label]
|
||||
|
||||
- if @can_edit && \
|
||||
block_definition[:no_settings].blank? && \
|
||||
(!block_definition.key?(:with_settings_if) || block_definition[:with_settings_if].call(@project))
|
||||
= render partial: block_definition[:settings_partial].presence || "#{async[:partial]}_settings",
|
||||
locals: build_dashboard_partial_locals(block,
|
||||
block_definition,
|
||||
settings,
|
||||
dashboard).merge({ hide: with_async })
|
||||
|
||||
- if with_async
|
||||
= render_async_cache _dashboard_async_blocks_path(@project,
|
||||
dashboard.async_params(block, async, settings)) do
|
||||
.clear-both
|
||||
p
|
||||
i.fas.fa-sync.fa-spin
|
||||
'
|
||||
= l(:label_loading)
|
||||
|
||||
= content_for :render_async
|
||||
|
||||
javascript:
|
||||
$(function() {
|
||||
$('#ajax-indicator').hide();
|
||||
})
|
||||
- else
|
||||
p.nodata = l :label_no_data
|
|
@ -0,0 +1,27 @@
|
|||
h3.icon.icon-document = l :label_document_plural
|
||||
|
||||
- if @can_edit
|
||||
div id="#{block}-settings" style='display: none;'
|
||||
= form_tag(_update_layout_setting_dashboard_path(@project, @dashboard), remote: true) do
|
||||
.box
|
||||
p
|
||||
label
|
||||
= l(:label_max_entries)
|
||||
' :
|
||||
= number_field_tag "settings[#{block}][max_entries]", max_entries, min: 1, max: 1000, required: true
|
||||
p
|
||||
= submit_tag l(:button_save)
|
||||
'
|
||||
= link_to_function l(:button_cancel), "$('##{block}-settings').toggle();"
|
||||
|
||||
- if documents.any?
|
||||
= render partial: 'documents/document', collection: documents
|
||||
p
|
||||
- if @project
|
||||
= link_to l(:label_document_view_all), project_documents_path(@project)
|
||||
/- else
|
||||
/ no route available
|
||||
/= link_to l(:label_news_view_all), documents_path
|
||||
|
||||
- else
|
||||
p.nodata = l :label_no_data
|
|
@ -0,0 +1,23 @@
|
|||
- cache render_async_cache_key(_dashboard_async_blocks_path(@project,
|
||||
dashboard.async_params(block, async, settings))),
|
||||
expires_in: async[:cache_expires_in],
|
||||
skip_digest: true do
|
||||
|
||||
- feed = dashboard_feed_catcher settings[:url], settings[:max_entries]
|
||||
h3
|
||||
= dashboard_feed_title settings[:title], block_definition
|
||||
|
||||
- if feed[:valid]
|
||||
- if feed[:items].count.positive?
|
||||
ul.reporting-list.feed
|
||||
- feed[:items].each do |item|
|
||||
li
|
||||
= link_to item[:title],
|
||||
item[:link],
|
||||
class: 'external', rel: 'noopener noreferrer', target: '_blank'
|
||||
- else
|
||||
p.nodata = l :label_no_data
|
||||
- elsif settings[:url].blank?
|
||||
p.nodata = l :label_no_data
|
||||
- else
|
||||
p.nodata = l(:label_invalid_feed_data)
|
|
@ -0,0 +1,25 @@
|
|||
- max_entries = settings[:max_entries].presence || DashboardContent::DEFAULT_MAX_ENTRIES
|
||||
|
||||
div id="#{block}-settings" style="#{'display: none;' if hide}"
|
||||
= form_tag(_update_layout_setting_dashboard_path(@project, @dashboard), remote: true) do
|
||||
.box
|
||||
p
|
||||
label
|
||||
= l :field_title
|
||||
' :
|
||||
= text_field_tag "settings[#{block}][title]", dashboard_feed_title(settings[:title], block_definition)
|
||||
p
|
||||
label
|
||||
= l :field_url
|
||||
' :
|
||||
= url_field_tag "settings[#{block}][url]", settings[:url], required: true
|
||||
p
|
||||
label
|
||||
= l(:label_max_entries)
|
||||
' :
|
||||
= number_field_tag "settings[#{block}][max_entries]", max_entries, min: 1, max: 100, required: true
|
||||
|
||||
p
|
||||
= submit_tag l(:button_save)
|
||||
'
|
||||
= link_to_function l(:button_cancel), "$('##{block}-settings').toggle();"
|
|
@ -0,0 +1,35 @@
|
|||
h3 = block_definition[:label]
|
||||
|
||||
- if @can_edit
|
||||
div id="#{block}-settings" style='display: none;'
|
||||
= form_tag(_update_layout_setting_dashboard_path(@project, @dashboard), remote: true) do
|
||||
.box
|
||||
p
|
||||
label
|
||||
= l :button_show
|
||||
' :
|
||||
= number_field_tag "settings[#{block}][days]", days, min: 1, max: 1000, required: true
|
||||
'
|
||||
= l :label_day_plural
|
||||
p
|
||||
= submit_tag l(:button_save)
|
||||
'
|
||||
= link_to_function l(:button_cancel), "$('#my_spent_time-settings').toggle();"
|
||||
|
||||
ul.reporting-list
|
||||
li.today
|
||||
= l :label_today
|
||||
' :
|
||||
= l_hours_short entries_today.sum(&:hours)
|
||||
|
||||
li.days
|
||||
= l :label_last_n_days, days
|
||||
' :
|
||||
= l_hours_short entries_days.sum(&:hours)
|
||||
|
||||
= link_to l(:label_spent_time), _time_entries_path(@project, nil, user_id: 'me')
|
||||
'
|
||||
= link_to l(:button_log_time),
|
||||
_new_time_entry_path(@project, nil),
|
||||
class: 'icon-only icon-add',
|
||||
title: l(:button_log_time)
|
|
@ -0,0 +1,25 @@
|
|||
h3.icon.icon-news = l :label_news_latest
|
||||
|
||||
- if @can_edit
|
||||
div id="#{block}-settings" style='display: none;'
|
||||
= form_tag(_update_layout_setting_dashboard_path(@project, @dashboard), remote: true) do
|
||||
.box
|
||||
p
|
||||
label
|
||||
= l(:label_max_entries)
|
||||
' :
|
||||
= number_field_tag "settings[#{block}][max_entries]", max_entries, min: 1, max: 1000, required: true
|
||||
p
|
||||
= submit_tag l(:button_save)
|
||||
'
|
||||
= link_to_function l(:button_cancel), "$('##{block}-settings').toggle();"
|
||||
|
||||
- if news.any?
|
||||
= render partial: 'news/news', collection: news
|
||||
p
|
||||
- if @project
|
||||
= link_to l(:label_news_view_all), project_news_index_path(@project)
|
||||
- else
|
||||
= link_to l(:label_news_view_all), news_index_path
|
||||
- else
|
||||
p.nodata = l :label_no_data
|
|
@ -0,0 +1,21 @@
|
|||
h3 = block_definition[:label]
|
||||
|
||||
- if @project.description.present?
|
||||
.wiki.project-description
|
||||
= textilizable @project.description
|
||||
- if @project.homepage.present? || @project.visible_custom_field_values.any? { |o| o.value.present? }
|
||||
ul.reporting-list
|
||||
- if @project.homepage.present?
|
||||
li
|
||||
span.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|
|
||||
li class="#{custom_field.css_classes}"
|
||||
span.label
|
||||
= custom_field.name
|
||||
' :
|
||||
= formatted
|
||||
|
||||
= call_hook :view_projects_show_dashboard_info_block, project: @project, dashboard: dashboard
|
|
@ -0,0 +1,42 @@
|
|||
h3.icon.icon-issue
|
||||
= l :label_issue_tracking
|
||||
'
|
||||
= link_to l(:label_details),
|
||||
project_issues_report_details_path(@project, detail: 'tracker'),
|
||||
class: 'icon-only icon-zoom-in',
|
||||
title: l(:label_details)
|
||||
|
||||
- if @trackers.present?
|
||||
table.list.issue-report
|
||||
thead
|
||||
tr
|
||||
th
|
||||
th
|
||||
= l :label_open_issues_plural
|
||||
th
|
||||
= l :label_closed_issues_plural
|
||||
th
|
||||
= l :label_total
|
||||
tbody
|
||||
- @trackers.each do |tracker|
|
||||
tr
|
||||
td.name
|
||||
= link_to tracker.name, project_issues_path(@project, set_filter: 1, tracker_id: tracker.id), title: tracker.description
|
||||
td
|
||||
= link_to @open_issues_by_tracker[tracker].to_i, project_issues_path(@project, set_filter: 1, tracker_id: tracker.id)
|
||||
td
|
||||
= link_to (@total_issues_by_tracker[tracker].to_i - @open_issues_by_tracker[tracker].to_i),
|
||||
project_issues_path(@project, set_filter: 1, tracker_id: tracker.id, status_id: 'c')
|
||||
td.total
|
||||
= link_to @total_issues_by_tracker[tracker].to_i,
|
||||
project_issues_path(@project, set_filter: 1, tracker_id: tracker.id, status_id: '*')
|
||||
p
|
||||
= link_to l(:label_issue_view_all), project_issues_path(@project, set_filter: 1)
|
||||
' |
|
||||
= link_to l(:field_summary), project_issues_report_path(@project)
|
||||
- if User.current.allowed_to? :view_calendar, @project, global: true
|
||||
' |
|
||||
= link_to l(:label_calendar), project_calendar_path(@project)
|
||||
- if User.current.allowed_to? :view_gantt, @project, global: true
|
||||
' |
|
||||
= link_to l(:label_gantt), project_gantt_path(@project)
|
|
@ -0,0 +1,7 @@
|
|||
- if @subprojects.any?
|
||||
h3.icon.icon-projects
|
||||
= l(:label_subproject_plural)
|
||||
ul.subprojects
|
||||
- @subprojects.each do |project|
|
||||
li
|
||||
= link_to(project.name, project_path(project), class: project.css_classes).html_safe
|
|
@ -0,0 +1,20 @@
|
|||
h3.icon.icon-time
|
||||
= block_definition[:label]
|
||||
ul
|
||||
- if @total_estimated_hours.present?
|
||||
li
|
||||
= l :field_estimated_hours
|
||||
' :
|
||||
= l_hours @total_estimated_hours
|
||||
- if @total_hours.present?
|
||||
li
|
||||
= l :label_spent_time
|
||||
' :
|
||||
= l_hours @total_hours
|
||||
p
|
||||
- if User.current.allowed_to? :log_time, @project
|
||||
= link_to l(:button_log_time), new_project_time_entry_path(@project)
|
||||
' |
|
||||
= link_to l(:label_details), project_time_entries_path(@project)
|
||||
' |
|
||||
= link_to l(:label_report), report_project_time_entries_path(@project)
|
|
@ -0,0 +1,34 @@
|
|||
- cache render_async_cache_key(_dashboard_async_blocks_path(@project, dashboard.async_params(block, async, settings))),
|
||||
expires_in: DashboardContent::RENDER_ASYNC_CACHE_EXPIRES_IN,
|
||||
skip_digest: true do
|
||||
|
||||
- query = klass.visible.find_by(id: settings[:query_id])
|
||||
- if query
|
||||
ruby:
|
||||
query.project = @project if query_block[:with_project]
|
||||
count = query.send query_block[:count_method]
|
||||
query.column_names = settings[:columns].split(',').map(&:to_sym) if settings[:columns].present?
|
||||
query.sort_criteria = params[:sort] if params[:sort].present?
|
||||
|
||||
h3.query-list-block
|
||||
= dashboard_query_list_block_title query, query_block, @project
|
||||
= " (#{count})"
|
||||
= dashboard_query_list_block_alerts dashboard, query, block_definition
|
||||
|
||||
- if query.respond_to?(:description) && query.description.present?
|
||||
.query-description
|
||||
= textilizable query, :description
|
||||
|
||||
- if count.positive?
|
||||
/ required by some helpers of other plugins
|
||||
- @query = query
|
||||
= render partial: query_block[:list_partial],
|
||||
locals: { query_block[:entities_var] => query.send(query_block[:entries_method],
|
||||
limit: settings[:max_entries] || DashboardContent::DEFAULT_MAX_ENTRIES),
|
||||
query: query,
|
||||
query_options: { sort_param: 'sort',
|
||||
sort_link_options: { method: :post, remote: true } } }
|
||||
- else
|
||||
p.nodata = l :label_no_data
|
||||
- else
|
||||
p.nodata = l :label_no_data
|
|
@ -0,0 +1,27 @@
|
|||
- query = klass.find_by id: settings[:query_id]
|
||||
- query.column_names = settings[:columns].map(&:to_sym) if query && settings[:columns].present?
|
||||
|
||||
div id="#{block}-settings" style="#{'display: none;' if hide}"
|
||||
= form_tag(_update_layout_setting_dashboard_path(@project, dashboard), remote: true) do
|
||||
.box
|
||||
- if query
|
||||
= render_query_columns_selection query, name: "settings[#{block}][columns]"
|
||||
- else
|
||||
p
|
||||
label
|
||||
= block_definition[:label]
|
||||
'
|
||||
= select_tag "settings[#{block}][query_id]",
|
||||
options_for_query_select(klass, @project),
|
||||
required: true
|
||||
p
|
||||
label
|
||||
= l :label_max_entries
|
||||
' :
|
||||
= number_field_tag "settings[#{block}][max_entries]",
|
||||
settings[:max_entries].presence || DashboardContent::DEFAULT_MAX_ENTRIES,
|
||||
min: 1, max: 100, required: true
|
||||
p
|
||||
= submit_tag l(:button_save)
|
||||
'
|
||||
= link_to_function l(:button_cancel), "$('##{block}-settings').toggle();"
|
|
@ -0,0 +1,26 @@
|
|||
ruby:
|
||||
title = settings[:title] || l(:label_text)
|
||||
text = settings[:text]
|
||||
|
||||
- if title.present?
|
||||
h3 = title
|
||||
|
||||
- if @can_edit
|
||||
div id="#{block}-settings" style='display: none;'
|
||||
= form_tag(_update_layout_setting_dashboard_path(@project, @dashboard), remote: true) do
|
||||
.box
|
||||
p
|
||||
label
|
||||
= l :field_title
|
||||
' :
|
||||
= text_field_tag "settings[#{block}][title]", title
|
||||
p
|
||||
= text_area_tag "settings[#{block}][text]", text, rows: addtionals_textarea_cols(text), class: 'wiki-edit'
|
||||
= wikitoolbar_for "settings_#{block}_text"
|
||||
p
|
||||
= submit_tag l(:button_save)
|
||||
'
|
||||
= link_to_function l(:button_cancel), "$('##{block}-settings').toggle();"
|
||||
|
||||
.wiki
|
||||
= textilizable text
|
|
@ -0,0 +1,2 @@
|
|||
.wiki
|
||||
= textilizable Setting.welcome_text
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue