Project

General

Profile

Patch #20384 » workspaces.patch

Frederico Camara, 2017-03-13 19:20

View differences:

app/controllers/admin_controller.rb
36 36
    scope = scope.like(params[:name]) if params[:name].present?
37 37
    @projects = scope.to_a
38 38

  
39
    @workspaces = Hash[Workspace.pluck(:id, :name)]
40

  
39 41
    render :action => "projects", :layout => false if request.xhr?
40 42
  end
41 43

  
app/controllers/workflows_controller.rb
23 23
  def index
24 24
    @roles = Role.sorted.select(&:consider_workflow?)
25 25
    @trackers = Tracker.sorted
26
    @workflow_counts = WorkflowTransition.group(:tracker_id, :role_id).count
26
    @workspaces = Workspace.sorted
27
    @workflow_counts = WorkflowTransition.group(:tracker_id, :role_id, :workspace_id).count
27 28
  end
28 29

  
29 30
  def edit
30 31
    find_trackers_roles_and_statuses_for_edit
31 32

  
32
    if request.post? && @roles && @trackers && params[:transitions]
33
    if request.post? && @roles && @trackers && @workspaces && params[:transitions]
33 34
      transitions = params[:transitions].deep_dup
34 35
      transitions.each do |old_status_id, transitions_by_new_status|
35 36
        transitions_by_new_status.each do |new_status_id, transition_by_rule|
36 37
          transition_by_rule.reject! {|rule, transition| transition == 'no_change'}
37 38
        end
38 39
      end
39
      WorkflowTransition.replace_transitions(@trackers, @roles, transitions)
40
      WorkflowTransition.replace_transitions(@trackers, @roles, transitions, @workspaces)
40 41
      flash[:notice] = l(:notice_successful_update)
41 42
      redirect_to_referer_or workflows_edit_path
42 43
      return
43 44
    end
44 45

  
45
    if @trackers && @roles && @statuses.any?
46
    if @trackers && @roles && @workspaces && @statuses.any?
46 47
      workflows = WorkflowTransition.
47
        where(:role_id => @roles.map(&:id), :tracker_id => @trackers.map(&:id)).
48
        where(:role_id => @roles.map(&:id), :tracker_id => @trackers.map(&:id), :workspace_id => @workspaces.map(&:id)).
48 49
        preload(:old_status, :new_status)
49 50
      @workflows = {}
50 51
      @workflows['always'] = workflows.select {|w| !w.author && !w.assignee}
......
56 57
  def permissions
57 58
    find_trackers_roles_and_statuses_for_edit
58 59

  
59
    if request.post? && @roles && @trackers && params[:permissions]
60
    if request.post? && @roles && @trackers && @workspaces && params[:permissions]
60 61
      permissions = params[:permissions].deep_dup
61 62
      permissions.each { |field, rule_by_status_id|
62 63
        rule_by_status_id.reject! {|status_id, rule| rule == 'no_change'}
63 64
      }
64
      WorkflowPermission.replace_permissions(@trackers, @roles, permissions)
65
      WorkflowPermission.replace_permissions(@trackers, @roles, permissions, @workspaces)
65 66
      flash[:notice] = l(:notice_successful_update)
66 67
      redirect_to_referer_or workflows_permissions_path
67 68
      return
68 69
    end
69 70

  
70
    if @roles && @trackers
71
    if @roles && @trackers && @workspaces
71 72
      @fields = (Tracker::CORE_FIELDS_ALL - @trackers.map(&:disabled_core_fields).reduce(:&)).map {|field| [field, l("field_"+field.sub(/_id$/, ''))]}
72 73
      @custom_fields = @trackers.map(&:custom_fields).flatten.uniq.sort
73
      @permissions = WorkflowPermission.rules_by_status_id(@trackers, @roles)
74
      @permissions = WorkflowPermission.rules_by_status_id(@trackers, @roles, @workspaces)
74 75
      @statuses.each {|status| @permissions[status.id] ||= {}}
75 76
    end
76 77
  end
......
78 79
  def copy
79 80
    @roles = Role.sorted.select(&:consider_workflow?)
80 81
    @trackers = Tracker.sorted
82
    @workspaces = Workspace.sorted
81 83

  
82 84
    if params[:source_tracker_id].blank? || params[:source_tracker_id] == 'any'
83 85
      @source_tracker = nil
......
89 91
    else
90 92
      @source_role = Role.find_by_id(params[:source_role_id].to_i)
91 93
    end
94
    if params[:source_workspace_id].blank? || params[:source_workspace_id] == 'any'
95
      @source_workspace = nil
96
    else
97
      @source_workspace = Workspace.find_by_id(params[:source_workspace_id].to_i)
98
    end
92 99
    @target_trackers = params[:target_tracker_ids].blank? ?
93 100
        nil : Tracker.where(:id => params[:target_tracker_ids]).to_a
94 101
    @target_roles = params[:target_role_ids].blank? ?
95 102
        nil : Role.where(:id => params[:target_role_ids]).to_a
103
    @target_workspaces = params[:target_workspace_ids].blank? ?
104
        nil : Workspace.where(:id => params[:target_workspace_ids]).to_a
96 105
    if request.post?
97
      if params[:source_tracker_id].blank? || params[:source_role_id].blank? || (@source_tracker.nil? && @source_role.nil?)
106
      if params[:source_tracker_id].blank? || params[:source_role_id].blank? || params[:source_workspace_id].blank? || (@source_tracker.nil? && @source_role.nil? && @source_workspace.nil?)
98 107
        flash.now[:error] = l(:error_workflow_copy_source)
99
      elsif @target_trackers.blank? || @target_roles.blank?
108
      elsif @target_trackers.blank? || @target_roles.blank? || @target_workspaces.blank?
100 109
        flash.now[:error] = l(:error_workflow_copy_target)
101 110
      else
102
        WorkflowRule.copy(@source_tracker, @source_role, @target_trackers, @target_roles)
111
        WorkflowRule.copy(@source_tracker, @source_role, @source_workspace, @target_trackers, @target_roles, @target_workspaces)
103 112
        flash[:notice] = l(:notice_successful_update)
104
        redirect_to workflows_copy_path(:source_tracker_id => @source_tracker, :source_role_id => @source_role)
113
        redirect_to workflows_copy_path(:source_tracker_id => @source_tracker, :source_role_id => @source_role, :source_workspace_id => @source_workspace)
105 114
      end
106 115
    end
107 116
  end
......
111 120
  def find_trackers_roles_and_statuses_for_edit
112 121
    find_roles
113 122
    find_trackers
123
    find_workspaces
114 124
    find_statuses
115 125
  end
116 126

  
......
134 144
    @trackers = nil if @trackers.blank?
135 145
  end
136 146

  
147
  def find_workspaces
148
    ids = Array.wrap(params[:workspace_id])
149
    if ids == ['all']
150
      @workspaces = Workspace.sorted.to_a
151
    elsif ids.present?
152
      @workspaces = Workspace.where(:id => ids).to_a
153
    end
154
    @workspaces = nil if @workspaces.blank?
155
  end
156

  
137 157
  def find_statuses
138 158
    @used_statuses_only = (params[:used_statuses_only] == '0' ? false : true)
139 159
    if @trackers && @used_statuses_only
app/controllers/workspaces_controller.rb
1
# Redmine - project management software
2
# Copyright (C) 2006-2015  Jean-Philippe Lang
3
#
4
# This program is free software; you can redistribute it and/or
5
# modify it under the terms of the GNU General Public License
6
# as published by the Free Software Foundation; either version 2
7
# of the License, or (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17

  
18
class WorkspacesController < ApplicationController
19
  layout 'admin'
20

  
21
  before_filter :require_admin, :except => :index
22
  before_filter :require_admin_or_api_request, :only => :index
23
  accept_api_auth :index
24

  
25
  def index
26
    respond_to do |format|
27
      format.html {
28
        @workspace_pages, @workspaces = paginate Workspace.sorted, :per_page => 25
29
        render :action => "index", :layout => false if request.xhr?
30
      }
31
      format.api {
32
        @workspaces = Workspace.order('position').to_a
33
      }
34
    end
35
  end
36

  
37
  def new
38
    @workspace = Workspace.new
39
  end
40

  
41
  def create
42
    @workspace = Workspace.new(params[:workspace])
43
    if @workspace.save
44
      flash[:notice] = l(:notice_successful_create)
45
      redirect_to workspaces_path
46
    else
47
      render :action => 'new'
48
    end
49
  end
50

  
51
  def edit
52
    @workspace = Workspace.find(params[:id])
53
  end
54

  
55
  def update
56
    @workspace = Workspace.find(params[:id])
57
    if @workspace.update_attributes(params[:workspace])
58
      flash[:notice] = l(:notice_successful_update)
59
      redirect_to workspaces_path(:page => params[:page])
60
    else
61
      render :action => 'edit'
62
    end
63
  end
64

  
65
  def destroy
66
    unless Project.where(:workspace_id => params[:id]).any? || params[:id] == "1"
67
      Workspace.find(params[:id]).destroy
68
      redirect_to workspaces_path
69
    else
70
      flash[:error] = l(:error_unable_delete_workspace)
71
      redirect_to workspaces_path
72
    end
73
  end
74
end
app/helpers/projects_helper.rb
95 95
    version_options_for_select(versions, project.default_version)
96 96
  end
97 97

  
98
  def project_workspace_options(project)
99
    grouped = Hash.new {|h,k| h[k] = []}
100
    Workspace.all.sorted.each do |workspace|
101
      grouped[workspace.name] = workspace.id
102
    end
103
    options_for_select(grouped, project.workspace_id)
104
  end
105

  
106
  def used_workspaces_by_tracker(tracker)
107
    WorkflowTransition.where(:tracker_id => tracker).map{|t| "ws-" + t.workspace_id.to_s}.uniq.join(" ")
108
  end
109

  
98 110
  def format_version_sharing(sharing)
99 111
    sharing = 'none' unless Version::VERSION_SHARINGS.include?(sharing)
100 112
    l("label_version_sharing_#{sharing}")
app/helpers/workflows_helper.rb
53 53
    html_options = {}
54 54
    
55 55
    if perm = permissions[status.id][name]
56
      if perm.uniq.size > 1 || perm.size < @roles.size * @trackers.size
56
      if perm.uniq.size > 1 || perm.size < @roles.size * @trackers.size * @workspaces.size
57 57
        options << [l(:label_no_change_option), "no_change"]
58 58
        selected = 'no_change'
59 59
      else
......
78 78
    w = workflows.select {|w| w.old_status == old_status && w.new_status == new_status}.size
79 79
    
80 80
    tag_name = "transitions[#{ old_status.try(:id) || 0 }][#{new_status.id}][#{name}]"
81
    if w == 0 || w == @roles.size * @trackers.size
81
    if w == 0 || w == @roles.size * @trackers.size * @workspaces.size
82 82
      
83 83
      hidden_field_tag(tag_name, "0", :id => nil) +
84 84
      check_box_tag(tag_name, "1", w != 0,
app/models/issue.rb
623 623
    user_real = user || User.current
624 624
    roles = user_real.admin ? Role.all.to_a : user_real.roles_for_project(project)
625 625
    roles = roles.select(&:consider_workflow?)
626
    workspace = Project.where(:id => project).pluck(:workspace_id)
626 627
    return {} if roles.empty?
627 628

  
628 629
    result = {}
629
    workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).to_a
630
    workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id), :workspace_id => workspace).to_a
630 631
    if workflow_permissions.any?
631 632
      workflow_rules = workflow_permissions.inject({}) do |h, wp|
632 633
        h[wp.field_name] ||= {}
......
925 926
        initial_status,
926 927
        user.admin ? Role.all.to_a : user.roles_for_project(project),
927 928
        tracker,
929
        project.workspace_id,
928 930
        author == user,
929 931
        assignee_transitions_allowed
930 932
      )
app/models/issue_status.rb
45 45
  end
46 46

  
47 47
  # Returns an array of all statuses the given role can switch to
48
  def new_statuses_allowed_to(roles, tracker, author=false, assignee=false)
49
    self.class.new_statuses_allowed(self, roles, tracker, author, assignee)
48
  def new_statuses_allowed_to(roles, tracker, workspace_id, author=false, assignee=false)
49
    self.class.new_statuses_allowed(self, roles, tracker, workspace_id, author, assignee)
50 50
  end
51 51
  alias :find_new_statuses_allowed_to :new_statuses_allowed_to
52 52

  
53
  def self.new_statuses_allowed(status, roles, tracker, author=false, assignee=false)
54
    if roles.present? && tracker
53
  def self.new_statuses_allowed(status, roles, tracker, workspace_id, author=false, assignee=false)
54
    if roles.present? && tracker && workspace_id
55 55
      status_id = status.try(:id) || 0
56

  
57 56
      scope = IssueStatus.
58 57
        joins(:workflow_transitions_as_new_status).
59
        where(:workflows => {:old_status_id => status_id, :role_id => roles.map(&:id), :tracker_id => tracker.id})
58
        where(:workflows => {:old_status_id => status_id, :role_id => roles.map(&:id), :tracker_id => tracker.id, :workspace_id => workspace_id})
60 59

  
61 60
      unless author && assignee
62 61
        if author || assignee
app/models/project.rb
39 39
  has_many :issue_changes, :through => :issues, :source => :journals
40 40
  has_many :versions, lambda {order("#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC")}, :dependent => :destroy
41 41
  belongs_to :default_version, :class_name => 'Version'
42
  belongs_to :workspace
42 43
  has_many :time_entries, :dependent => :destroy
43 44
  has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all
44 45
  has_many :documents, :dependent => :destroy
......
721 722
    'tracker_ids',
722 723
    'issue_custom_field_ids',
723 724
    'parent_id',
724
    'default_version_id'
725
    'default_version_id',
726
    'workspace_id'
725 727

  
726 728
  safe_attributes 'enabled_module_names',
727 729
    :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
app/models/role.rb
59 59
  before_destroy :check_deletable
60 60
  has_many :workflow_rules, :dependent => :delete_all do
61 61
    def copy(source_role)
62
      WorkflowRule.copy(nil, source_role, nil, proxy_association.owner)
62
      WorkflowRule.copy(nil, source_role, nil, nil, proxy_association.owner, nil)
63 63
    end
64 64
  end
65 65
  has_and_belongs_to_many :custom_fields, :join_table => "#{table_name_prefix}custom_fields_roles#{table_name_suffix}", :foreign_key => "role_id"
app/models/tracker.rb
28 28
  has_many :issues
29 29
  has_many :workflow_rules, :dependent => :delete_all do
30 30
    def copy(source_tracker)
31
      WorkflowRule.copy(source_tracker, nil, proxy_association.owner, nil)
31
      WorkflowRule.copy(source_tracker, nil, nil, proxy_association.owner, nil, nil)
32 32
    end
33 33
  end
34 34

  
app/models/workflow_permission.rb
20 20
  validates_presence_of :old_status
21 21
  validate :validate_field_name
22 22

  
23
  # Returns the workflow permissions for the given trackers and roles
23
  # Returns the workflow permissions for the given trackers, roles and workspaces
24 24
  # grouped by status_id
25 25
  #
26 26
  # Example:
27
  #   WorkflowPermission.rules_by_status_id trackers, roles
27
  #   WorkflowPermission.rules_by_status_id trackers, roles, workspaces
28 28
  #   # => {1 => {'start_date' => 'required', 'due_date' => 'readonly'}}
29
  def self.rules_by_status_id(trackers, roles)
30
    WorkflowPermission.where(:tracker_id => trackers.map(&:id), :role_id => roles.map(&:id)).inject({}) do |h, w|
29
  def self.rules_by_status_id(trackers, roles, workspaces)
30
    WorkflowPermission.where(:tracker_id => trackers.map(&:id), :role_id => roles.map(&:id), :workspace_id => workspaces.map(&:id)).inject({}) do |h, w|
31 31
      h[w.old_status_id] ||= {}
32 32
      h[w.old_status_id][w.field_name] ||= []
33 33
      h[w.old_status_id][w.field_name] << w.rule
......
35 35
    end
36 36
  end
37 37

  
38
  # Replaces the workflow permissions for the given trackers and roles
38
  # Replaces the workflow permissions for the given trackers, roles and workspaces
39 39
  #
40 40
  # Example:
41
  #   WorkflowPermission.replace_permissions trackers, roles, {'1' => {'start_date' => 'required', 'due_date' => 'readonly'}}
42
  def self.replace_permissions(trackers, roles, permissions)
41
  #   WorkflowPermission.replace_permissions trackers, roles, {'1' => {'start_date' => 'required', 'due_date' => 'readonly'}}, workspaces
42
  def self.replace_permissions(trackers, roles, permissions, workspaces)
43 43
    trackers = Array.wrap trackers
44 44
    roles = Array.wrap roles
45
    workspaces = Array.wrap workspaces
45 46

  
46 47
    transaction do
47 48
      permissions.each { |status_id, rule_by_field|
48 49
        rule_by_field.each { |field, rule|
49
          destroy_all(:tracker_id => trackers.map(&:id), :role_id => roles.map(&:id), :old_status_id => status_id, :field_name => field)
50
          destroy_all(:tracker_id => trackers.map(&:id), :role_id => roles.map(&:id), :old_status_id => status_id, :field_name => field, :workspace_id => workspaces.map(&:id))
50 51
          if rule.present?
51 52
            trackers.each do |tracker|
52 53
              roles.each do |role|
53
                WorkflowPermission.create(:role_id => role.id, :tracker_id => tracker.id, :old_status_id => status_id, :field_name => field, :rule => rule)
54
                workspaces.each do |workspace|
55
                  WorkflowPermission.create(:role_id => role.id, :tracker_id => tracker.id, :old_status_id => status_id, :field_name => field, :rule => rule, :workspace_id => workspace.id)
56
                end
54 57
              end
55 58
            end
56 59
          end
app/models/workflow_rule.rb
22 22
  belongs_to :tracker
23 23
  belongs_to :old_status, :class_name => 'IssueStatus'
24 24
  belongs_to :new_status, :class_name => 'IssueStatus'
25
  belongs_to :workspace
25 26

  
26
  validates_presence_of :role, :tracker
27
  validates_presence_of :role, :tracker, :workspace
27 28
  attr_protected :id
28 29

  
29 30
  # Copies workflows from source to targets
30
  def self.copy(source_tracker, source_role, target_trackers, target_roles)
31
    unless source_tracker.is_a?(Tracker) || source_role.is_a?(Role)
32
      raise ArgumentError.new("source_tracker or source_role must be specified")
31
  def self.copy(source_tracker, source_role, source_workspace, target_trackers, target_roles, target_workspaces)
32
    unless source_tracker.is_a?(Tracker) || source_role.is_a?(Role) || source_workspace.is_a?(Workspace)
33
      raise ArgumentError.new("source_tracker, source_role or source_workspace must be specified")
33 34
    end
34 35

  
35 36
    target_trackers = [target_trackers].flatten.compact
36 37
    target_roles = [target_roles].flatten.compact
38
    target_workspaces = [target_workspaces].flatten.compact
37 39

  
38 40
    target_trackers = Tracker.sorted.to_a if target_trackers.empty?
39 41
    target_roles = Role.all.select(&:consider_workflow?) if target_roles.empty?
42
    target_workspaces = Workspace.sorted.to_a if target_workspaces.empty?
40 43

  
41 44
    target_trackers.each do |target_tracker|
42 45
      target_roles.each do |target_role|
43
        copy_one(source_tracker || target_tracker,
44
                   source_role || target_role,
45
                   target_tracker,
46
                   target_role)
46
        target_workspaces.each do |target_workspace|
47
          copy_one(source_tracker || target_tracker,
48
                     source_role || target_role,
49
                     source_workspace || target_workspace,
50
                     target_tracker,
51
                     target_role,
52
                     target_workspace)
53
        end
47 54
      end
48 55
    end
49 56
  end
50 57

  
51 58
  # Copies a single set of workflows from source to target
52
  def self.copy_one(source_tracker, source_role, target_tracker, target_role)
59
  def self.copy_one(source_tracker, source_role, source_workspace, target_tracker, target_role, target_workspace)
53 60
    unless source_tracker.is_a?(Tracker) && !source_tracker.new_record? &&
54 61
      source_role.is_a?(Role) && !source_role.new_record? &&
62
      source_workspace.is_a?(Workspace) && !source_workspace.new_record? &&
55 63
      target_tracker.is_a?(Tracker) && !target_tracker.new_record? &&
56 64
      target_role.is_a?(Role) && !target_role.new_record?
65
      target_workspace.is_a?(Workspace) && !target_workspace.new_record?
57 66

  
58 67
      raise ArgumentError.new("arguments can not be nil or unsaved objects")
59 68
    end
60 69

  
61
    if source_tracker == target_tracker && source_role == target_role
70
    if source_tracker == target_tracker && source_role == target_role && source_workspace == target_workspace
62 71
      false
63 72
    else
64 73
      transaction do
65
        delete_all :tracker_id => target_tracker.id, :role_id => target_role.id
66
        connection.insert "INSERT INTO #{WorkflowRule.table_name} (tracker_id, role_id, old_status_id, new_status_id, author, assignee, field_name, #{connection.quote_column_name 'rule'}, type)" +
67
                          " SELECT #{target_tracker.id}, #{target_role.id}, old_status_id, new_status_id, author, assignee, field_name, #{connection.quote_column_name 'rule'}, type" +
74
        delete_all :tracker_id => target_tracker.id, :role_id => target_role.id, :workspace_id => target_workspace.id
75
        connection.insert "INSERT INTO #{WorkflowRule.table_name} (tracker_id, role_id, old_status_id, new_status_id, author, assignee, field_name, #{connection.quote_column_name 'rule'}, type, workspace_id)" +
76
                          " SELECT #{target_tracker.id}, #{target_role.id}, old_status_id, new_status_id, author, assignee, field_name, #{connection.quote_column_name 'rule'}, type, #{target_workspace.id}" +
68 77
                          " FROM #{WorkflowRule.table_name}" +
69
                          " WHERE tracker_id = #{source_tracker.id} AND role_id = #{source_role.id}"
78
                          " WHERE tracker_id = #{source_tracker.id} AND role_id = #{source_role.id} AND workspace_id = #{source_workspace.id}"
70 79
      end
71 80
      true
72 81
    end
app/models/workflow_transition.rb
18 18
class WorkflowTransition < WorkflowRule
19 19
  validates_presence_of :new_status
20 20

  
21
  def self.replace_transitions(trackers, roles, transitions)
21
  def self.replace_transitions(trackers, roles, transitions, workspaces)
22 22
    trackers = Array.wrap trackers
23 23
    roles = Array.wrap roles
24
    workspaces = Array.wrap workspaces
24 25

  
25 26
    transaction do
26
      records = WorkflowTransition.where(:tracker_id => trackers.map(&:id), :role_id => roles.map(&:id)).to_a
27
      records = WorkflowTransition.where(:tracker_id => trackers.map(&:id), :role_id => roles.map(&:id), :workspace_id => workspaces.map(&:id)).to_a
27 28

  
28 29
      transitions.each do |old_status_id, transitions_by_new_status|
29 30
        transitions_by_new_status.each do |new_status_id, transition_by_rule|
30 31
          transition_by_rule.each do |rule, transition|
31 32
            trackers.each do |tracker|
32 33
              roles.each do |role|
33
                w = records.select {|r|
34
                  r.old_status_id == old_status_id.to_i &&
35
                  r.new_status_id == new_status_id.to_i &&
36
                  r.tracker_id == tracker.id &&
37
                  r.role_id == role.id &&
38
                  !r.destroyed?
39
                }
34
                workspaces.each do |workspace|
35
                  w = records.select {|r|
36
                    r.old_status_id == old_status_id.to_i &&
37
                    r.new_status_id == new_status_id.to_i &&
38
                    r.tracker_id == tracker.id &&
39
                    r.role_id == role.id &&
40
                    r.workspace_id == workspace.id &&
41
                    !r.destroyed?
42
                  }
40 43

  
41
                if rule == 'always'
42
                  w = w.select {|r| !r.author && !r.assignee}
43
                else
44
                  w = w.select {|r| r.author || r.assignee}
45
                end
46
                if w.size > 1
47
                  w[1..-1].each(&:destroy)
48
                end
49
                w = w.first
50

  
51
                if transition == "1" || transition == true
52
                  unless w
53
                    w = WorkflowTransition.new(:old_status_id => old_status_id, :new_status_id => new_status_id, :tracker_id => tracker.id, :role_id => role.id)
54
                    records << w
55
                  end
56
                  w.author = true if rule == "author"
57
                  w.assignee = true if rule == "assignee"
58
                  w.save if w.changed?
59
                elsif w
60 44
                  if rule == 'always'
61
                    w.destroy
62
                  elsif rule == 'author'
63
                    if w.assignee
64
                      w.author = false
65
                      w.save if w.changed?
66
                    else
67
                      w.destroy
45
                    w = w.select {|r| !r.author && !r.assignee}
46
                  else
47
                    w = w.select {|r| r.author || r.assignee}
48
                  end
49
                  if w.size > 1
50
                    w[1..-1].each(&:destroy)
51
                  end
52
                  w = w.first
53

  
54
                  if transition == "1" || transition == true
55
                    unless w
56
                      w = WorkflowTransition.new(:old_status_id => old_status_id, :new_status_id => new_status_id, :tracker_id => tracker.id, :role_id => role.id, :workspace_id => workspace.id)
57
                      records << w
68 58
                    end
69
                  elsif rule == 'assignee'
70
                    if w.author
71
                      w.assignee = false
72
                      w.save if w.changed?
73
                    else
59
                    w.author = true if rule == "author"
60
                    w.assignee = true if rule == "assignee"
61
                    w.save if w.changed?
62
                  elsif w
63
                    if rule == 'always'
74 64
                      w.destroy
65
                    elsif rule == 'author'
66
                      if w.assignee
67
                        w.author = false
68
                        w.save if w.changed?
69
                      else
70
                        w.destroy
71
                      end
72
                    elsif rule == 'assignee'
73
                      if w.author
74
                        w.assignee = false
75
                        w.save if w.changed?
76
                      else
77
                        w.destroy
78
                      end
75 79
                    end
76 80
                  end
77 81
                end
app/models/workspace.rb
1
# Redmine - project management software
2
# Copyright (C) 2006-2015  Jean-Philippe Lang
3
#
4
# This program is free software; you can redistribute it and/or
5
# modify it under the terms of the GNU General Public License
6
# as published by the Free Software Foundation; either version 2
7
# of the License, or (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17

  
18
class Workspace < ActiveRecord::Base
19

  
20
  before_destroy :check_integrity
21
  has_many :projects
22
  has_many :workflow_rules, :dependent => :delete_all do
23
    def copy(source_workflow)
24
      WorkflowRule.copy(nil, nil, source_tracker, nil, nil, proxy_association.owner)
25
    end
26
  end
27
  acts_as_list
28

  
29
  validates_presence_of :name
30
  validates_uniqueness_of :name
31
  validates_length_of :name, :maximum => 30
32
  attr_protected :id
33

  
34
  scope :sorted, lambda { order(:position) }
35
  scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
36

  
37
  # Returns an array of IssueStatus that are used
38
  # in the tracker's workflows
39
  def issue_statuses
40
    if @issue_statuses
41
      return @issue_statuses
42
    elsif new_record?
43
      return []
44
    end
45

  
46
    ids = WorkflowTransition.
47
      connection.select_rows("SELECT DISTINCT old_status_id, new_status_id FROM #{WorkflowTransition.table_name} WHERE workspace_id = #{id} AND type = 'WorkflowTransition'").
48
      flatten.
49
      uniq
50
    @issue_statuses = IssueStatus.where(:id => ids).all.sort
51
  end
52

  
53
  def <=>(status)
54
    position <=> status.position
55
  end
56

  
57
  def to_s; name end
58

  
59
private
60
  def check_integrity
61
    raise Exception.new("Cannot delete workspace") if Project.where(:workspace_id => self.id).any?
62
  end
63
end
app/views/admin/projects.html.erb
22 22
  <th><%=l(:label_project)%></th>
23 23
  <th><%=l(:field_is_public)%></th>
24 24
  <th><%=l(:field_created_on)%></th>
25
  <th><%=l(:field_workspace)%></th>
25 26
  <th></th>
26 27
  </tr></thead>
27 28
  <tbody>
......
30 31
  <td class="name"><span><%= link_to_project_settings(project, {}, :title => project.short_description) %></span></td>
31 32
  <td><%= checked_image project.is_public? %></td>
32 33
  <td><%= format_date(project.created_on) %></td>
34
  <td><%= @workspaces[project.workspace_id] %></td>
33 35
  <td class="buttons">
34 36
    <%= link_to(l(:button_archive), archive_project_path(project, :status => params[:status]), :data => {:confirm => l(:text_are_you_sure)}, :method => :post, :class => 'icon icon-lock') unless project.archived? %>
35 37
    <%= link_to(l(:button_unarchive), unarchive_project_path(project, :status => params[:status]), :method => :post, :class => 'icon icon-unlock') if project.archived? && (project.parent.nil? || !project.parent.archived?) %>
app/views/members/_new_form.html.erb
7 7
  </div>
8 8
</fieldset>
9 9
<fieldset class="box">
10
  <legend><%= l(:label_role_plural) %> <%= toggle_checkboxes_link('.roles-selection input') %></legend>
10
  <legend><%= l(:label_role_plural) %> (<a onclick='$(".unused").toggleClass("show");'><%= l(:label_all) %></a>) <%= toggle_checkboxes_link('.roles-selection input') %></legend>
11 11
  <div class="roles-selection">
12 12
    <% User.current.managed_roles(@project).each do |role| %>
13
      <label><%= check_box_tag 'membership[role_ids][]', role.id, false, :id => nil %> <%= role %></label>
13
      <label<%= " class=unused" unless WorkflowTransition.where(:role_id => role.id, :workspace_id => @project.workspace_id).any? || ! role.assignable %>><%= check_box_tag 'membership[role_ids][]', role.id, false, :id => nil %> <%= role %></label>
14 14
    <% end %>
15 15
  </div>
16 16
</fieldset>
app/views/projects/_form.html.erb
16 16
    <p><%= label(:project, :parent_id, l(:field_parent)) %><%= parent_project_select_tag(@project) %></p>
17 17
<% end %>
18 18

  
19
<% if @project.safe_attribute? 'workspace_id' %>
20
<p><%= f.select :workspace_id, project_workspace_options(@project), {}, {:onChange=>"displaytrackers(this);"} %></p>
21
<% end %>
22

  
19 23
<% if @project.safe_attribute? 'inherit_members' %>
20 24
<p><%= f.check_box :inherit_members %></p>
21 25
<% end %>
......
46 50

  
47 51
<% if @project.new_record? || @project.module_enabled?('issue_tracking') %>
48 52
<% unless @trackers.empty? %>
49
<fieldset class="box tabular" id="project_trackers"><legend><%=l(:label_tracker_plural)%></legend>
53
<fieldset class="box tabular" id="project_trackers"><legend><%=l(:label_tracker_plural)%> (<a onclick="$('#project_trackers').toggleClass('showall'); displaytrackers(document.getElementById('project_workspace_id'));"><%= l(:label_all) %></a>)</legend>
50 54
<% @trackers.each do |tracker| %>
51
    <label class="floating">
55
    <label class="floating <%= used_workspaces_by_tracker(tracker.id) %>">
52 56
    <%= check_box_tag 'project[tracker_ids][]', tracker.id, @project.trackers.to_a.include?(tracker), :id => nil %>
53 57
    <%= tracker %>
54 58
    </label>
......
104 108
  }).trigger('change');
105 109
});
106 110
<% end %>
111

  
112
<script>
113
  function displaytrackers(sel) {
114
    $('#project_trackers').children('label').each( function() {
115
      if ($(this).hasClass('ws-' + sel.value) || $('#project_trackers').hasClass('showall'))
116
      {
117
        $(this).show();
118
      } else {
119
        $(this).hide();
120
      }
121
    });
122
  };
123
  displaytrackers(document.getElementById('project_workspace_id'));
124
</script>
app/views/projects/settings/_members.html.erb
8 8
  <thead>
9 9
    <tr>
10 10
      <th><%= l(:label_user) %> / <%= l(:label_group) %></th>
11
      <th><%= l(:label_role_plural) %></th>
11
      <th><%= l(:label_role_plural) %> (<a onclick='$(".unused").toggle();'><%= l(:label_all) %></a>)</th>
12 12
      <th style="width:15%"></th>
13 13
      <%= call_hook(:view_projects_settings_members_table_header, :project => @project) %>
14 14
    </tr>
......
28 28
             ) do |f| %>
29 29
        <p>
30 30
          <% roles.each do |role| %>
31
          <label>
32
            <%= check_box_tag('membership[role_ids][]',
33
                              role.id, member.roles.include?(role),
34
                              :id => nil,
35
                              :disabled => !member.role_editable?(role)) %> <%= role %>
36
          </label><br />
31
          <div<%= " class=unused" unless WorkflowTransition.where(:role_id => role.id, :workspace_id => @project.workspace_id).any? || ! role.assignable %>> 
32
            <label>
33
              <%= check_box_tag('membership[role_ids][]',
34
                                role.id, member.roles.include?(role),
35
                                :id => nil,
36
                                :disabled => !member.role_editable?(role)) %> <%= role %>
37
            </label><br />
38
          </div>
37 39
          <% end %>
38 40
        </p>
39 41
        <%= hidden_field_tag 'membership[role_ids][]', '', :id => nil %>
app/views/workflows/copy.html.erb
17 17
                  content_tag('option', "--- #{ l(:label_copy_same_as_target) } ---", :value => 'any') +
18 18
                  options_from_collection_for_select(@roles, 'id', 'name', @source_role && @source_role.id)) %>
19 19
</p>
20
<p>
21
  <label><%= l(:label_workspace) %></label>
22
  <%= select_tag('source_workspace_id',
23
                  content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '') +
24
                  content_tag('option', "--- #{ l(:label_copy_same_as_target) } ---", :value => 'any') +
25
                  options_from_collection_for_select(@workspaces, 'id', 'name', @source_workspace && @source_workspace.id)) %>
26
</p>
20 27
</fieldset>
21 28

  
22 29
<fieldset class="tabular box">
......
33 40
                  content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '', :disabled => true) +
34 41
                  options_from_collection_for_select(@roles, 'id', 'name', @target_roles && @target_roles.map(&:id)), :multiple => true %>
35 42
</p>
43
<p>
44
  <label><%= l(:label_workspace) %></label>
45
  <%= select_tag 'target_workspace_ids',
46
                  content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '', :disabled => true) +
47
                  options_from_collection_for_select(@workspaces, 'id', 'name', @target_workspaces && @target_workspaces.map(&:id)), :multiple => true %>
48
</p>
36 49
</fieldset>
37 50
<%= submit_tag l(:button_copy) %>
38 51
<% end %>
app/views/workflows/edit.html.erb
4 4

  
5 5
<div class="tabs">
6 6
  <ul>
7
    <li><%= link_to l(:label_status_transitions), workflows_edit_path(:role_id => @roles, :tracker_id => @trackers), :class => 'selected' %></li>
8
    <li><%= link_to l(:label_fields_permissions), workflows_permissions_path(:role_id => @roles, :tracker_id => @trackers) %></li>
7
    <li><%= link_to l(:label_status_transitions), workflows_edit_path(:role_id => @roles, :tracker_id => @trackers, :workspace_id => @workspaces), :class => 'selected' %></li>
8
    <li><%= link_to l(:label_fields_permissions), workflows_permissions_path(:role_id => @roles, :tracker_id => @trackers, :workspace_id => @workspaces) %></li>
9 9
  </ul>
10 10
</div>
11 11

  
......
23 23
  </label>
24 24
  <a href="#" data-expands="#tracker_id"><span class="toggle-multiselect"></span></a>
25 25

  
26
  <label><%=l(:label_workspace)%>:
27
  <%= options_for_workflow_select 'workspace_id[]', Workspace.sorted, @workspaces, :id => 'workspace_id', :class => 'expandable' %>
28
  </label>
29
  <a href="#" data-expands="#workspace_id"><span class="toggle-multiselect"></span></a>
30

  
26 31
  <%= submit_tag l(:button_edit), :name => nil %>
27 32

  
28 33
  <%= hidden_field_tag 'used_statuses_only', '0', :id => nil %>
......
31 36
</p>
32 37
<% end %>
33 38

  
34
<% if @trackers && @roles && @statuses.any? %>
39
<% if @trackers && @roles && @workspaces && @statuses.any? %>
35 40
  <%= form_tag({}, :id => 'workflow_form' ) do %>
36 41
    <%= @trackers.map {|tracker| hidden_field_tag 'tracker_id[]', tracker.id, :id => nil}.join.html_safe %>
37 42
    <%= @roles.map {|role| hidden_field_tag 'role_id[]', role.id, :id => nil}.join.html_safe %>
43
    <%= @workspaces.map {|workspace| hidden_field_tag 'workspace_id[]', workspace.id, :id => nil}.join.html_safe %>
38 44
    <%= hidden_field_tag 'used_statuses_only', params[:used_statuses_only], :id => nil %>
39 45
    <div class="autoscroll">
40 46
      <%= render :partial => 'form', :locals => {:name => 'always', :workflows => @workflows['always']} %>
app/views/workflows/index.html.erb
3 3
<% if @roles.empty? || @trackers.empty? %>
4 4
<p class="nodata"><%= l(:label_no_data) %></p>
5 5
<% else %>
6

  
7
<p><label><%=l(:label_workspace)%>:
8
<%= options_for_workflow_select 'workspace_id[]', Workspace.sorted, @workspaces, :id => 'workspace_id', :class => 'expandable', :onchange=>'refresh_table(this.value);' %>
9
</label></p>
10

  
6 11
<div class="autoscroll">
7 12
<table class="list">
8 13
<thead>
......
20 25
<tr class="<%= cycle('odd', 'even') %>">
21 26
  <td class="name"><%= tracker.name %></td>
22 27
  <% @roles.each do |role| -%>
23
  <% count = @workflow_counts[[tracker.id, role.id]] || 0 %>
24 28
    <td>
25
      <%= link_to((count > 0 ? count : content_tag(:span, nil, :class => 'icon-only icon-not-ok')),
26
                  {:action => 'edit', :role_id => role, :tracker_id => tracker},
29
      <% countall = 0 %>
30
      <% @workspaces.each do |workspace| %>
31
        <% count = @workflow_counts[[tracker.id, role.id, workspace.id]] || 0 %>
32
        <% countall += count %>
33
        <span class="ws ws<%= workspace.id%>" style="display: none;">
34
          <%= link_to((count > 0 ? count : content_tag(:span, nil, :class => 'icon-only icon-not-ok')),
35
                  {:action => 'edit', :role_id => role, :tracker_id => tracker, :workspace_id => workspace},
27 36
                  :title => l(:button_edit)) %>
37
        </span>
38
      <% end %>
39
      <span class="ws wsall" style="display: none;">
40
        <%= link_to((countall > 0 ? countall : content_tag(:span, nil, :class => 'icon-only icon-not-ok')),
41
                    {:action => 'edit', :role_id => role, :tracker_id => tracker, :workspace_id => 'all'},
42
                    :title => l(:button_edit)) %>
43
      </span>
28 44
    </td>
29 45
  <% end -%>
30 46
</tr>
......
32 48
</tbody>
33 49
</table>
34 50
</div>
51
<script>
52
  function refresh_table(value) {
53
    $('.ws').css("display","none");
54
    $('.ws' + value).css("display","inline");
55
  }
56
  refresh_table(document.getElementById('workspace_id').value);
57
</script>
35 58
<% end %>
app/views/workflows/permissions.html.erb
4 4

  
5 5
<div class="tabs">
6 6
  <ul>
7
    <li><%= link_to l(:label_status_transitions), workflows_edit_path(:role_id => @roles, :tracker_id => @trackers) %></li>
8
    <li><%= link_to l(:label_fields_permissions), workflows_permissions_path(:role_id => @roles, :tracker_id => @trackers), :class => 'selected' %></li>
7
    <li><%= link_to l(:label_status_transitions), workflows_edit_path(:role_id => @roles, :tracker_id => @trackers, :workspace_id => @workspaces) %></li>
8
    <li><%= link_to l(:label_fields_permissions), workflows_permissions_path(:role_id => @roles, :tracker_id => @trackers, :workspace_id => @workspaces), :class => 'selected' %></li>
9 9
  </ul>
10 10
</div>
11 11

  
......
23 23
  </label>
24 24
  <a href="#" data-expands="#tracker_id"><span class="toggle-multiselect"></a>
25 25

  
26
  <label><%=l(:label_workspace)%>:
27
  <%= options_for_workflow_select 'workspace_id[]', Workspace.sorted, @workspaces, :id => 'workspace_id', :class => 'expandable' %>
28
  </label>
29
  <a href="#" data-expands="#workspace_id"><%= image_tag 'bullet_toggle_plus.png' %></a>
30

  
26 31
  <%= submit_tag l(:button_edit), :name => nil %>
27 32

  
28 33
  <%= hidden_field_tag 'used_statuses_only', '0', :id => nil %>
......
30 35
</p>
31 36
<% end %>
32 37

  
33
<% if @trackers && @roles && @statuses.any? %>
38
<% if @trackers && @roles && @workspaces && @statuses.any? %>
34 39
  <%= form_tag({}, :id => 'workflow_form' ) do %>
35 40
    <%= @trackers.map {|tracker| hidden_field_tag 'tracker_id[]', tracker.id, :id => nil}.join.html_safe %>
36 41
    <%= @roles.map {|role| hidden_field_tag 'role_id[]', role.id, :id => nil}.join.html_safe %>
42
    <%= @workspaces.map {|workspace| hidden_field_tag 'workspace_id[]', workspace.id, :id => nil}.join.html_safe %>
37 43
    <%= hidden_field_tag 'used_statuses_only', params[:used_statuses_only], :id => nil %>
38 44
    <div class="autoscroll">
39 45
    <table class="list workflows fields_permissions">
app/views/workspaces/_form.html.erb
1
<%= error_messages_for 'workspace' %>
2

  
3
<div class="box tabular">
4
<p><%= f.text_field :name, :required => true %></p>
5
<p><%= f.text_field :description %></p>
6

  
7
<%= call_hook(:view_workspaces_form, :workspace => @workspace) %>
8
</div>
app/views/workspaces/edit.html.erb
1
<%= title [l(:label_workspace_plural), workspaces_path], @workspace.name %>
2

  
3
<%= labelled_form_for @workspace do |f| %>
4
  <%= render :partial => 'form', :locals => {:f => f} %>
5
  <%= submit_tag l(:button_save) %>
6
<% end %>
app/views/workspaces/index.api.rsb
1
api.array :workspaces do
2
  @workspaces.each do |workspace|
3
    api.workspace do
4
      api.id status.id
5
      api.name status.name
6
      api.description status.description
7
    end
8
  end
9
end
app/views/workspaces/index.html.erb
1
<div class="contextual">
2
<%= link_to l(:label_workspace_new), new_workspace_path, :class => 'icon icon-add' %>
3
</div>
4

  
5
<h2><%=l(:label_workspace_plural)%></h2>
6

  
7
<table class="list">
8
  <thead><tr>
9
  <th><%=l(:field_name)%></th>
10
  <th><%=l(:field_description)%></th>
11
  <th><%=l(:button_sort)%></th>
12
  <th></th>
13
  </tr></thead>
14
  <tbody>
15
<% for status in @workspaces %>
16
  <tr class="<%= cycle("odd", "even") %>">
17
  <td class="name"><%= link_to status.name, edit_workspace_path(status) %></td>
18
  <td class="description"><%= link_to status.description, edit_workspace_path(status) %></td>
19
  <td class="reorder"><%= reorder_links('workspace', {:action => 'update', :id => status, :page => params[:page]}, :put) %></td>
20
  <td class="buttons">
21
    <%= delete_link workspace_path(status) unless status.id == 1 %>
22
  </td>
23
  </tr>
24
<% end %>
25
  </tbody>
26
</table>
27

  
28
<span class="pagination"><%= pagination_links_full @workspace_pages %></span>
29

  
30
<% html_title(l(:label_workspace_plural)) -%>
app/views/workspaces/new.html.erb
1
<%= title [l(:label_workspace_plural), workspaces_path], l(:label_workspace_new) %>
2

  
3
<%= labelled_form_for @workspace do |f| %>
4
  <%= render :partial => 'form', :locals => {:f => f} %>
5
  <%= submit_tag l(:button_create) %>
6
<% end %>
config/locales/en.yml
566 566
  label_tracker_plural: Trackers
567 567
  label_tracker_all: All trackers
568 568
  label_tracker_new: New tracker
569
  label_workspace: Workspace
570
  label_workspace_plural: Workspaces
571
  label_workspace_new: New workspace
572
  error_unable_delete_workspace: Unable to delete workspace
573
  field_workspace: Workspace
569 574
  label_workflow: Workflow
570 575
  label_issue_status: Issue status
571 576
  label_issue_status_plural: Issue statuses
config/locales/pt-BR.yml
375 375
  label_tracker: Tipo de tarefa
376 376
  label_tracker_plural: Tipos de tarefas
377 377
  label_tracker_new: Novo tipo
378
  label_workspace: Espaço de trabalho
379
  label_workspace_plural: Espaços de trabalho
380
  label_workspace_new: Novo espaço de trabalho
381
  error_unable_delete_workspace: Não foi possível excluir espaço de trabalho
382
  field_workspace: Espaço de trabalho
378 383
  label_workflow: Fluxo de trabalho
379 384
  label_issue_status: Situação da tarefa
380 385
  label_issue_status_plural: Situação das tarefas
config/routes.rb
310 310
      match 'fields', :via => [:get, :post]
311 311
    end
312 312
  end
313
  resources :workspaces, :except => :show
313 314
  resources :issue_statuses, :except => :show do
314 315
    collection do
315 316
      post 'update_issue_done_ratio'
db/migrate/20160314174310_add_workspace_to_projects.rb
1
class AddWorkspaceToProjects < ActiveRecord::Migration
2
  def self.up
3
    unless ActiveRecord::Base.connection.column_exists?(:projects, :workspace_id)
4
      add_column :projects, :workspace_id, :integer, :default => 1
5
    end
6
  end
7
end
db/migrate/20160314174311_add_workspace_to_workflows.rb
1
class AddWorkspaceToWorkflows < ActiveRecord::Migration
2
  def self.up
3
    unless ActiveRecord::Base.connection.column_exists?(:workflows, :workspace_id)
4
      add_column :workflows, :workspace_id, :integer, :default => 1
5
    end
6
  end
7
end
db/migrate/20160314174312_create_workspaces.rb
1
class CreateWorkspaces < ActiveRecord::Migration
2
  class Workspace < ActiveRecord::Base; end
3

  
4
  def self.up
5
    unless ActiveRecord::Base.connection.table_exists?('workspaces')
6
      create_table :workspaces do |t|
7
        t.string :name
8
        t.string :description
9
        t.integer :position
10
      end
11

  
12
      # create default workspace
13
      workspace = Workspace.new :name => "Default",
14
                                :description => "Default workspace",
15
                                :position => "1"
16
      workspace.save
17
    end
18
  end
19
end
db/migrate/20161221111437_fix_workspaces_allow_null_position.rb
1
class FixWorkspacesAllowNullPosition < ActiveRecord::Migration
2
  def self.up
3
    # removes the 'not null' constraint on position fields
4
    change_column :workspaces, :position, :integer, :default => 1, :null => true
5
  end
6

  
7
  def self.down
8
    # nothing to do
9
  end
10
end
lib/redmine.rb
214 214
  menu.push :trackers, {:controller => 'trackers'}, :caption => :label_tracker_plural
215 215
  menu.push :issue_statuses, {:controller => 'issue_statuses'}, :caption => :label_issue_status_plural,
216 216
            :html => {:class => 'issue_statuses'}
217
  menu.push :workspaces, {:controller => 'workspaces'}, :caption => :label_workspace_plural
217 218
  menu.push :workflows, {:controller => 'workflows', :action => 'edit'}, :caption => :label_workflow
218 219
  menu.push :custom_fields, {:controller => 'custom_fields'},  :caption => :label_custom_field_plural,
219 220
            :html => {:class => 'custom_fields'}
public/stylesheets/application.css
85 85
#admin-menu a.roles { background-image: url(../images/database_key.png); }
86 86
#admin-menu a.trackers { background-image: url(../images/ticket.png); }
87 87
#admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); }
88
#admin-menu a.workspaces { background-image: url(../images/table_multiple.png); }
88 89
#admin-menu a.workflows { background-image: url(../images/ticket_go.png); }
89 90
#admin-menu a.custom_fields { background-image: url(../images/textfield.png); }
90 91
#admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); }
......
719 720

  
720 721
input#principal_search, input#user_search {width:90%}
721 722
.roles-selection label {display:inline-block; width:210px;}
723
.roles-selection .unused {display:none;}
724
.roles-selection .unused.show {display:inline-block;}
725
.unused {display:none;}
722 726

  
723 727
input.autocomplete {
724 728
  background: #fff url(../images/magnifier.png) no-repeat 2px 50%; padding-left:20px !important;
(1-1/5)