Move all custom actions into an automations namespace

This commit is contained in:
Oliver Günther
2026-05-11 21:27:00 +02:00
parent 35f4555683
commit 3b35a7d98c
122 changed files with 1148 additions and 1403 deletions
@@ -0,0 +1,30 @@
# frozen_string_literal: true
module Automations
class IndexComponent < ::TableComponent
columns :name, :triggers, :conditions, :actions, :sort
def headers
[
["name", { caption: Automation.human_attribute_name(:name) }],
["triggers", { caption: I18n.t("automations.triggers.name") }],
["conditions", { caption: I18n.t("automations.conditions") }],
["actions", { caption: I18n.t("automations.actions.name") }],
["sort", { caption: I18n.t(:label_sort) }]
]
end
def sortable?
false
end
def inline_create_link
link_to new_automation_path,
aria: { label: t("automations.new") },
class: "wp-inline-create--add-link",
title: t("automations.new") do
helpers.op_icon("icon icon-add")
end
end
end
end
@@ -0,0 +1,63 @@
# frozen_string_literal: true
module Automations
class RowComponent < ::RowComponent
def automation
row
end
def name
link_to automation.name, edit_automation_path(automation)
end
def triggers
automation.triggers.map do |trigger|
case trigger
when Automations::Triggers::Manual
I18n.t("automations.triggers.manual.label")
else
trigger.type.demodulize
end
end.join(", ")
end
def conditions
automation.conditions.map(&:human_name).join(", ")
end
def actions
automation.actions.map(&:human_name).join(", ")
end
def sort
helpers.reorder_links("automation", { action: "update", id: automation }, method: :put)
end
def button_links
[
edit_link,
delete_link
]
end
def edit_link
link_to(
helpers.op_icon("icon icon-edit"),
helpers.edit_automation_path(automation),
title: t(:button_edit)
)
end
def delete_link
link_to(
helpers.op_icon("icon icon-delete"),
helpers.automation_path(automation),
data: {
turbo_method: :delete,
turbo_confirm: I18n.t(:text_are_you_sure)
},
title: t(:button_delete)
)
end
end
end
@@ -1,74 +0,0 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module CustomActions
class RowComponent < ::RowComponent
def action
row
end
def name
link_to action.name, edit_custom_action_path(action)
end
delegate :description, to: :action
def sort
helpers.reorder_links("custom_action", { action: "update", id: action }, method: :put)
end
def button_links
[
edit_link,
delete_link
]
end
def edit_link
link_to(
helpers.op_icon("icon icon-edit"),
helpers.edit_custom_action_path(action),
title: t(:button_edit)
)
end
def delete_link
link_to(
helpers.op_icon("icon icon-delete"),
helpers.custom_action_path(action),
data: {
turbo_method: :delete,
turbo_confirm: I18n.t(:text_are_you_sure)
},
title: t(:button_delete)
)
end
end
end
@@ -1,56 +0,0 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module CustomActions
class TableComponent < ::TableComponent
columns :name, :description, :sort
def headers
[
["name", { caption: CustomAction.human_attribute_name(:name) }],
["description", { caption: CustomAction.human_attribute_name(:description) }],
["sort", { caption: I18n.t(:label_sort) }]
]
end
def sortable?
false
end
def inline_create_link
link_to new_custom_action_path,
aria: { label: t("custom_actions.new") },
class: "wp-inline-create--add-link",
title: t("custom_actions.new") do
helpers.op_icon("icon icon-add")
end
end
end
end
+23
View File
@@ -0,0 +1,23 @@
# frozen_string_literal: true
require "model_contract"
module Automations
class CuContract < ::ModelContract
def self.model
Automation
end
attribute :name
attribute :description
attribute :actions do
errors.add(:actions, :empty) if model.actions.empty?
model.actions.each { |action| action.validate(errors) }
end
attribute :conditions do
model.conditions.each { |condition| condition.validate(errors) }
end
end
end
@@ -0,0 +1,30 @@
# frozen_string_literal: true
module Automations
class ExecuteContract < BaseContract
property :lock_version
property :work_package_id
validates :work_package_id, presence: true
validate :work_package_visible
validate :automation_conditions_fulfilled
private
def work_package_visible
return unless model.work_package_id
errors.add(:work_package_id, :does_not_exist) unless WorkPackage.visible(user).where(id: model.work_package_id).exists?
end
def automation_conditions_fulfilled
return unless model.work_package_id
return unless options[:automation]
work_package = WorkPackage.visible(user).find_by(id: model.work_package_id)
return unless work_package
errors.add(:base, :error_unauthorized) unless options[:automation].conditions_fulfilled?(work_package, user)
end
end
end
@@ -1,62 +0,0 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
require "model_contract"
# Contract for create (c) and update (u)
module CustomActions
class CuContract < ::ModelContract
def self.model
CustomAction
end
def initialize(model, user = nil)
super
end
attribute :name
attribute :description
attribute :actions do
if model.actions.empty?
errors.add :actions, :empty
end
model.actions.each do |action|
action.validate(errors)
end
end
attribute :conditions do
model.conditions.each do |condition|
condition.validate(errors)
end
end
end
end
@@ -1,62 +0,0 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module CustomActions
class ExecuteContract < BaseContract
property :lock_version
property :work_package_id
validates :work_package_id, presence: true
validate :work_package_visible
validate :custom_action_conditions_fulfilled
private
def work_package_visible
return unless model.work_package_id
unless WorkPackage.visible(user).where(id: model.work_package_id).exists?
errors.add(:work_package_id, :does_not_exist)
end
end
def custom_action_conditions_fulfilled
return unless model.work_package_id
return unless options[:custom_action]
work_package = WorkPackage.visible(user).find_by(id: model.work_package_id)
return unless work_package
unless options[:custom_action].conditions_fulfilled?(work_package, user)
errors.add(:base, :error_unauthorized)
end
end
end
end
+72
View File
@@ -0,0 +1,72 @@
# frozen_string_literal: true
class AutomationsController < ApplicationController
before_action :require_admin
guard_enterprise_feature(:custom_actions, only: %i[new create edit update]) do
redirect_to action: :index
end
before_action :find_automation, only: %i[edit update destroy]
before_action :pad_params, only: %i[create update]
layout "admin"
def index
@automations = Automation.order_by_position.includes(:triggers)
end
def new
@automation = Automation.new
@automation.triggers.build(type: "Automations::Triggers::Manual")
end
def edit; end
def create
Automations::CreateService
.new(user: current_user)
.call(attributes: permitted_params.automation.to_h,
&index_or_render(:new))
end
def update
Automations::UpdateService
.new(action: @automation, user: current_user)
.call(attributes: permitted_params.automation.to_h,
&index_or_render(:edit))
end
def destroy
@automation.destroy
redirect_to automations_path, status: :see_other
end
private
def find_automation
@automation = Automation.find(params[:id])
end
def index_or_render(render_action)
->(call) {
call.on_success do
redirect_to automations_path, status: :see_other
end
call.on_failure do
@automation = call.result
render action: render_action, status: :unprocessable_entity
end
}
end
def pad_params
return if !params[:automation] || params[:automation][:move_to]
params[:automation][:conditions] ||= {}
params[:automation][:actions] ||= {}
params[:automation][:triggers_attributes] ||= [{ type: "Automations::Triggers::Manual", options: { button_label: params[:automation][:name] } }]
end
end
@@ -1,102 +0,0 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
class CustomActionsController < ApplicationController
before_action :require_admin
guard_enterprise_feature(:custom_actions, only: %i[new create edit update]) do
redirect_to action: :index
end
before_action :find_custom_action, only: %i(edit update destroy)
before_action :pad_params, only: %i(create update)
layout "admin"
def index
@custom_actions = CustomAction.order_by_position
end
def new
@custom_action = CustomAction.new
end
def edit; end
def create
CustomActions::CreateService
.new(user: current_user)
.call(attributes: permitted_params.custom_action.to_h,
&index_or_render(:new))
end
def update
CustomActions::UpdateService
.new(action: @custom_action, user: current_user)
.call(attributes: permitted_params.custom_action.to_h,
&index_or_render(:edit))
end
def destroy
@custom_action.destroy
redirect_to custom_actions_path, status: :see_other
end
private
def find_custom_action
@custom_action = CustomAction.find(params[:id])
end
def index_or_render(render_action)
->(call) {
call.on_success do
redirect_to custom_actions_path, status: :see_other
end
call.on_failure do
@custom_action = call.result
render action: render_action, status: :unprocessable_entity
end
}
end
# If no action/condition is set in the view, the
# actions/conditions already existing on a custom action should be removed.
# But because it is not feasible to have an empty and hidden hash object in a form
# we have to pad the params here.
def pad_params
return if !params[:custom_action] || params[:custom_action][:move_to]
params[:custom_action][:conditions] ||= {}
params[:custom_action][:actions] ||= {}
end
end
@@ -1,40 +1,24 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
class CustomAction < ApplicationRecord
class Automation < ApplicationRecord
validates :name, length: { maximum: 255, minimum: 1 }
serialize :actions, coder: CustomActions::Actions::Serializer
has_and_belongs_to_many :status_conditions, class_name: "Status"
has_and_belongs_to_many :role_conditions, class_name: "Role"
has_and_belongs_to_many :type_conditions, class_name: "Type"
has_and_belongs_to_many :project_conditions, class_name: "Project"
validate :must_have_at_least_one_trigger
validate :must_not_have_more_than_one_manual_trigger
serialize :actions, coder: Automations::Actions::Serializer
before_validation :ensure_manual_trigger
has_and_belongs_to_many :status_conditions, class_name: "Status", join_table: :automations_statuses
has_and_belongs_to_many :role_conditions, class_name: "Role", join_table: :automations_roles
has_and_belongs_to_many :type_conditions, class_name: "Type", join_table: :automations_types
has_and_belongs_to_many :project_conditions, class_name: "Project", join_table: :automations_projects
has_many :triggers,
-> { order(:position, :id) },
class_name: "Automations::Triggers::Base",
dependent: :destroy,
inverse_of: :automation
accepts_nested_attributes_for :triggers
after_save :persist_conditions
@@ -43,19 +27,18 @@ class CustomAction < ApplicationRecord
acts_as_list
scope :with_manual_trigger, -> {
joins(:triggers).where(automation_triggers: { type: "Automations::Triggers::Manual" }).distinct
}
def initialize(*args)
ret = super
if actions.nil?
self.actions = []
end
self.actions ||= []
ret
end
def reload(*args)
@conditions = nil
super
end
@@ -77,7 +60,7 @@ class CustomAction < ApplicationRecord
end
def available_actions
::CustomActions::Register.actions.map(&:all).flatten
::Automations::Register.actions.map(&:all).flatten
end
def all_conditions
@@ -104,11 +87,17 @@ class CustomAction < ApplicationRecord
end
def self.available_conditions
::CustomActions::Register.conditions
::Automations::Register.conditions
end
private
def ensure_manual_trigger
return unless triggers.reject(&:marked_for_destruction?).empty?
triggers.build(type: "Automations::Triggers::Manual", options: { button_label: name })
end
def all_of(availables, actual)
availables.map do |available|
existing = actual.detect { |a| a.key == available.key }
@@ -124,4 +113,13 @@ class CustomAction < ApplicationRecord
condition_class.setter(self, condition)
end
end
def must_have_at_least_one_trigger
errors.add(:triggers, :blank) if triggers.reject(&:marked_for_destruction?).empty?
end
def must_not_have_more_than_one_manual_trigger
manual_triggers = triggers.reject(&:marked_for_destruction?).count { |trigger| trigger.type == "Automations::Triggers::Manual" }
errors.add(:triggers, :invalid) if manual_triggers > 1
end
end
@@ -28,8 +28,8 @@
# See COPYRIGHT and LICENSE files for more details.
#++
class CustomActions::Actions::AssignedTo < CustomActions::Actions::Base
include CustomActions::Actions::Strategies::MeAssociated
class Automations::Actions::AssignedTo < Automations::Actions::Base
include Automations::Actions::Strategies::MeAssociated
def self.key
:assigned_to
@@ -28,7 +28,7 @@
# See COPYRIGHT and LICENSE files for more details.
#++
class CustomActions::Actions::Base
class Automations::Actions::Base
attr_reader :values
DEFAULT_PRIORITY = 100
@@ -98,10 +98,6 @@ class CustomActions::Actions::Base
private
def deconstruct_keys(*)
{ type:, custom_field_based: respond_to?(:custom_field) }
end
def validate_value_required(errors)
if required? && values.empty?
errors.add :actions,
@@ -28,7 +28,7 @@
# See COPYRIGHT and LICENSE files for more details.
#++
class CustomActions::Actions::CustomField < CustomActions::Actions::Base
class Automations::Actions::CustomField < Automations::Actions::Base
class << self
def key
custom_field.attribute_name.to_sym
@@ -40,7 +40,7 @@ class CustomActions::Actions::CustomField < CustomActions::Actions::Base
def all
WorkPackageCustomField
.usable_as_custom_action
.usable_as_automation
.map do |cf|
create_subclass(cf)
end
@@ -57,7 +57,7 @@ class CustomActions::Actions::CustomField < CustomActions::Actions::Base
private
def create_subclass(custom_field)
klass = Class.new(CustomActions::Actions::CustomField)
klass = Class.new(Automations::Actions::CustomField)
klass.define_singleton_method(:custom_field) do
custom_field
end
@@ -69,23 +69,23 @@ class CustomActions::Actions::CustomField < CustomActions::Actions::Base
def strategy(custom_field)
case custom_field.field_format
when "string"
CustomActions::Actions::Strategies::String
Automations::Actions::Strategies::String
when "text"
CustomActions::Actions::Strategies::Text
Automations::Actions::Strategies::Text
when "link"
CustomActions::Actions::Strategies::Link
Automations::Actions::Strategies::Link
when "int"
CustomActions::Actions::Strategies::Integer
Automations::Actions::Strategies::Integer
when "float"
CustomActions::Actions::Strategies::Float
Automations::Actions::Strategies::Float
when "date"
CustomActions::Actions::Strategies::Date
Automations::Actions::Strategies::Date
when "bool"
CustomActions::Actions::Strategies::Boolean
Automations::Actions::Strategies::Boolean
when "user"
CustomActions::Actions::Strategies::UserCustomField
Automations::Actions::Strategies::UserCustomField
when "list", "version"
CustomActions::Actions::Strategies::AssociatedCustomField
Automations::Actions::Strategies::AssociatedCustomField
end
end
end
@@ -28,8 +28,8 @@
# See COPYRIGHT and LICENSE files for more details.
#++
class CustomActions::Actions::Date < CustomActions::Actions::Base
include CustomActions::Actions::Strategies::Date
class Automations::Actions::Date < Automations::Actions::Base
include Automations::Actions::Strategies::Date
def self.key
:date
@@ -28,8 +28,8 @@
# See COPYRIGHT and LICENSE files for more details.
#++
class CustomActions::Actions::DoneRatio < CustomActions::Actions::Base
include CustomActions::Actions::Strategies::Integer
class Automations::Actions::DoneRatio < Automations::Actions::Base
include Automations::Actions::Strategies::Integer
def self.key
:done_ratio
@@ -28,8 +28,8 @@
# See COPYRIGHT and LICENSE files for more details.
#++
class CustomActions::Actions::DueDate < CustomActions::Actions::Base
include CustomActions::Actions::Strategies::DateProperty
class Automations::Actions::DueDate < Automations::Actions::Base
include Automations::Actions::Strategies::DateProperty
def self.key
:due_date
@@ -28,8 +28,8 @@
# See COPYRIGHT and LICENSE files for more details.
#++
class CustomActions::Actions::EstimatedHours < CustomActions::Actions::Base
include CustomActions::Actions::Strategies::Float
class Automations::Actions::EstimatedHours < Automations::Actions::Base
include Automations::Actions::Strategies::Float
def self.key
:estimated_hours
@@ -28,7 +28,7 @@
# See COPYRIGHT and LICENSE files for more details.
#++
class CustomActions::Actions::Inexistent < CustomActions::Actions::Base
class Automations::Actions::Inexistent < Automations::Actions::Base
def self.key
:inexistent
end
@@ -28,8 +28,8 @@
# See COPYRIGHT and LICENSE files for more details.
#++
class CustomActions::Actions::Notify < CustomActions::Actions::Base
include CustomActions::Actions::Strategies::Associated
class Automations::Actions::Notify < Automations::Actions::Base
include Automations::Actions::Strategies::Associated
def apply(work_package)
comment = principals.where(id: values).map do |p|
@@ -28,8 +28,8 @@
# See COPYRIGHT and LICENSE files for more details.
#++
class CustomActions::Actions::Priority < CustomActions::Actions::Base
include CustomActions::Actions::Strategies::Associated
class Automations::Actions::Priority < Automations::Actions::Base
include Automations::Actions::Strategies::Associated
def associated
IssuePriority
@@ -28,8 +28,8 @@
# See COPYRIGHT and LICENSE files for more details.
#++
class CustomActions::Actions::Project < CustomActions::Actions::Base
include CustomActions::Actions::Strategies::Associated
class Automations::Actions::Project < Automations::Actions::Base
include Automations::Actions::Strategies::Associated
PRIORITY = 10
@@ -28,8 +28,8 @@
# See COPYRIGHT and LICENSE files for more details.
#++
class CustomActions::Actions::Responsible < CustomActions::Actions::Base
include CustomActions::Actions::Strategies::MeAssociated
class Automations::Actions::Responsible < Automations::Actions::Base
include Automations::Actions::Strategies::MeAssociated
def type
:user
@@ -28,7 +28,7 @@
# See COPYRIGHT and LICENSE files for more details.
#++
module CustomActions::Actions::Serializer
module Automations::Actions::Serializer
module_function
def load(value)
@@ -39,13 +39,13 @@ module CustomActions::Actions::Serializer
.filter_map do |key, values|
klass = nil
CustomActions::Register
Automations::Register
.actions
.detect do |a|
klass = a.for(key)
end
klass ||= CustomActions::Actions::Inexistent
klass ||= Automations::Actions::Inexistent
klass.new(values)
end
@@ -28,8 +28,8 @@
# See COPYRIGHT and LICENSE files for more details.
#++
class CustomActions::Actions::StartDate < CustomActions::Actions::Base
include CustomActions::Actions::Strategies::DateProperty
class Automations::Actions::StartDate < Automations::Actions::Base
include Automations::Actions::Strategies::DateProperty
def self.key
:start_date
@@ -28,8 +28,8 @@
# See COPYRIGHT and LICENSE files for more details.
#++
class CustomActions::Actions::Status < CustomActions::Actions::Base
include CustomActions::Actions::Strategies::Associated
class Automations::Actions::Status < Automations::Actions::Base
include Automations::Actions::Strategies::Associated
def self.key
:status
@@ -28,9 +28,9 @@
# See COPYRIGHT and LICENSE files for more details.
#++
module CustomActions::Actions::Strategies::Associated
include CustomActions::ValidateAllowedValue
include CustomActions::ValuesToInteger
module Automations::Actions::Strategies::Associated
include Automations::ValidateAllowedValue
include Automations::ValuesToInteger
def allowed_values
@allowed_values ||= begin
@@ -28,9 +28,9 @@
# See COPYRIGHT and LICENSE files for more details.
#++
module CustomActions::Actions::Strategies::AssociatedCustomField
include CustomActions::Actions::Strategies::Associated
include CustomActions::Actions::Strategies::CustomField
module Automations::Actions::Strategies::AssociatedCustomField
include Automations::Actions::Strategies::Associated
include Automations::Actions::Strategies::CustomField
def associated
custom_field
@@ -21,8 +21,8 @@
# See COPYRIGHT and LICENSE files for more details.
#++
module CustomActions::Actions::Strategies::Boolean
include CustomActions::ValidateAllowedValue
module Automations::Actions::Strategies::Boolean
include Automations::ValidateAllowedValue
def allowed_values
[
@@ -28,7 +28,7 @@
# See COPYRIGHT and LICENSE files for more details.
#++
module CustomActions::Actions::Strategies::CustomField
module Automations::Actions::Strategies::CustomField
def apply(work_package)
if work_package.respond_to?(custom_field.attribute_setter)
set_custom_field_value(work_package)
@@ -28,7 +28,7 @@
# See COPYRIGHT and LICENSE files for more details.
#++
module CustomActions::Actions::Strategies::Date
module Automations::Actions::Strategies::Date
def values=(values)
super(Array(values).map { |v| to_date_or_nil(v) }.uniq)
end
@@ -28,6 +28,6 @@
# See COPYRIGHT and LICENSE files for more details.
#++
module CustomActions::Actions::Strategies::DateProperty
include CustomActions::Actions::Strategies::Date
module Automations::Actions::Strategies::DateProperty
include Automations::Actions::Strategies::Date
end
@@ -28,8 +28,8 @@
# See COPYRIGHT and LICENSE files for more details.
#++
module CustomActions::Actions::Strategies::Float
include CustomActions::Actions::Strategies::ValidateInRange
module Automations::Actions::Strategies::Float
include Automations::Actions::Strategies::ValidateInRange
def values=(values)
super(Array(values).map { |v| to_float_or_nil(v) }.uniq)
@@ -28,9 +28,9 @@
# See COPYRIGHT and LICENSE files for more details.
#++
module CustomActions::Actions::Strategies::Integer
include CustomActions::ValuesToInteger
include CustomActions::Actions::Strategies::ValidateInRange
module Automations::Actions::Strategies::Integer
include Automations::ValuesToInteger
include Automations::Actions::Strategies::ValidateInRange
def type
:integer_property
@@ -28,8 +28,8 @@
# See COPYRIGHT and LICENSE files for more details.
#++
module CustomActions::Actions::Strategies::Link
include CustomActions::Actions::Strategies::ValuesToString
module Automations::Actions::Strategies::Link
include Automations::Actions::Strategies::ValuesToString
def type
:link_property
@@ -28,8 +28,8 @@
# See COPYRIGHT and LICENSE files for more details.
#++
module CustomActions::Actions::Strategies::MeAssociated
include ::CustomActions::Actions::Strategies::Associated
module Automations::Actions::Strategies::MeAssociated
include ::Automations::Actions::Strategies::Associated
def me_value
[current_user_value_key, current_user_name]
@@ -66,7 +66,7 @@ module CustomActions::Actions::Strategies::MeAssociated
end
def current_user_name
I18n.t("custom_actions.actions.assigned_to.executing_user_value")
I18n.t("automations.actions.assigned_to.executing_user_value")
end
def has_me_value?
@@ -28,8 +28,8 @@
# See COPYRIGHT and LICENSE files for more details.
#++
module CustomActions::Actions::Strategies::String
include CustomActions::Actions::Strategies::ValuesToString
module Automations::Actions::Strategies::String
include Automations::Actions::Strategies::ValuesToString
def type
:string_property
@@ -28,8 +28,8 @@
# See COPYRIGHT and LICENSE files for more details.
#++
module CustomActions::Actions::Strategies::Text
include CustomActions::Actions::Strategies::ValuesToString
module Automations::Actions::Strategies::Text
include Automations::Actions::Strategies::ValuesToString
def type
:text_property
@@ -28,9 +28,9 @@
# See COPYRIGHT and LICENSE files for more details.
#++
module CustomActions::Actions::Strategies::UserCustomField
include ::CustomActions::Actions::Strategies::CustomField
include ::CustomActions::Actions::Strategies::MeAssociated
module Automations::Actions::Strategies::UserCustomField
include ::Automations::Actions::Strategies::CustomField
include ::Automations::Actions::Strategies::MeAssociated
def type
:user
@@ -28,7 +28,7 @@
# See COPYRIGHT and LICENSE files for more details.
#++
module CustomActions::Actions::Strategies::ValidateInRange
module Automations::Actions::Strategies::ValidateInRange
def minimum
nil
end
@@ -28,7 +28,7 @@
# See COPYRIGHT and LICENSE files for more details.
#++
module CustomActions::Actions::Strategies::ValuesToString
module Automations::Actions::Strategies::ValuesToString
def values=(values)
super(Array(values).map { |v| to_string_or_nil(v) }.uniq)
end
@@ -28,8 +28,8 @@
# See COPYRIGHT and LICENSE files for more details.
#++
class CustomActions::Actions::Type < CustomActions::Actions::Base
include CustomActions::Actions::Strategies::Associated
class Automations::Actions::Type < Automations::Actions::Base
include Automations::Actions::Strategies::Associated
PRIORITY = 20
@@ -28,11 +28,11 @@
# See COPYRIGHT and LICENSE files for more details.
#++
class CustomActions::Conditions::Base
class Automations::Conditions::Base
attr_reader :values
prepend CustomActions::ValuesToInteger
include CustomActions::ValidateAllowedValue
prepend Automations::ValuesToInteger
include Automations::ValidateAllowedValue
def initialize(values = nil)
self.values = values
@@ -74,38 +74,38 @@ class CustomActions::Conditions::Base
validate_allowed_value(errors, :conditions)
end
def self.getter(custom_action)
ids = custom_action.send(association_ids)
def self.getter(automation)
ids = automation.send(association_ids)
new(ids) if ids.any?
end
def self.setter(custom_action, condition)
def self.setter(automation, condition)
if condition
custom_action.send(:"#{association_ids}=", condition.values)
automation.send(:"#{association_ids}=", condition.values)
else
custom_action.send(:"#{association_key}").clear
automation.send(:"#{association_key}").clear
end
end
def self.custom_action_scope(work_packages, user)
custom_action_scope_has_current(work_packages, user)
.or(custom_action_scope_has_no)
def self.automation_scope(work_packages, user)
automation_scope_has_current(work_packages, user)
.or(automation_scope_has_no)
end
def self.custom_action_scope_has_current(work_packages, _user)
CustomAction
def self.automation_scope_has_current(work_packages, _user)
Automation
.includes(association_key)
.where(habtm_table => { key_id => Array(work_packages).map { |w| w.send(key_id) }.uniq })
end
private_class_method :custom_action_scope_has_current
private_class_method :automation_scope_has_current
def self.custom_action_scope_has_no
CustomAction
def self.automation_scope_has_no
Automation
.includes(association_key)
.where(habtm_table => { key_id => nil })
end
private_class_method :custom_action_scope_has_no
private_class_method :automation_scope_has_no
def self.pluralized_key
key.to_s.pluralize.to_sym
@@ -113,7 +113,7 @@ class CustomActions::Conditions::Base
private_class_method :pluralized_key
def self.habtm_table
:"custom_actions_#{pluralized_key}"
:"automations_#{pluralized_key}"
end
private_class_method :habtm_table
@@ -28,7 +28,7 @@
# See COPYRIGHT and LICENSE files for more details.
#++
class CustomActions::Conditions::Inexistent < CustomActions::Conditions::Base
class Automations::Conditions::Inexistent < Automations::Conditions::Base
def self.key
:inexistent
end
@@ -28,7 +28,7 @@
# See COPYRIGHT and LICENSE files for more details.
#++
class CustomActions::Conditions::Project < CustomActions::Conditions::Base
class Automations::Conditions::Project < Automations::Conditions::Base
def self.key
:project
end
@@ -28,7 +28,7 @@
# See COPYRIGHT and LICENSE files for more details.
#++
class CustomActions::Conditions::Role < CustomActions::Conditions::Base
class Automations::Conditions::Role < Automations::Conditions::Base
def fulfilled_by?(work_package, user)
values.empty? ||
(self.class.roles_in_project(work_package, user).map(&:id) & values).any?
@@ -49,8 +49,8 @@ class CustomActions::Conditions::Role < CustomActions::Conditions::Base
private
def custom_action_scope_has_current(work_packages, user)
CustomAction
def automation_scope_has_current(work_packages, user)
Automation
.includes(association_key)
.where(habtm_table => { key_id => roles_in_project(work_packages, user) })
end
@@ -67,11 +67,11 @@ class CustomActions::Conditions::Role < CustomActions::Conditions::Base
end
def with_request_store(projects)
RequestStore.store[:custom_actions_role] ||= Hash.new do |hash, hash_projects|
RequestStore.store[:automations_role] ||= Hash.new do |hash, hash_projects|
hash[hash_projects] = yield hash_projects
end
RequestStore.store[:custom_actions_role][projects]
RequestStore.store[:automations_role][projects]
end
end
@@ -28,7 +28,7 @@
# See COPYRIGHT and LICENSE files for more details.
#++
class CustomActions::Conditions::Status < CustomActions::Conditions::Base
class Automations::Conditions::Status < Automations::Conditions::Base
def self.key
:status
end
@@ -28,7 +28,7 @@
# See COPYRIGHT and LICENSE files for more details.
#++
class CustomActions::Conditions::Type < CustomActions::Conditions::Base
class Automations::Conditions::Type < Automations::Conditions::Base
def self.key
:type
end
@@ -28,32 +28,32 @@
# See COPYRIGHT and LICENSE files for more details.
#++
module CustomActions::Register
module Automations::Register
class << self
def actions
[
CustomActions::Actions::AssignedTo,
CustomActions::Actions::Responsible,
CustomActions::Actions::Status,
CustomActions::Actions::Priority,
CustomActions::Actions::CustomField,
CustomActions::Actions::Type,
CustomActions::Actions::Project,
CustomActions::Actions::Notify,
CustomActions::Actions::DoneRatio,
CustomActions::Actions::EstimatedHours,
CustomActions::Actions::StartDate,
CustomActions::Actions::DueDate,
CustomActions::Actions::Date
Automations::Actions::AssignedTo,
Automations::Actions::Responsible,
Automations::Actions::Status,
Automations::Actions::Priority,
Automations::Actions::CustomField,
Automations::Actions::Type,
Automations::Actions::Project,
Automations::Actions::Notify,
Automations::Actions::DoneRatio,
Automations::Actions::EstimatedHours,
Automations::Actions::StartDate,
Automations::Actions::DueDate,
Automations::Actions::Date
]
end
def conditions
[
CustomActions::Conditions::Status,
CustomActions::Conditions::Role,
CustomActions::Conditions::Type,
CustomActions::Conditions::Project
Automations::Conditions::Status,
Automations::Conditions::Role,
Automations::Conditions::Type,
Automations::Conditions::Project
]
end
end
+13
View File
@@ -0,0 +1,13 @@
# frozen_string_literal: true
module Automations
module Triggers
class Base < ApplicationRecord
self.table_name = "automation_triggers"
belongs_to :automation, inverse_of: :triggers
acts_as_list scope: :automation
end
end
end
+11
View File
@@ -0,0 +1,11 @@
# frozen_string_literal: true
module Automations
module Triggers
class Manual < Base
store_attribute :options, :button_label, :string
validates :button_label, presence: true, length: { maximum: 255 }
end
end
end
@@ -21,7 +21,7 @@
# See COPYRIGHT and LICENSE files for more details.
#++
module CustomActions::ValidateAllowedValue
module Automations::ValidateAllowedValue
private
def validate_allowed_value(errors, attribute)
@@ -28,7 +28,7 @@
# See COPYRIGHT and LICENSE files for more details.
#++
module CustomActions::ValuesToInteger
module Automations::ValuesToInteger
def values=(values)
super(Array(values).map { |v| to_integer_or_nil(v) }.uniq)
end
+10 -9
View File
@@ -92,12 +92,12 @@ class PermittedParams
params.require(:custom_field).permit(*self.class.permitted_attributes[:custom_field])
end
def custom_action
def automation
whitelisted = params
.require(:custom_action)
.permit(*self.class.permitted_attributes[:custom_action])
.require(:automation)
.permit(*self.class.permitted_attributes[:automation])
whitelisted.merge(params[:custom_action].slice(:actions, :conditions).permit!)
whitelisted.merge(params[:automation].slice(:actions, :conditions).permit!)
end
def custom_field_type
@@ -506,11 +506,12 @@ class PermittedParams
hexcode
move_to
),
custom_action: %i(
name
description
move_to
),
automation: [
:name,
:description,
:move_to,
{ triggers_attributes: [:id, :type, :position, { options: {} }, :_destroy] }
],
custom_field: [
:editable,
:field_format,
+1 -1
View File
@@ -36,7 +36,7 @@ class WorkPackage < ApplicationRecord
include WorkPackage::AskBeforeDestruction
include WorkPackage::TimeEntriesCleaner
include WorkPackage::Ancestors
include WorkPackage::CustomActioned
include WorkPackage::Automatable
include WorkPackage::Hooks
include WorkPackages::DerivedDates
include WorkPackages::SpentTime
+20
View File
@@ -0,0 +1,20 @@
# frozen_string_literal: true
module WorkPackage::Automatable
extend ActiveSupport::Concern
included do
def automations(user)
@automations = Automation
.available_conditions
.inject(Automation.all) do |scope, condition|
scope.merge(condition.automation_scope(self, user))
end
end
# API compatibility for /api/v3/custom_actions
def custom_actions(user)
automations(user)
end
end
end
@@ -1,43 +0,0 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module WorkPackage::CustomActioned
extend ActiveSupport::Concern
included do
def custom_actions(user)
@custom_actions = CustomAction
.available_conditions
.inject(CustomAction.all) do |scope, condition|
scope.merge(condition.custom_action_scope(self, user))
end
end
end
end
+1 -1
View File
@@ -43,7 +43,7 @@ class WorkPackageCustomField < CustomField
scopes :visible,
:on_visible_type_and_project
scope :usable_as_custom_action, -> {
scope :usable_as_automation, -> {
where.not(field_format: %w[hierarchy weighted_item_list])
.order(:name)
}
@@ -1,34 +1,6 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
class CustomActions::BaseService
class Automations::BaseService
include Shared::BlockService
attr_accessor :user
@@ -38,7 +10,7 @@ class CustomActions::BaseService
&)
set_attributes(action, attributes)
contract = CustomActions::CuContract.new(action)
contract = Automations::CuContract.new(action)
result = ServiceResult.new(success: contract.validate && action.save,
result: action,
errors: contract.errors)
@@ -51,10 +23,12 @@ class CustomActions::BaseService
def set_attributes(action, attributes)
actions_attributes = attributes.delete(:actions)
conditions_attributes = attributes.delete(:conditions)
action.attributes = attributes
triggers_attributes = attributes.delete(:triggers_attributes)
action.attributes = attributes
set_actions(action, actions_attributes.symbolize_keys) if actions_attributes
set_conditions(action, conditions_attributes.symbolize_keys) if conditions_attributes
set_triggers(action, triggers_attributes) if triggers_attributes
end
def set_actions(action, actions_attributes)
@@ -66,21 +40,15 @@ class CustomActions::BaseService
end
def remove_actions(action, keys)
keys.each do |key|
remove_action(action, key)
end
keys.each { |key| remove_action(action, key) }
end
def update_actions(action, key_values)
key_values.each do |key, values|
update_action(action, key, values)
end
key_values.each { |key, values| update_action(action, key, values) }
end
def add_actions(action, key_values)
key_values.each do |key, values|
add_action(action, key, values)
end
key_values.each { |key, values| add_action(action, key, values) }
end
def update_action(action, key, values)
@@ -102,10 +70,14 @@ class CustomActions::BaseService
end
def available_action_for(action, key)
action.available_actions.detect { |a| a.key == key } || CustomActions::Actions::Inexistent
action.available_actions.detect { |a| a.key == key } || Automations::Actions::Inexistent
end
def available_condition_for(action, key)
action.available_conditions.detect { |a| a.key == key } || CustomActions::Conditions::Inexistent
action.available_conditions.detect { |a| a.key == key } || Automations::Conditions::Inexistent
end
def set_triggers(action, attributes)
action.assign_attributes(triggers_attributes: attributes)
end
end
@@ -0,0 +1,11 @@
# frozen_string_literal: true
class Automations::CreateService < Automations::BaseService
def initialize(user:)
self.user = user
end
def call(attributes:, action: Automation.new, &block)
super
end
end
@@ -0,0 +1,15 @@
# frozen_string_literal: true
class Automations::UpdateService < Automations::BaseService
attr_accessor :user,
:action
def initialize(action:, user:)
self.action = action
self.user = user
end
def call(attributes:, &)
super(attributes:, action:, &)
end
end
@@ -0,0 +1,58 @@
# frozen_string_literal: true
class Automations::UpdateWorkPackageService
include Shared::BlockService
include Contracted
attr_accessor :user,
:action
def initialize(action:, user:)
self.action = action
self.user = user
self.contract_class = ::WorkPackages::UpdateContract
end
def call(work_package:, &)
apply_actions(work_package, action.actions)
result = ::WorkPackages::UpdateService
.new(user:,
model: work_package)
.call
block_with_result(result, &)
end
private
def apply_actions(work_package, actions)
changes_before = work_package.changes.dup
apply_actions_sorted(work_package, actions)
success, errors = validate(work_package, user)
retry_apply_actions(work_package, actions, errors, changes_before) unless success
end
def retry_apply_actions(work_package, actions, errors, changes_before)
new_actions = without_invalid_actions(actions, errors)
if new_actions.any? && actions.length != new_actions.length
work_package.restore_attributes(work_package.changes.keys - changes_before.keys)
apply_actions(work_package, new_actions)
end
end
def without_invalid_actions(actions, errors)
invalid_keys = errors.attribute_names.map { |k| append_id(k) }
actions.reject { |a| invalid_keys.include?(append_id(a.key)) }
end
def apply_actions_sorted(work_package, actions)
actions.sort_by(&:priority).each { |a| a.apply(work_package) }
end
def append_id(sym)
"#{sym.to_s.chomp('_id')}_id"
end
end
@@ -1,41 +0,0 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
class CustomActions::CreateService < CustomActions::BaseService
def initialize(user:)
self.user = user
end
def call(attributes:,
action: CustomAction.new,
&block)
super
end
end
@@ -1,43 +0,0 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
class CustomActions::UpdateService < CustomActions::BaseService
attr_accessor :user,
:action
def initialize(action:, user:)
self.action = action
self.user = user
end
def call(attributes:, &)
super(attributes:, action:, &)
end
end
@@ -1,92 +0,0 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
class CustomActions::UpdateWorkPackageService
include Shared::BlockService
include Contracted
attr_accessor :user,
:action
def initialize(action:, user:)
self.action = action
self.user = user
self.contract_class = ::WorkPackages::UpdateContract
end
def call(work_package:, &)
apply_actions(work_package, action.actions)
result = ::WorkPackages::UpdateService
.new(user:,
model: work_package)
.call
block_with_result(result, &)
end
private
def apply_actions(work_package, actions)
changes_before = work_package.changes.dup
apply_actions_sorted(work_package, actions)
success, errors = validate(work_package, user)
unless success
retry_apply_actions(work_package, actions, errors, changes_before)
end
end
def retry_apply_actions(work_package, actions, errors, changes_before)
new_actions = without_invalid_actions(actions, errors)
if new_actions.any? && actions.length != new_actions.length
work_package.restore_attributes(work_package.changes.keys - changes_before.keys)
apply_actions(work_package, new_actions)
end
end
def without_invalid_actions(actions, errors)
invalid_keys = errors.attribute_names.map { |k| append_id(k) }
actions.reject { |a| invalid_keys.include?(append_id(a.key)) }
end
def apply_actions_sorted(work_package, actions)
actions
.sort_by(&:priority)
.each { |a| a.apply(work_package) }
end
def append_id(sym)
"#{sym.to_s.chomp('_id')}_id"
end
end
+120
View File
@@ -0,0 +1,120 @@
<% active_section_keys = @automation.actions.map(&:key) %>
<% trigger = @automation.triggers.detect { |t| t.is_a?(Automations::Triggers::Manual) } || @automation.triggers.build(type: "Automations::Triggers::Manual") %>
<div class="form--field -required">
<%= f.text_field :name, required: true, container_class: "-middle" %>
</div>
<div class="form--field">
<%= f.text_area :description, container_class: "-middle" %>
</div>
<fieldset class="form--fieldset" id="automations-form--trigger">
<legend class="form--fieldset-legend"><%= t("automations.trigger") %></legend>
<div class="form--field">
<%= styled_label_tag "automation_trigger_type", t("automations.triggers.name"), class: "-top" %>
<div class="form--field-container">
<%= styled_text_field_tag "automation_trigger_type",
t("automations.triggers.manual.label"),
disabled: true,
container_class: "-middle" %>
</div>
</div>
<%= render partial: "trigger_fields_manual", locals: { trigger: } %>
</fieldset>
<fieldset class="form--fieldset" id="automations-form--conditions">
<legend class="form--fieldset-legend"><%= t("automations.conditions") %></legend>
<% @automation.all_conditions.each do |condition| %>
<div class="form--field">
<%= styled_label_tag("automation_conditions_#{condition.key}", condition.human_name, class: "-top") %>
<% input_name = "automation[conditions][#{condition.key}]" %>
<div class="form--field-container">
<div class="form--select-container -middle">
<% if condition.key == :project %>
<%= angular_component_tag "opce-project-autocompleter",
inputs: {
multiple: true,
filters: [{ name: "active", operator: "=", values: ["t"] }],
resource: "projects",
inputName: input_name,
labelForId: "automation_conditions_#{condition.key}",
inputValue: condition.values
} %>
<% else %>
<%= angular_component_tag "opce-autocompleter",
inputs: {
multiple: true,
defaultData: false,
items: condition.allowed_values.map { |v| { id: v[:value], name: v[:label] } },
model: condition.value_objects.map { |v| { id: v[:value], name: v[:label] } },
inputName: input_name,
bindLabel: "name",
labelForId: "automation_conditions_#{condition.key}"
} %>
<% end %>
</div>
</div>
</div>
<% end %>
</fieldset>
<fieldset class="form--fieldset" id="automations-form--actions">
<legend class="form--fieldset-legend"><%= t("automations.actions.name") %></legend>
<div id="automations-form--active-actions">
<% @automation.all_actions.each do |action| %>
<section class="hide-section"
data-hide-sections-target="section"
data-name="<%= action.key %>"
<%= tag.attributes(data: { section_name: action.key }, hidden: active_section_keys.exclude?(action.key)) %>>
<div class="form--field">
<%= styled_label_tag("automation_actions_#{action.key}", action.human_name, class: "-top") %>
<% input_name = "automation[actions][#{action.key}]" %>
<% if %i(associated_property boolean user project).include?(action.type) %>
<div class="form--field-container">
<div class="form--select-container -middle">
<% selected = action.value_objects.map { |v| { id: v[:value], name: v[:label] } } %>
<%= angular_component_tag "opce-autocompleter",
inputs: {
multiple: action.multi_value?,
hideSelected: true,
defaultData: false,
items: action.allowed_values.map { |v| { id: v[:value], name: v[:label] } },
model: action.multi_value? ? selected : selected.first,
inputName: input_name,
bindLabel: "name",
labelForId: "automation_actions_#{action.key}"
} %>
</div>
</div>
<% elsif %i(string_property text_property link_property float_property integer_property).include?(action.type) %>
<div class="form--field-container">
<%= styled_text_field_tag input_name, action.values, container_class: "-slim" %>
</div>
<% end %>
<%= render(Primer::Beta::IconButton.new(icon: :x, size: :small, scheme: :invisible, type: :button, aria: { label: t(:button_close) }, data: { action: "click->hide-sections#hide" })) %>
</div>
</section>
<% end %>
</div>
<div class="form--field">
<label class="form--label" for="add-automation-action-select">
<%= render(Primer::Beta::Octicon.new(icon: :plus, size: :small)) %>
<%= I18n.t(:"automations.actions.add") %>
</label>
<span class="form--field-container">
<span class="form--select-container -middle">
<%= select_tag "add_action",
options_for_select(@automation.all_actions.map { |a| [a.human_name, a.key] }, disabled: active_section_keys),
id: "add-automation-action-select",
prompt: t(:"automations.actions.add"),
data: { "hide-sections-target": "select", action: "change->hide-sections#add" },
class: "form--select" %>
</span>
</span>
</div>
</fieldset>
@@ -0,0 +1,13 @@
<%= hidden_field_tag "automation[triggers_attributes][0][type]", "Automations::Triggers::Manual" %>
<%= hidden_field_tag "automation[triggers_attributes][0][id]", trigger.id if trigger.persisted? %>
<div class="form--field -required">
<%= styled_label_tag "automation_triggers_attributes_0_options_button_label", t("automations.triggers.manual.button_label"), class: "-top" %>
<div class="form--field-container">
<%= styled_text_field_tag "automation[triggers_attributes][0][options][button_label]",
trigger.button_label || @automation.name,
id: "automation_triggers_attributes_0_options_button_label",
container_class: "-middle",
required: true %>
</div>
</div>
+23
View File
@@ -0,0 +1,23 @@
<% html_title t(:label_administration), t("automations.edit", name: @automation.name) %>
<%=
render Primer::OpenProject::PageHeader.new do |header|
header.with_title { @automation.name }
header.with_breadcrumbs(
[{ href: admin_index_path, text: t("label_administration") },
{ href: admin_settings_work_packages_general_path, text: t(:label_work_package_plural) },
{ href: automations_path, text: t("automations.plural") },
@automation.name]
)
end
%>
<%= error_messages_for @automation %>
<% content_controller "hide-sections" %>
<%= labelled_tabular_form_for @automation,
data: { "hide-sections-target": "form" } do |f| %>
<%= render partial: "form", locals: { f: f } %>
<%= styled_button_tag t(:button_save), class: "-primary -with-icon icon-checkmark" %>
<% end %>
@@ -1,12 +1,12 @@
<% html_title t(:label_administration), t("custom_actions.plural") -%>
<% html_title t(:label_administration), t("automations.plural") -%>
<%=
render Primer::OpenProject::PageHeader.new do |header|
header.with_title { t("custom_actions.plural") }
header.with_title { t("automations.plural") }
header.with_breadcrumbs(
[{ href: admin_index_path, text: t("label_administration") },
{ href: admin_settings_work_packages_general_path, text: t(:label_work_package_plural) },
t("custom_actions.plural")]
t("automations.plural")]
)
end
%>
@@ -17,15 +17,15 @@
subheader.with_action_button(
scheme: :primary,
leading_icon: :plus,
label: t("custom_actions.new"),
test_selector: "op-admin-custom-actions--button-new",
label: t("automations.new"),
test_selector: "op-admin-automations--button-new",
tag: :a,
disabled: !EnterpriseToken.allows_to?(:custom_actions),
href: new_custom_action_path
href: new_automation_path
) do
CustomAction.model_name.human
Automation.model_name.human
end
end
%>
<%= render(::CustomActions::TableComponent.new(rows: @custom_actions)) %>
<%= render(::Automations::IndexComponent.new(rows: @automations)) %>
<% end %>
+23
View File
@@ -0,0 +1,23 @@
<% html_title t(:label_administration), t("automations.new") %>
<%=
render Primer::OpenProject::PageHeader.new do |header|
header.with_title { t("automations.new") }
header.with_breadcrumbs(
[{ href: admin_index_path, text: t("label_administration") },
{ href: admin_settings_work_packages_general_path, text: t(:label_work_package_plural) },
{ href: automations_path, text: t("automations.plural") },
t("automations.new")]
)
end
%>
<%= error_messages_for @automation %>
<% content_controller "hide-sections" %>
<%= labelled_tabular_form_for @automation,
data: { "hide-sections-target": "form" } do |f| %>
<%= render partial: "form", locals: { f: f } %>
<%= styled_button_tag t(:button_create), class: "-primary -with-icon icon-checkmark" %>
<% end %>
-200
View File
@@ -1,200 +0,0 @@
<% active_section_keys = @custom_action.actions.map(&:key) %>
<div class="form--field -required">
<%= f.text_field :name, required: true, container_class: "-middle" %>
</div>
<div class="form--field">
<%= f.text_area :description, container_class: "-middle" %>
</div>
<fieldset class="form--fieldset" id="custom-actions-form--conditions">
<legend class="form--fieldset-legend">
<%= t("custom_actions.conditions") %>
</legend>
<% @custom_action.all_conditions.each do |condition| %>
<div class="form--field">
<%= styled_label_tag("custom_action_conditions_#{condition.key}", condition.human_name, class: "-top") %>
<% input_name = "custom_action[conditions][#{condition.key}]" %>
<div class="form--field-container">
<div class="form--select-container -middle">
<% if condition.key == :project %>
<%= angular_component_tag "opce-project-autocompleter",
inputs: {
multiple: true,
filters: [{ name: "active", operator: "=", values: ["t"] }],
resource: "projects",
inputName: input_name,
labelForId: "custom_action_actions_#{condition.key}",
inputValue: condition.values
} %>
<% else %>
<%= angular_component_tag "opce-autocompleter",
inputs: {
multiple: true,
defaultData: false,
items: condition.allowed_values.map { |v| { id: v[:value], name: v[:label] } },
model: condition.value_objects.map { |v| { id: v[:value], name: v[:label] } },
inputName: input_name,
bindLabel: "name",
labelForId: "custom_action_conditions_#{condition.key}"
} %>
<% end %>
</div>
</div>
</div>
<div class="form--space"></div>
<% end %>
</fieldset>
<fieldset class="form--fieldset" id="custom-actions-form--actions">
<legend class="form--fieldset-legend">
<%= t("custom_actions.actions.name") %>
</legend>
<div id="custom-actions-form--active-actions">
<% @custom_action.all_actions.each do |action| %>
<section class="hide-section"
data-hide-sections-target="section"
data-name="<%= action.key %>"
<%= tag.attributes(data: { section_name: action.key }, hidden: active_section_keys.exclude?(action.key)) %>>
<div class="form--field">
<%= styled_label_tag("custom_action_actions_#{action.key}", action.human_name, class: "-top") %>
<% input_name = "custom_action[actions][#{action.key}]" %>
<% if %i(associated_property boolean user project).include?(action.type) %>
<div class="form--field-container">
<div class="form--select-container -middle">
<% case action %>
<% in { type: :project } %>
<%= angular_component_tag "opce-project-autocompleter",
inputs: {
multiple: false,
filters: [{ name: "active", operator: "=", values: ["t"] }],
resource: "projects",
inputName: input_name,
appendTo: "body",
labelForId: "custom_action_actions_#{action.key}",
inputValue: action.values.first
} %>
<% in { type: :user, custom_field_based: false } %>
<% selected = action.value_objects.map { |v| { id: v[:value], name: v[:label] } } %>
<%= angular_component_tag "opce-user-autocompleter",
inputs: {
multiple: action.multi_value?,
hideSelected: true,
defaultData: false,
placeholder: I18n.t(:label_user_search),
resource: "principals",
url: ::API::V3::Utilities::PathHelper::ApiV3Path.principals,
model: action.multi_value? ? selected : selected.first,
inputName: input_name,
filters: [
{ name: "status", operator: "!", values: [Principal.statuses["locked"].to_s] }
],
additionalOptions: [
{ id: nil, name: I18n.t("placeholders.default") },
{ id: action.current_user_value_key, name: action.current_user_name }
],
searchKey: "any_name_attribute",
labelForId: "custom_action_actions_#{action.key}",
focusDirectly: false
} %>
<% else %>
<% selected = action.value_objects.map { |v| { id: v[:value], name: v[:label] } } %>
<%= angular_component_tag "opce-autocompleter",
inputs: {
multiple: action.multi_value?,
hideSelected: true,
defaultData: false,
items: action.allowed_values.map { |v| { id: v[:value], name: v[:label] } },
model: action.multi_value? ? selected : selected.first,
inputName: input_name,
bindLabel: "name",
labelForId: "custom_action_actions_#{action.key}"
} %>
<% end %>
</div>
</div>
<% elsif %i(date_property).include?(action.type) %>
<div class="form--field-container">
<% date = action.values.first %>
<%= angular_component_tag "opce-custom-date-action-admin",
data: { "field-value": date.try(:iso8601) || date, "field-name": input_name } %>
</div>
<% elsif %i(string_property text_property).include?(action.type) %>
<div class="form--field-container">
<%= styled_text_field_tag input_name,
action.values,
container_class: "-slim",
step: "any" %>
</div>
<% elsif %i(link_property).include?(action.type) %>
<div class="form--field-container">
<%= styled_url_field_tag input_name,
action.values,
container_class: "-slim",
step: "any" %>
</div>
<% elsif action.type == :float_property %>
<div class="form--field-container">
<%= styled_number_field_tag input_name,
action.values,
container_class: "-slim",
min: action.minimum,
max: action.maximum,
step: "any" %>
</div>
<% elsif action.type == :integer_property %>
<div class="form--field-container">
<%= styled_number_field_tag input_name,
action.values,
container_class: "-slim",
min: action.minimum,
max: action.maximum,
step: 1 %>
</div>
<% end %>
<%=
render(
Primer::Beta::IconButton.new(
icon: :x,
size: :small,
scheme: :invisible,
type: :button,
aria: { label: t(:button_close) },
data: { action: "click->hide-sections#hide" }
)
)
%>
</div>
</section>
<% end %>
</div>
<div class="form--space"></div>
<div class="form--field">
<label class="form--label" for="add-custom-action-select">
<%= render(Primer::Beta::Octicon.new(icon: :plus, size: :small)) %>
<%= I18n.t(:"custom_actions.actions.add") %>
</label>
<span class="form--field-container">
<span class="form--select-container -middle">
<%= select_tag "add_action",
options_for_select(
@custom_action.all_actions.map { |a| [a.human_name, a.key] },
disabled: active_section_keys
),
id: "add-custom-action-select",
prompt: t(:"custom_actions.actions.add"),
data: {
"hide-sections-target": "select",
action: "change->hide-sections#add"
},
class: "form--select" %>
</span>
</span>
</div>
</fieldset>
-52
View File
@@ -1,52 +0,0 @@
<%#-- copyright
OpenProject is an open source project management software.
Copyright (C) the OpenProject GmbH
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 3.
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
Copyright (C) 2006-2013 Jean-Philippe Lang
Copyright (C) 2010-2013 the ChiliProject Team
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See COPYRIGHT and LICENSE files for more details.
++#%>
<% html_title t(:label_administration), t("custom_actions.edit", name: @custom_action.name) %>
<%=
render Primer::OpenProject::PageHeader.new do |header|
header.with_title { @custom_action.name }
header.with_breadcrumbs(
[{ href: admin_index_path, text: t("label_administration") },
{ href: admin_settings_work_packages_general_path, text: t(:label_work_package_plural) },
{ href: custom_actions_path, text: t("custom_actions.plural") },
@custom_action.name]
)
end
%>
<%= error_messages_for @custom_action %>
<% content_controller "hide-sections" %>
<%= labelled_tabular_form_for @custom_action,
data: { "hide-sections-target": "form" } do |f| %>
<%= render partial: "form", locals: { f: f } %>
<%= styled_button_tag t(:button_save), class: "-primary -with-icon icon-checkmark" %>
<% end %>
-52
View File
@@ -1,52 +0,0 @@
<%#-- copyright
OpenProject is an open source project management software.
Copyright (C) the OpenProject GmbH
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 3.
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
Copyright (C) 2006-2013 Jean-Philippe Lang
Copyright (C) 2010-2013 the ChiliProject Team
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See COPYRIGHT and LICENSE files for more details.
++#%>
<% html_title t(:label_administration), t("custom_actions.new") %>
<%=
render Primer::OpenProject::PageHeader.new do |header|
header.with_title { t("custom_actions.new") }
header.with_breadcrumbs(
[{ href: admin_index_path, text: t("label_administration") },
{ href: admin_settings_work_packages_general_path, text: t(:label_work_package_plural) },
{ href: custom_actions_path, text: t("custom_actions.plural") },
t("custom_actions.new")]
)
end
%>
<%= error_messages_for @custom_action %>
<% content_controller "hide-sections" %>
<%= labelled_tabular_form_for @custom_action,
data: { "hide-sections-target": "form" } do |f| %>
<%= render partial: "form", locals: { f: f } %>
<%= styled_button_tag t(:button_create), class: "-primary -with-icon icon-checkmark" %>
<% end %>
+3 -3
View File
@@ -475,10 +475,10 @@ Redmine::MenuManager.map :admin_menu do |menu|
icon: "op-custom-fields",
html: { class: "custom_fields" }
menu.push :custom_actions,
{ controller: "/custom_actions" },
menu.push :automations,
{ controller: "/automations" },
if: ->(_) { User.current.admin? },
caption: :"custom_actions.plural",
caption: :"automations.plural",
parent: :admin_work_packages,
enterprise_feature: "custom_actions"
+24
View File
@@ -658,6 +658,29 @@ en:
edit: "Edit custom action %{name}"
execute: "Execute %{name}"
automations:
trigger: "Trigger"
triggers:
name: "Triggers"
manual:
label: "Manual button click"
button_label: "Button label"
actions:
name: "Actions"
add: "Add action"
assigned_to:
executing_user_value: "(Assign to executing user)"
conditions: "Conditions"
plural: "Automations"
new: "New automation"
edit: "Edit automation %{name}"
summary:
title: "Summary"
when: "When"
if: "If"
then: "Then"
none: "None"
custom_fields:
admin:
custom_field_projects:
@@ -2807,6 +2830,7 @@ en:
principal: "User or group"
# kept for backwards compatibility
issue: "Work package"
inexistent: "Inexistent"
journal: "Journal"
journal_notes: "Comment"
lastname: "Last name"
+1 -1
View File
@@ -661,7 +661,7 @@ Rails.application.routes.draw do
end
end
resources :custom_actions, except: :show
resources :automations, except: :show
namespace :oauth do
resources :applications do
@@ -0,0 +1,71 @@
# frozen_string_literal: true
class ConvertCustomActionsToAutomations < ActiveRecord::Migration[8.1]
class MigrationAutomation < ApplicationRecord
self.table_name = "automations"
end
class MigrationTrigger < ApplicationRecord
self.table_name = "automation_triggers"
end
def up
rename_table :custom_actions, :automations
rename_habtm_table :custom_actions_statuses, :automations_statuses, :custom_action_id, :automation_id
rename_habtm_table :custom_actions_roles, :automations_roles, :custom_action_id, :automation_id
rename_habtm_table :custom_actions_types, :automations_types, :custom_action_id, :automation_id
rename_habtm_table :custom_actions_projects, :automations_projects, :custom_action_id, :automation_id
create_table :automation_triggers do |t|
t.references :automation, null: false, foreign_key: true, index: true
t.string :type, null: false
t.jsonb :options, null: false, default: {}
t.integer :position
t.timestamps
end
MigrationAutomation.reset_column_information
MigrationTrigger.reset_column_information
MigrationAutomation.find_each do |automation|
MigrationTrigger.create!(
automation_id: automation.id,
type: "Automations::Triggers::Manual",
options: { button_label: automation.name },
position: 1
)
end
end
def down
drop_table :automation_triggers
rename_habtm_table :automations_projects, :custom_actions_projects, :automation_id, :custom_action_id
rename_habtm_table :automations_types, :custom_actions_types, :automation_id, :custom_action_id
rename_habtm_table :automations_roles, :custom_actions_roles, :automation_id, :custom_action_id
rename_habtm_table :automations_statuses, :custom_actions_statuses, :automation_id, :custom_action_id
rename_table :automations, :custom_actions
end
private
def rename_habtm_table(from, to, old_fk, new_fk)
rename_table from, to
rename_column to, old_fk, new_fk
rename_fk_index(to, from, to, old_fk, new_fk)
end
def rename_fk_index(table, from_name, to_name, old_fk, new_fk)
old_index_name = "index_#{from_name}_on_#{old_fk}"
new_index_name = "index_#{to_name}_on_#{new_fk}"
if index_name_exists?(table, old_index_name)
rename_index table, old_index_name, new_index_name
elsif !index_name_exists?(table, new_index_name)
add_index table, new_fk, name: new_index_name
end
end
end
@@ -33,7 +33,7 @@ module API
link :executeImmediately do
{
href: api_v3_paths.custom_action_execute(represented.id),
title: I18n.t("custom_actions.execute", name: represented.name),
title: I18n.t("custom_actions.execute", name: name),
method: "post"
}
end
@@ -44,6 +44,10 @@ module API
property :description,
render_nil: true
def name
represented.triggers.detect { |trigger| trigger.is_a?(::Automations::Triggers::Manual) }&.button_label || represented.name
end
def _type
"CustomAction"
end
@@ -34,7 +34,7 @@ module API
route_param :id, type: Integer, desc: "Custom action ID" do
helpers do
def custom_action
@custom_action ||= CustomAction.find(params[:id])
@custom_action ||= Automation.with_manual_trigger.find(params[:id])
end
end
@@ -63,8 +63,8 @@ module API
end
after_validation do
contract = ::CustomActions::ExecuteContract.new(parsed_params, current_user,
options: { custom_action: })
contract = ::Automations::ExecuteContract.new(parsed_params, current_user,
options: { automation: custom_action })
unless contract.valid?
fail ::API::Errors::ErrorBase.create_and_merge_errors(contract.errors)
@@ -75,7 +75,7 @@ module API
work_package = WorkPackage.visible.find_by(id: parsed_params.work_package_id)
work_package.lock_version = parsed_params.lock_version
::CustomActions::UpdateWorkPackageService
::Automations::UpdateWorkPackageService
.new(user: current_user,
action: custom_action)
.call(work_package:) do |call|
@@ -30,40 +30,43 @@ module API
module V3
module WorkPackages
module EagerLoading
class CustomAction < Base
class Automation < Base
def apply(work_package)
applicable_actions = custom_actions.select do |action|
action.conditions_fulfilled?(work_package, User.current)
applicable_automations = automations.select do |automation|
automation.conditions_fulfilled?(work_package, User.current)
end
work_package.custom_actions = applicable_actions
work_package.automations = applicable_automations
end
def self.module
CustomActionAccessor
AutomationAccessor
end
private
def custom_actions
@custom_actions ||= ::CustomAction
.available_conditions
.inject(::CustomAction.all) do |scope, condition|
scope.merge(condition.custom_action_scope(work_packages, User.current))
def automations
@automations ||= ::Automation
.available_conditions
.inject(::Automation.with_manual_trigger.includes(:triggers)) do |scope, condition|
scope.merge(condition.automation_scope(work_packages, User.current))
end
end
module CustomActionAccessor
module AutomationAccessor
extend ActiveSupport::Concern
included do
attr_writer :custom_actions
attr_writer :automations
# Hiding the work_package's own custom_actions method
# to profit from the eager loaded actions
def custom_actions(_user)
@custom_actions
# Hiding the work_package's own automations method
# to profit from the eager loaded automations
def automations(_user)
@automations
end
# API compatibility for /api/v3/custom_actions
def custom_actions(user) = automations(user)
end
end
end
@@ -78,7 +78,7 @@ module API
::API::V3::WorkPackages::EagerLoading::Principals,
::API::V3::WorkPackages::EagerLoading::Checksum,
::API::V3::WorkPackages::EagerLoading::CustomValue,
::API::V3::WorkPackages::EagerLoading::CustomAction,
::API::V3::WorkPackages::EagerLoading::Automation,
# Have the historic attributes last as they require the custom values
# to be loaded first in order to create the diffs between the current
# and the historic values without loading the custom fields (Acts::Journalized::Differ).
@@ -31,13 +31,13 @@
require "spec_helper"
require "contracts/shared/model_contract_shared_context"
RSpec.describe CustomActions::CuContract do
RSpec.describe Automations::CuContract do
include_context "ModelContract shared context"
let(:user) { build_stubbed(:user) }
let(:action) do
build_stubbed(:custom_action, actions:
[CustomActions::Actions::AssignedTo.new])
build_stubbed(:automation, actions:
[Automations::Actions::AssignedTo.new])
end
let(:contract) { described_class.new(action) }
@@ -65,7 +65,7 @@ RSpec.describe CustomActions::CuContract do
describe "actions" do
it "is writable" do
responsible_action = CustomActions::Actions::Responsible.new
responsible_action = Automations::Actions::Responsible.new
action.actions = [responsible_action]
@@ -79,13 +79,13 @@ RSpec.describe CustomActions::CuContract do
end
it "requires a value if the action requires one" do
action.actions = [CustomActions::Actions::Status.new([])]
action.actions = [Automations::Actions::Status.new([])]
expect_contract_invalid actions: :empty
end
it "allows only the allowed values" do
status_action = CustomActions::Actions::Status.new([0])
status_action = Automations::Actions::Status.new([0])
allow(status_action)
.to receive(:allowed_values)
.and_return([{ value: nil, label: "-" },
@@ -97,7 +97,7 @@ RSpec.describe CustomActions::CuContract do
end
it "is not allowed to have an inexistent action" do
action.actions = [CustomActions::Actions::Inexistent.new]
action.actions = [Automations::Actions::Inexistent.new]
expect_contract_invalid actions: :does_not_exist
end
@@ -112,7 +112,7 @@ RSpec.describe CustomActions::CuContract do
end
it "allows only the allowed values" do
status_condition = CustomActions::Conditions::Status.new([0])
status_condition = Automations::Conditions::Status.new([0])
allow(status_condition)
.to receive(:allowed_values)
.and_return([{ value: nil, label: "-" },
@@ -124,7 +124,7 @@ RSpec.describe CustomActions::CuContract do
end
it "is not allowed to have an inexistent condition" do
action.conditions = [CustomActions::Conditions::Inexistent.new]
action.conditions = [Automations::Conditions::Inexistent.new]
expect_contract_invalid conditions: :does_not_exist
end
@@ -30,12 +30,12 @@
require "spec_helper"
RSpec.describe CustomActionsController, with_ee: %i[custom_actions] do
RSpec.describe AutomationsController, with_ee: %i[automations] do
let(:admin) { build(:admin) }
let(:non_admin) { build(:user) }
let(:action) { build_stubbed(:custom_action) }
let(:action) { build_stubbed(:automation) }
let(:params) do
{ custom_action: { name: "blubs",
{ automation: { name: "blubs",
actions: { assigned_to: 1 } } }
end
@@ -72,7 +72,7 @@ RSpec.describe CustomActionsController, with_ee: %i[custom_actions] do
let(:call) { get :index }
before do
allow(CustomAction)
allow(Automation)
.to receive(:order_by_position)
.and_return([action])
end
@@ -94,8 +94,8 @@ RSpec.describe CustomActionsController, with_ee: %i[custom_actions] do
.to render_template("index")
end
it "assigns the custom actions" do
expect(assigns(:custom_actions))
it "assigns the automations" do
expect(assigns(:automations))
.to contain_exactly(action)
end
end
@@ -122,7 +122,7 @@ RSpec.describe CustomActionsController, with_ee: %i[custom_actions] do
before do
login_as(admin)
allow(CustomAction)
allow(Automation)
.to receive(:new)
.and_return(action)
@@ -139,8 +139,8 @@ RSpec.describe CustomActionsController, with_ee: %i[custom_actions] do
.to render_template("new")
end
it "assigns custom_action" do
expect(assigns(:custom_action))
it "assigns automation" do
expect(assigns(:automation))
.to eql action
end
end
@@ -156,14 +156,14 @@ RSpec.describe CustomActionsController, with_ee: %i[custom_actions] do
let(:permitted_params) do
ActionController::Parameters
.new(params)
.require(:custom_action)
.require(:automation)
.permit(:name)
.merge(ActionController::Parameters.new(actions: { assigned_to: "1" }).permit!)
end
let!(:service) do
service = double("create service")
allow(CustomActions::CreateService)
allow(Automations::CreateService)
.to receive(:new)
.with(user: admin)
.and_return(service)
@@ -190,7 +190,7 @@ RSpec.describe CustomActionsController, with_ee: %i[custom_actions] do
context "on success" do
it "redirects to index" do
expect(response)
.to redirect_to(custom_actions_path)
.to redirect_to(automations_path)
end
end
@@ -203,7 +203,7 @@ RSpec.describe CustomActionsController, with_ee: %i[custom_actions] do
end
it "assigns custom action" do
expect(assigns[:custom_action])
expect(assigns[:automation])
.to eql action
end
end
@@ -222,7 +222,7 @@ RSpec.describe CustomActionsController, with_ee: %i[custom_actions] do
end
before do
allow(CustomAction)
allow(Automation)
.to receive(:find)
.with(params[:id])
.and_return(action)
@@ -245,15 +245,15 @@ RSpec.describe CustomActionsController, with_ee: %i[custom_actions] do
.to render_template("edit")
end
it "assigns custom_action" do
expect(assigns(:custom_action))
it "assigns automation" do
expect(assigns(:automation))
.to eql action
end
end
context "for admins on invalid id" do
before do
allow(CustomAction)
allow(Automation)
.to receive(:find)
.with(params[:id])
.and_raise(ActiveRecord::RecordNotFound)
@@ -280,19 +280,19 @@ RSpec.describe CustomActionsController, with_ee: %i[custom_actions] do
let(:permitted_params) do
ActionController::Parameters
.new(params)
.require(:custom_action)
.require(:automation)
.permit(:name)
.merge(ActionController::Parameters.new(actions: { assigned_to: "1" }).permit!)
end
let(:params) do
{ custom_action: { name: "blubs",
{ automation: { name: "blubs",
actions: { assigned_to: 1 } },
id: "42" }
end
let!(:service) do
service = double("update service")
allow(CustomActions::UpdateService)
allow(Automations::UpdateService)
.to receive(:new)
.with(user: admin, action:)
.and_return(service)
@@ -310,7 +310,7 @@ RSpec.describe CustomActionsController, with_ee: %i[custom_actions] do
end
before do
allow(CustomAction)
allow(Automation)
.to receive(:find)
.with(params[:id])
.and_return(action)
@@ -326,7 +326,7 @@ RSpec.describe CustomActionsController, with_ee: %i[custom_actions] do
context "on success" do
it "redirects to index" do
expect(response)
.to redirect_to(custom_actions_path)
.to redirect_to(automations_path)
end
end
@@ -339,7 +339,7 @@ RSpec.describe CustomActionsController, with_ee: %i[custom_actions] do
end
it "assigns the action" do
expect(assigns[:custom_action])
expect(assigns[:automation])
.to eql(action)
end
end
@@ -347,7 +347,7 @@ RSpec.describe CustomActionsController, with_ee: %i[custom_actions] do
context "for admins on invalid id" do
before do
allow(CustomAction)
allow(Automation)
.to receive(:find)
.with(params[:id])
.and_raise(ActiveRecord::RecordNotFound)
@@ -375,7 +375,7 @@ RSpec.describe CustomActionsController, with_ee: %i[custom_actions] do
end
before do
allow(CustomAction)
allow(Automation)
.to receive(:find)
.with(params[:id])
.and_return(action)
@@ -394,13 +394,13 @@ RSpec.describe CustomActionsController, with_ee: %i[custom_actions] do
it "redirects to index" do
expect(response)
.to redirect_to(custom_actions_path)
.to redirect_to(automations_path)
end
end
context "for admins on invalid id" do
before do
allow(CustomAction)
allow(Automation)
.to receive(:find)
.with(params[:id])
.and_raise(ActiveRecord::RecordNotFound)
@@ -29,8 +29,23 @@
#++
FactoryBot.define do
factory :custom_action do
factory :automation do
sequence(:name) { |n| "Custom action #{n} - name" }
sequence(:description) { |n| "Custom action #{n} - description" }
after(:build) do |automation|
next if automation.triggers.any? { |trigger| trigger.is_a?(Automations::Triggers::Manual) }
automation.triggers << build(:manual_trigger, automation:)
end
trait :with_manual_trigger
end
factory :manual_trigger, class: "Automations::Triggers::Manual" do
automation
button_label { "Run automation" }
end
factory :automation_with_manual_trigger, parent: :automation
end
@@ -30,7 +30,7 @@
require "spec_helper"
RSpec.describe "Custom actions link cf value", :js, with_ee: %i[custom_actions] do
RSpec.describe "Automations link cf value", :js, with_ee: %i[automations] do
shared_let(:admin) { create(:admin) }
let(:permissions) { %i(view_work_packages edit_work_packages) }
@@ -51,7 +51,7 @@ RSpec.describe "Custom actions link cf value", :js, with_ee: %i[custom_actions]
let(:default_priority) do
create(:default_priority, name: "Normal")
end
let(:new_ca_page) { Pages::Admin::CustomActions::New.new }
let(:new_ca_page) { Pages::Admin::Automations::New.new }
before do
login_as(admin)
@@ -65,7 +65,7 @@ RSpec.describe "Custom actions link cf value", :js, with_ee: %i[custom_actions]
new_ca_page.create
assign = CustomAction.last
assign = Automation.last
expect(assign.actions.length).to eq(1)
expect(assign.conditions.length).to eq(0)
expect(assign.actions.first.values).to eq(["https://example.com"])
@@ -73,8 +73,8 @@ RSpec.describe "Custom actions link cf value", :js, with_ee: %i[custom_actions]
login_as user
wp_page.visit!
wp_page.expect_custom_action("Set Link CF")
wp_page.click_custom_action("Set Link CF")
wp_page.expect_automation("Set Link CF")
wp_page.click_automation("Set Link CF")
wp_page.expect_attributes "customField#{custom_field.id}": "https://example.com"
end
end
@@ -30,7 +30,7 @@
require "spec_helper"
RSpec.describe "Custom actions me value", :js, with_ee: %i[custom_actions] do
RSpec.describe "Automations me value", :js, with_ee: %i[automations] do
shared_let(:admin) { create(:admin) }
let(:permissions) { %i(view_work_packages edit_work_packages) }
@@ -51,7 +51,7 @@ RSpec.describe "Custom actions me value", :js, with_ee: %i[custom_actions] do
let(:default_priority) do
create(:default_priority, name: "Normal")
end
let(:index_ca_page) { Pages::Admin::CustomActions::Index.new }
let(:index_ca_page) { Pages::Admin::Automations::Index.new }
before do
login_as(admin)
@@ -63,11 +63,11 @@ RSpec.describe "Custom actions me value", :js, with_ee: %i[custom_actions] do
new_ca_page = index_ca_page.new
new_ca_page.set_name("Set CF to me")
new_ca_page.add_action(custom_field.name, I18n.t("custom_actions.actions.assigned_to.executing_user_value"))
new_ca_page.add_action(custom_field.name, I18n.t("automations.actions.assigned_to.executing_user_value"))
new_ca_page.create
assign = CustomAction.last
assign = Automation.last
expect(assign.actions.length).to eq(1)
expect(assign.conditions.length).to eq(0)
expect(assign.actions.first.values).to eq(["current_user"])
@@ -75,8 +75,8 @@ RSpec.describe "Custom actions me value", :js, with_ee: %i[custom_actions] do
login_as user
wp_page.visit!
wp_page.expect_custom_action("Set CF to me")
wp_page.click_custom_action("Set CF to me")
wp_page.expect_automation("Set CF to me")
wp_page.click_automation("Set CF to me")
wp_page.expect_attributes "customField#{custom_field.id}": user.name
end
end
@@ -30,7 +30,7 @@
require "spec_helper"
RSpec.describe "Custom actions", :js, with_ee: %i[custom_actions] do
RSpec.describe "Automations", :js, with_ee: %i[automations] do
shared_let(:admin) { create(:admin) }
shared_let(:permissions) { %i(view_work_packages edit_work_packages move_work_packages work_package_assigned) }
@@ -135,7 +135,7 @@ RSpec.describe "Custom actions", :js, with_ee: %i[custom_actions] do
work_package.type.custom_fields << cf
end
end
let(:index_ca_page) { Pages::Admin::CustomActions::Index.new }
let(:index_ca_page) { Pages::Admin::Automations::Index.new }
let(:activity_tab) { Components::WorkPackages::Activities.new(work_package) }
before do
@@ -161,7 +161,7 @@ RSpec.describe "Custom actions", :js, with_ee: %i[custom_actions] do
index_ca_page.expect_current_path
index_ca_page.expect_listed("Unassign")
unassign = CustomAction.last
unassign = Automation.last
expect(unassign.actions.length).to eq(1)
expect(unassign.conditions.length).to eq(0)
@@ -186,7 +186,7 @@ RSpec.describe "Custom actions", :js, with_ee: %i[custom_actions] do
index_ca_page.expect_current_path
index_ca_page.expect_listed("Unassign", "Close")
close = CustomAction.last
close = Automation.last
expect(close.actions.length).to eq(1)
expect(close.conditions.length).to eq(2)
@@ -210,7 +210,7 @@ RSpec.describe "Custom actions", :js, with_ee: %i[custom_actions] do
index_ca_page.expect_current_path
index_ca_page.expect_listed("Unassign", "Close", "Escalate")
escalate = CustomAction.last
escalate = Automation.last
expect(escalate.actions.length).to eq(3)
expect(escalate.conditions.length).to eq(0)
@@ -241,7 +241,7 @@ RSpec.describe "Custom actions", :js, with_ee: %i[custom_actions] do
index_ca_page.expect_current_path
index_ca_page.expect_listed("Unassign", "Close", "Escalate", "Reset")
reset = CustomAction.last
reset = Automation.last
expect(reset.actions.length).to eq(4)
expect(reset.conditions.length).to eq(1)
@@ -261,7 +261,7 @@ RSpec.describe "Custom actions", :js, with_ee: %i[custom_actions] do
index_ca_page.expect_current_path
index_ca_page.expect_listed("Unassign", "Close", "Escalate", "Reset", "Other roles action")
other_roles_action = CustomAction.last
other_roles_action = Automation.last
expect(other_roles_action.actions.length).to eq(1)
expect(other_roles_action.conditions.length).to eq(1)
@@ -282,7 +282,7 @@ RSpec.describe "Custom actions", :js, with_ee: %i[custom_actions] do
end
date = (Date.current + 5.days)
find("#custom_action_actions_custom_field_#{date_custom_field.id}_visible").click
find("#automation_actions_custom_field_#{date_custom_field.id}_visible").click
datepicker = Components::Datepicker.new "body"
datepicker.set_date date
@@ -301,7 +301,7 @@ RSpec.describe "Custom actions", :js, with_ee: %i[custom_actions] do
index_ca_page.expect_current_path
index_ca_page.expect_listed("Unassign", "Close", "Escalate", "Reset", "Other roles action", "Move project")
move_project = CustomAction.last
move_project = Automation.last
expect(move_project.actions.length).to eq(3)
expect(move_project.conditions.length).to eq(1)
@@ -310,13 +310,13 @@ RSpec.describe "Custom actions", :js, with_ee: %i[custom_actions] do
wp_page.visit!
wp_page.expect_custom_action("Unassign")
wp_page.expect_custom_action("Close")
wp_page.expect_custom_action("Escalate")
wp_page.expect_custom_action("Move project")
wp_page.expect_no_custom_action("Reset")
wp_page.expect_no_custom_action("Other roles action")
wp_page.expect_custom_action_order("Unassign", "Close", "Escalate", "Move project")
wp_page.expect_automation("Unassign")
wp_page.expect_automation("Close")
wp_page.expect_automation("Escalate")
wp_page.expect_automation("Move project")
wp_page.expect_no_automation("Reset")
wp_page.expect_no_automation("Other roles action")
wp_page.expect_automation_order("Unassign", "Close", "Escalate", "Move project")
within(".custom-actions") do
# When hovering over the button, the description is displayed
@@ -324,11 +324,11 @@ RSpec.describe "Custom actions", :js, with_ee: %i[custom_actions] do
.to have_button("Unassign", title: "Removes the assignee")
end
wp_page.click_custom_action("Unassign")
wp_page.click_automation("Unassign")
wp_page.expect_attributes assignee: "-"
activity_tab.expect_journal_details_header(text: user.name)
wp_page.click_custom_action("Escalate")
wp_page.click_automation("Escalate")
wp_page.expect_attributes priority: immediate_priority.name,
status: default_status.name,
assignee: "-",
@@ -336,14 +336,14 @@ RSpec.describe "Custom actions", :js, with_ee: %i[custom_actions] do
activity_tab.expect_journal_mention(text: other_member_user.name)
wp_page.click_custom_action("Close")
wp_page.click_automation("Close")
wp_page.expect_attributes status: closed_status.name,
priority: immediate_priority.name
wp_page.expect_custom_action("Reset")
wp_page.expect_no_custom_action("Close")
wp_page.expect_automation("Reset")
wp_page.expect_no_automation("Close")
wp_page.click_custom_action("Reset")
wp_page.click_automation("Reset")
wp_page.expect_attributes priority: default_priority.name,
status: default_status.name,
@@ -375,7 +375,7 @@ RSpec.describe "Custom actions", :js, with_ee: %i[custom_actions] do
index_ca_page.expect_current_path
index_ca_page.expect_listed("Unassign", "Close", "Escalate", "Reject")
reset = CustomAction.find_by(name: "Reject")
reset = Automation.find_by(name: "Reject")
expect(reset.actions.length).to eq(3)
expect(reset.conditions.length).to eq(1)
@@ -389,21 +389,21 @@ RSpec.describe "Custom actions", :js, with_ee: %i[custom_actions] do
wp_page.visit!
wp_page.expect_custom_action("Unassign")
wp_page.expect_custom_action("Close")
wp_page.expect_custom_action("Escalate")
wp_page.expect_custom_action("Move project")
wp_page.expect_custom_action("Reject")
wp_page.expect_no_custom_action("Reset")
wp_page.expect_custom_action_order("Move project", "Close", "Reject", "Unassign", "Escalate")
wp_page.expect_automation("Unassign")
wp_page.expect_automation("Close")
wp_page.expect_automation("Escalate")
wp_page.expect_automation("Move project")
wp_page.expect_automation("Reject")
wp_page.expect_no_automation("Reset")
wp_page.expect_automation_order("Move project", "Close", "Reject", "Unassign", "Escalate")
wp_page.click_custom_action("Reject")
wp_page.click_automation("Reject")
wp_page.expect_attributes assignee: "-",
status: rejected_status.name,
priority: default_priority.name
wp_page.expect_custom_action("Close")
wp_page.expect_no_custom_action("Reject")
wp_page.expect_automation("Close")
wp_page.expect_no_automation("Reject")
# Delete 'Reject' from list of actions
login_as(admin)
@@ -419,13 +419,13 @@ RSpec.describe "Custom actions", :js, with_ee: %i[custom_actions] do
wp_page.visit!
wp_page.expect_no_custom_action("Unassign")
wp_page.expect_custom_action("Close")
wp_page.expect_custom_action("Escalate")
wp_page.expect_no_custom_action("Reject")
wp_page.expect_no_automation("Unassign")
wp_page.expect_automation("Close")
wp_page.expect_automation("Escalate")
wp_page.expect_no_automation("Reject")
# Move project
wp_page.click_custom_action("Move project")
wp_page.click_automation("Move project")
wp_page.expect_attributes assignee: "-",
status: rejected_status.name,
@@ -437,7 +437,7 @@ RSpec.describe "Custom actions", :js, with_ee: %i[custom_actions] do
## Bump the lockVersion and by that force a conflict.
work_package.reload.touch
wp_page.click_custom_action("Escalate", expect_success: false)
wp_page.click_automation("Escalate", expect_success: false)
wp_page.expect_conflict_error_banner
end
@@ -457,12 +457,12 @@ RSpec.describe "Custom actions", :js, with_ee: %i[custom_actions] do
index_ca_page.expect_current_path
index_ca_page.expect_listed("Current date")
date_action = CustomAction.last
date_action = Automation.last
expect(date_action.actions.length).to eq(1)
expect(date_action.conditions.length).to eq(0)
index_ca_page.edit("Current date")
expect(page).to have_select("custom_action_actions_date", selected: "Current date")
expect(page).to have_select("automation_actions_date", selected: "Current date")
end
it "editing a status custom action (Regression #61888)" do
@@ -480,12 +480,12 @@ RSpec.describe "Custom actions", :js, with_ee: %i[custom_actions] do
index_ca_page.expect_current_path
index_ca_page.expect_listed("Status")
date_action = CustomAction.last
date_action = Automation.last
expect(date_action.actions.length).to eq(1)
expect(date_action.actions.first.value_objects).to contain_exactly(label: "Closed", value: closed_status.id)
edit_page = index_ca_page.edit("Status")
page.within "#custom-actions-form--actions" do
page.within "#automations-form--actions" do
edit_page.expect_selected_option "Close"
end
end
@@ -505,7 +505,7 @@ RSpec.describe "Custom actions", :js, with_ee: %i[custom_actions] do
index_ca_page.expect_current_path
index_ca_page.expect_listed("Unassign")
unassign = CustomAction.last
unassign = Automation.last
expect(unassign.actions.length).to eq(1)
expect(unassign.conditions.length).to eq(0)
@@ -515,13 +515,13 @@ RSpec.describe "Custom actions", :js, with_ee: %i[custom_actions] do
wp_page.ensure_page_loaded
wait_for_network_idle
wp_page.expect_custom_action("Unassign")
wp_page.expect_automation("Unassign")
# Stop sending ajax requests in order to test disabled fields upon submit
wp_page.disable_ajax_requests
wp_page.click_custom_action("Unassign", expect_success: false)
wp_page.expect_custom_action_disabled("Unassign")
wp_page.click_automation("Unassign", expect_success: false)
wp_page.expect_automation_disabled("Unassign")
find('[data-field-name="estimatedTime"]').click
expect(page).to have_css("#wp-#{work_package.id}-inline-edit--field-estimatedTime[disabled]")
end
@@ -537,8 +537,8 @@ RSpec.describe "Custom actions", :js, with_ee: %i[custom_actions] do
end
before do
create(:custom_action,
actions: [CustomActions::Actions::AssignedTo.new(value: nil)],
create(:automation,
actions: [Automations::Actions::AssignedTo.new(value: nil)],
name: "Unassign")
end
@@ -549,7 +549,7 @@ RSpec.describe "Custom actions", :js, with_ee: %i[custom_actions] do
wp_page.ensure_page_loaded
wp_page.click_custom_action("Unassign", expect_success: true)
wp_page.click_automation("Unassign", expect_success: true)
end
end
+3 -4
View File
@@ -50,14 +50,13 @@ RSpec.describe NoResultsHelper do
expect(no_results_box).to have_link "Add some foo", href: "/"
end
it "contains title and content_link with custom text" do
it "contains title with custom text" do
no_results_box = helper.no_results_box(action_url: root_path,
display_action: true,
custom_title: "This is a different title about foo",
custom_action_text: "Link to nowhere")
custom_title: "This is a different title about foo")
expect(no_results_box).to have_content "This is a different title about foo"
expect(no_results_box).to have_link "Link to nowhere", href: "/"
expect(no_results_box).to have_link "Add some foo", href: "/"
end
end
end
@@ -33,11 +33,11 @@ require "spec_helper"
RSpec.describe API::V3::CustomActions::CustomActionRepresenter do
include API::V3::Utilities::PathHelper
let(:custom_action) { build_stubbed(:custom_action) }
let(:automation) { build_stubbed(:automation) }
let(:user) { build_stubbed(:user) }
let(:representer) do
described_class.new(custom_action, current_user: user, embed_links: true)
described_class.new(automation, current_user: user, embed_links: true)
end
subject { representer.to_json }
@@ -51,13 +51,13 @@ RSpec.describe API::V3::CustomActions::CustomActionRepresenter do
it "has a name property" do
expect(subject)
.to be_json_eql(custom_action.name.to_json)
.to be_json_eql(automation.name.to_json)
.at_path("name")
end
it "has a description property" do
expect(subject)
.to be_json_eql(custom_action.description.to_json)
.to be_json_eql(automation.description.to_json)
.at_path("description")
end
end
@@ -65,14 +65,14 @@ RSpec.describe API::V3::CustomActions::CustomActionRepresenter do
context "links" do
it_behaves_like "has a titled link" do
let(:link) { "self" }
let(:href) { api_v3_paths.custom_action(custom_action.id) }
let(:title) { custom_action.name }
let(:href) { api_v3_paths.custom_action(automation.id) }
let(:title) { automation.name }
end
it_behaves_like "has a titled link" do
let(:link) { "executeImmediately" }
let(:href) { api_v3_paths.custom_action_execute(custom_action.id) }
let(:title) { "Execute #{custom_action.name}" }
let(:href) { api_v3_paths.custom_action_execute(automation.id) }
let(:title) { "Execute #{automation.name}" }
let(:method) { "post" }
end
end
@@ -31,7 +31,7 @@
require "spec_helper"
require_relative "eager_loading_mock_wrapper"
RSpec.describe API::V3::WorkPackages::EagerLoading::CustomAction do
RSpec.describe API::V3::WorkPackages::EagerLoading::Automation do
let!(:work_package1) { create(:work_package) }
let!(:work_package2) { create(:work_package) }
let!(:user) do
@@ -39,13 +39,13 @@ RSpec.describe API::V3::WorkPackages::EagerLoading::CustomAction do
member_with_roles: { work_package2.project => role })
end
let!(:role) { create(:project_role) }
let!(:status_custom_action) do
create(:custom_action,
conditions: [CustomActions::Conditions::Status.new(work_package1.status_id.to_s)])
let!(:status_automation) do
create(:automation,
conditions: [Automations::Conditions::Status.new(work_package1.status_id.to_s)])
end
let!(:role_custom_action) do
create(:custom_action,
conditions: [CustomActions::Conditions::Role.new(role.id)])
let!(:role_automation) do
create(:automation,
conditions: [Automations::Conditions::Role.new(role.id)])
end
before do
@@ -53,19 +53,19 @@ RSpec.describe API::V3::WorkPackages::EagerLoading::CustomAction do
end
describe ".apply" do
it "preloads the correct custom_actions" do
it "preloads the correct automations" do
wrapped = EagerLoadingMockWrapper.wrap(described_class, [work_package1, work_package2])
expect(work_package1)
.not_to receive(:custom_actions)
.not_to receive(:automations)
expect(work_package2)
.not_to receive(:custom_actions)
.not_to receive(:automations)
expect(wrapped.detect { |w| w.id == work_package1.id }.custom_actions(user))
.to contain_exactly(status_custom_action)
expect(wrapped.detect { |w| w.id == work_package1.id }.automations(user))
.to contain_exactly(status_automation)
expect(wrapped.detect { |w| w.id == work_package2.id }.custom_actions(user))
.to contain_exactly(role_custom_action)
expect(wrapped.detect { |w| w.id == work_package2.id }.automations(user))
.to contain_exactly(role_automation)
end
end
end
@@ -1381,16 +1381,16 @@ RSpec.describe API::V3::WorkPackages::WorkPackageRepresenter do
describe "customActions" do
it "has a collection of customActions" do
unassign_action = build_stubbed(:custom_action,
actions: [CustomActions::Actions::AssignedTo.new(value: nil)],
unassign_action = build_stubbed(:automation,
actions: [Automations::Actions::AssignedTo.new(value: nil)],
name: "Unassign")
allow(work_package)
.to receive(:custom_actions)
.to receive(:automations)
.and_return([unassign_action])
expected = [
{
href: api_v3_paths.custom_action(unassign_action.id),
href: api_v3_paths.automation(unassign_action.id),
title: unassign_action.name
}
]
@@ -1496,11 +1496,11 @@ RSpec.describe API::V3::WorkPackages::WorkPackageRepresenter do
describe "customActions" do
it "has an array of customActions" do
unassign_action = build_stubbed(:custom_action,
actions: [CustomActions::Actions::AssignedTo.new(value: nil)],
unassign_action = build_stubbed(:automation,
actions: [Automations::Actions::AssignedTo.new(value: nil)],
name: "Unassign")
allow(work_package)
.to receive(:custom_actions)
.to receive(:automations)
.and_return([unassign_action])
expect(subject)
@@ -30,10 +30,10 @@
require "spec_helper"
RSpec.describe CustomAction do
let(:stubbed_instance) { build_stubbed(:custom_action) }
let(:instance) { create(:custom_action, name: "zzzzzzzzz") }
let(:other_instance) { create(:custom_action, name: "aaaaa") }
RSpec.describe Automation do
let(:stubbed_instance) { build_stubbed(:automation) }
let(:instance) { create(:automation, name: "zzzzzzzzz") }
let(:other_instance) { create(:automation, name: "aaaaa") }
describe "#name" do
it "can be set and read" do
@@ -95,18 +95,18 @@ RSpec.describe CustomAction do
end
it "can be set and read" do
stubbed_instance.actions = [CustomActions::Actions::AssignedTo.new(1)]
stubbed_instance.actions = [Automations::Actions::AssignedTo.new(1)]
expect(stubbed_instance.actions.map { |a| [a.key, a.values] })
.to contain_exactly([:assigned_to, [1]])
end
it "can be persisted" do
instance.actions = [CustomActions::Actions::AssignedTo.new(1)]
instance.actions = [Automations::Actions::AssignedTo.new(1)]
instance.save!
expect(CustomAction.find(instance.id).actions.map { |a| [a.key, a.values] })
expect(Automation.find(instance.id).actions.map { |a| [a.key, a.values] })
.to contain_exactly([:assigned_to, [1]])
end
end
@@ -118,7 +118,7 @@ RSpec.describe CustomAction do
end
it "returns the activated actions with their selected value and all other with the default value" do
stubbed_instance.actions = [CustomActions::Actions::AssignedTo.new(1)]
stubbed_instance.actions = [Automations::Actions::AssignedTo.new(1)]
expect(stubbed_instance.all_actions.map { |a| [a.key, a.values] })
.to include([:assigned_to, [1]], [:status, []])
@@ -149,25 +149,25 @@ RSpec.describe CustomAction do
end
it "can be set and read" do
stubbed_instance.conditions = [CustomActions::Conditions::Status.new(status.id),
CustomActions::Conditions::Role.new(role.id)]
stubbed_instance.conditions = [Automations::Conditions::Status.new(status.id),
Automations::Conditions::Role.new(role.id)]
expect(stubbed_instance.conditions.map { |a| [a.key, a.values] })
.to contain_exactly([:status, [status.id]], [:role, [role.id]])
end
it "can be persisted" do
instance.conditions = [CustomActions::Conditions::Status.new(status.id),
CustomActions::Conditions::Role.new(role.id)]
instance.conditions = [Automations::Conditions::Status.new(status.id),
Automations::Conditions::Role.new(role.id)]
instance.save!
expect(CustomAction.find(instance.id).conditions.map { |a| [a.key, a.values] })
expect(Automation.find(instance.id).conditions.map { |a| [a.key, a.values] })
.to contain_exactly([:status, [status.id]], [:role, [role.id]])
end
it "existing permissions can be removed" do
instance.conditions = [CustomActions::Conditions::Project.new(project.id)]
instance.conditions = [Automations::Conditions::Project.new(project.id)]
instance.save!
@@ -175,7 +175,7 @@ RSpec.describe CustomAction do
instance.save!
expect(CustomAction.find(instance.id).conditions.map { |a| [a.key, a.values] })
expect(Automation.find(instance.id).conditions.map { |a| [a.key, a.values] })
.to be_empty
end
end
@@ -30,7 +30,7 @@
require "spec_helper"
require_relative "../shared_expectations"
RSpec.describe CustomActions::Actions::AssignedTo do
RSpec.describe Automations::Actions::AssignedTo do
let(:key) { :assigned_to }
let(:type) { :user }
let(:allowed_values) do
@@ -74,7 +74,7 @@ RSpec.describe CustomActions::Actions::AssignedTo do
it "includes the value in available_values" do
expect(subject.associated)
.to include([subject.current_user_value_key, I18n.t("custom_actions.actions.assigned_to.executing_user_value")])
.to include([subject.current_user_value_key, I18n.t("automations.actions.assigned_to.executing_user_value")])
end
context "when logged in" do
@@ -88,7 +88,7 @@ RSpec.describe CustomActions::Actions::AssignedTo do
end
it "validates the me value when executing" do
errors = ActiveModel::Errors.new(CustomAction.new)
errors = ActiveModel::Errors.new(Automation.new)
subject.validate errors
expect(errors.symbols_for(:actions)).to be_empty
end
@@ -101,7 +101,7 @@ RSpec.describe CustomActions::Actions::AssignedTo do
end
it "validates the me value when executing" do
errors = ActiveModel::Errors.new(CustomAction.new)
errors = ActiveModel::Errors.new(Automation.new)
subject.validate errors
expect(errors.symbols_for(:actions)).to include :not_logged_in
end
@@ -30,7 +30,7 @@
require "spec_helper"
require_relative "../shared_expectations"
RSpec.describe CustomActions::Actions::CustomField do
RSpec.describe Automations::Actions::CustomField do
let(:scope) { instance_double(ActiveRecord::Relation) }
let(:list_custom_field) do
build_stubbed(:list_wp_custom_field,
@@ -101,7 +101,7 @@ RSpec.describe CustomActions::Actions::CustomField do
describe ".all" do
before do
allow(WorkPackageCustomField)
.to receive(:usable_as_custom_action)
.to receive(:usable_as_automation)
.and_return(custom_fields)
end
@@ -282,7 +282,7 @@ RSpec.describe CustomActions::Actions::CustomField do
it "includes the value in available_values" do
expect(instance.associated)
.to include([instance.current_user_value_key, I18n.t("custom_actions.actions.assigned_to.executing_user_value")])
.to include([instance.current_user_value_key, I18n.t("automations.actions.assigned_to.executing_user_value")])
end
context "when logged in" do
@@ -296,7 +296,7 @@ RSpec.describe CustomActions::Actions::CustomField do
end
it "validates the me value when executing" do
errors = ActiveModel::Errors.new(CustomAction.new)
errors = ActiveModel::Errors.new(Automation.new)
instance.validate errors
expect(errors.symbols_for(:actions)).to be_empty
end
@@ -313,7 +313,7 @@ RSpec.describe CustomActions::Actions::CustomField do
end
it "validates the me value when executing" do
errors = ActiveModel::Errors.new(CustomAction.new)
errors = ActiveModel::Errors.new(Automation.new)
instance.validate errors
expect(errors.symbols_for(:actions)).to include :not_logged_in
end
@@ -31,7 +31,7 @@
require "spec_helper"
require_relative "../shared_expectations"
RSpec.describe CustomActions::Actions::Date do
RSpec.describe Automations::Actions::Date do
let(:key) { :date }
let(:type) { :date_property }
let(:value) { Date.today }
@@ -31,7 +31,7 @@
require "spec_helper"
require_relative "../shared_expectations"
RSpec.describe CustomActions::Actions::DoneRatio do
RSpec.describe Automations::Actions::DoneRatio do
let(:key) { :done_ratio }
let(:type) { :integer_property }
@@ -58,7 +58,7 @@ RSpec.describe CustomActions::Actions::DoneRatio do
describe "validate" do
let(:errors) do
build_stubbed(:custom_action).errors
build_stubbed(:automation).errors
end
it "is valid for values between 0 and 100" do
@@ -31,7 +31,7 @@
require "spec_helper"
require_relative "../shared_expectations"
RSpec.describe CustomActions::Actions::DueDate do
RSpec.describe Automations::Actions::DueDate do
let(:key) { :due_date }
let(:type) { :date_property }
let(:value) { Date.today }
@@ -31,7 +31,7 @@
require "spec_helper"
require_relative "../shared_expectations"
RSpec.describe CustomActions::Actions::EstimatedHours do
RSpec.describe Automations::Actions::EstimatedHours do
let(:key) { :estimated_hours }
let(:type) { :float_property }
let(:value) { 1.0 }
@@ -59,7 +59,7 @@ RSpec.describe CustomActions::Actions::EstimatedHours do
describe "validate" do
let(:errors) do
build_stubbed(:custom_action).errors
build_stubbed(:automation).errors
end
it "is valid for values equal to or greater than 0" do
@@ -30,7 +30,7 @@
require "spec_helper"
require_relative "../shared_expectations"
RSpec.describe CustomActions::Actions::Notify do
RSpec.describe Automations::Actions::Notify do
let(:key) { :notify }
let(:type) { :associated_property }
let(:allowed_values) do
@@ -30,7 +30,7 @@
require "spec_helper"
require_relative "../shared_expectations"
RSpec.describe CustomActions::Actions::Priority do
RSpec.describe Automations::Actions::Priority do
let(:key) { :priority }
let(:type) { :associated_property }
let(:allowed_values) do

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