lib/avo/base_action.rb



module Avo
  class BaseAction
    include Avo::Concerns::HasFields

    class_attribute :name, default: nil
    class_attribute :message
    class_attribute :confirm_button_label
    class_attribute :cancel_button_label
    class_attribute :no_confirmation, default: false
    class_attribute :model
    class_attribute :view
    class_attribute :user
    class_attribute :resource
    class_attribute :standalone, default: false
    class_attribute :visible
    class_attribute :may_download_file, default: false

    attr_accessor :response
    attr_accessor :model
    attr_accessor :resource
    attr_accessor :user
    attr_reader :arguments

    delegate :view, to: :class
    delegate :context, to: ::Avo::App
    delegate :current_user, to: ::Avo::App
    delegate :params, to: ::Avo::App
    delegate :view_context, to: ::Avo::App
    delegate :avo, to: :view_context
    delegate :main_app, to: :view_context

    class << self
      delegate :context, to: ::Avo::App

      def form_data_attributes
        # We can't respond with a file download from Turbo se we disable it on the form
        if may_download_file
          {turbo: false, remote: false, action_target: :form}
        else
          {turbo_frame: :_top, action_target: :form}
        end
      end

      # We can't respond with a file download from Turbo se we disable close the modal manually after a while (it's a hack, we know)
      def submit_button_data_attributes
        if may_download_file
          {action: "click->modal#delayedClose"}
        else
          {}
        end
      end
    end

    def action_name
      return name if name.present?

      self.class.to_s.demodulize.underscore.humanize(keep_id_suffix: true)
    end

    def initialize(model: nil, resource: nil, user: nil, view: nil, arguments: {})
      self.class.model = model
      self.class.resource = resource
      self.class.user = user
      self.class.view = view
      @arguments = arguments

      self.class.message ||= I18n.t("avo.are_you_sure_you_want_to_run_this_option")
      self.class.confirm_button_label ||= I18n.t("avo.run")
      self.class.cancel_button_label ||= I18n.t("avo.cancel")

      @response ||= {}
      @response[:messages] = []
    end

    def get_message
      if self.class.message.respond_to? :call
        Avo::Hosts::ResourceRecordHost.new(block: self.class.message, record: self.class.model, resource: self.class.resource).handle
      else
        self.class.message
      end
    end

    def get_attributes_for_action
      get_fields.map do |field|
        default_value = if field.default.respond_to? :call
          Avo::Hosts::ResourceViewRecordHost.new(block: field.default, record: self.class.model, view: view, resource: self.class.resource).handle
        else
          field.default
        end
        [field.id, field.value || default_value]
      end.to_h
    end

    def handle_action(**args)
      models, fields, current_user, resource = args.values_at(:models, :fields, :current_user, :resource)
      # Fetching the field definitions and not the actual fields (get_fields) because they will break if the user uses a `visible` block and adds a condition using the `params` variable. The params are different in the show method and the handle method.
      action_fields = get_field_definitions.map { |field| [field.id, field] }.to_h

      # For some fields, like belongs_to, the id and database_id differ (user vs user_id).
      # That's why we need to fetch the database_id for when we process the action.
      action_fields_by_database_id = action_fields.map do |id, value|
        [value.database_id.to_sym, value]
      end.to_h

      if fields.present?
        processed_fields = fields.to_unsafe_h.map do |name, value|
          field = action_fields_by_database_id[name.to_sym]

          next if field.blank?

          [name, field.resolve_attribute(value)]
        end

        processed_fields = processed_fields.reject(&:blank?).to_h
      else
        processed_fields = {}
      end

      args = {
        fields: processed_fields.with_indifferent_access,
        current_user: current_user,
        resource: resource
      }

      args[:models] = models unless standalone

      handle(**args)

      self
    end

    def visible_in_view(parent_resource: nil)
      if visible.blank?
        # Hide on the :new view by default
        return false if view == :new

        # Show on all other views
        return true
      end

      # Run the visible block if available
      Avo::Hosts::VisibilityHost.new(
        block: visible,
        params: params,
        parent_resource: parent_resource,
        resource: self.class.resource,
        view: self.class.view,
        arguments: arguments
      ).handle
    end

    def param_id
      self.class.to_s
    end

    def succeed(text)
      add_message text, :success

      self
    end

    def fail(text)
      Rails.logger.warn "DEPRECATION WARNING: Action fail method is deprecated in favor of error method and will be removed from Avo version 3.0.0"

      error text
    end

    def error(text)
      add_message text, :error

      self
    end

    def inform(text)
      add_message text, :info

      self
    end

    def warn(text)
      add_message text, :warning

      self
    end

    def keep_modal_open
      response[:keep_modal_open] = true

      self
    end

    # Add a placeholder silent message from when a user wants to do a redirect action or something similar
    def silent
      add_message nil, :silent

      self
    end

    def redirect_to(path = nil, &block)
      response[:type] = :redirect
      response[:path] = if block.present?
        block
      else
        path
      end

      self
    end

    def reload
      response[:type] = :reload

      self
    end

    def download(path, filename)
      response[:type] = :download
      response[:path] = path
      response[:filename] = filename

      self
    end

    # We're overriding this method to hydrate with the proper resource attribute.
    def hydrate_fields(model: nil, view: nil)
      fields.map do |field|
        field.hydrate(model: @model, view: @view, resource: resource)
      end

      self
    end

    private

    def add_message(body, type = :info)
      response[:messages] << {
        type: type,
        body: body
      }
    end
  end
end