lib/avo/base_action.rb

Summary

Maintainability
A
3 hrs
Test Coverage
module Avo
  class BaseAction
    include Avo::Concerns::HasItems
    include Avo::Concerns::HasActionStimulusControllers

    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 :standalone, default: false
    class_attribute :visible
    class_attribute :may_download_file
    class_attribute :turbo
    class_attribute :authorize, default: true

    attr_accessor :view
    attr_accessor :response
    attr_accessor :record
    attr_accessor :resource
    attr_accessor :user
    attr_reader :arguments
    attr_reader :icon

    # TODO: find a differnet way to delegate this to the uninitialized Current variable
    delegate :context, to: Avo::Current
    def current_user
      Avo::Current.user
    end
    delegate :params, to: Avo::Current
    delegate :view_context, to: Avo::Current
    delegate :avo, to: :view_context
    delegate :main_app, to: :view_context
    delegate :to_param, to: :class

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

      def form_data_attributes
        {
          turbo: turbo,
          turbo_frame: :_top
        }.compact
      end

      def to_param
        to_s
      end

      def link_arguments(resource:, arguments: {}, **args)
        path = Avo::Services::URIService.parse(resource.record&.persisted? ? resource.record_path : resource.records_path)
          .append_paths("actions")
          .append_query(
            **{
              action_id: to_param,
              arguments: encode_arguments(arguments),
              **args
            }.compact
          )
          .to_s

        data = {
          turbo_frame: Avo::ACTIONS_TURBO_FRAME_ID,
        }

        [path, data]
      end

      # Encrypt the arguments so we can pass sensible data as a query param.
      # EncryptionService can generate special characters that can break the URL.
      # We use Base64 to encode the encrypted string so we can safely pass it as a query param and don't break the URL.
      def encode_arguments(arguments)
        return if arguments.blank?

        Base64.encode64 Avo::Services::EncryptionService.encrypt(
          message: arguments,
          purpose: :action_arguments
        )
      end

      def decode_arguments(arguments)
        return if arguments.blank?

        Avo::Services::EncryptionService.decrypt(
          message: Base64.decode64(arguments),
          purpose: :action_arguments
        )
      end
    end

    def action_name
      if name.present?
        return Avo::ExecutionContext.new(
          target: name,
          resource: @resource,
          record: @record,
          view: @view,
          arguments: @arguments
        ).handle
      end

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

    def initialize(record: nil, resource: nil, user: nil, view: nil, arguments: {}, icon: :play)
      @record = record
      @resource = resource
      @user = user
      @view = Avo::ViewInquirer.new(view)
      @icon = icon
      @arguments = Avo::ExecutionContext.new(
        target: arguments,
        resource: resource,
        record: record
      ).handle.with_indifferent_access

      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")

      self.items_holder = Avo::Resources::Items::Holder.new
      fields

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

      if may_download_file.present?
        puts "[Avo->] WARNING! Since version 3.2.2 'may_download_file' is unecessary and deprecated on actions. Can be safely removed from #{self.class.name}"
      end
    end

    # Blank method
    def fields
    end

    def get_message
      Avo::ExecutionContext.new(
        target: self.class.message,
        resource: @resource,
        record: @record,
        view: @view,
        arguments: @arguments
      ).handle
    end

    def handle_action(**args)
      processed_fields = if args[:fields].present?
        # 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 do |field|
          field.hydrate(resource: @resource)

          [field.id, field]
        end.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

        args[: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.reject(&:blank?).to_h
      else
        {}
      end

      handle(
        fields: processed_fields.with_indifferent_access,
        current_user: args[:current_user],
        resource: args[:resource],
        records: args[:query],
        query: args[:query]
      )

      self
    end

    def visible_in_view(parent_resource: nil)
      return false unless authorized?

      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::ExecutionContext.new(
        target: visible,
        params: params,
        parent_resource: parent_resource,
        resource: @resource,
        view: @view,
        arguments: arguments
      ).handle
    end

    def succeed(text)
      add_message text, :success

      self
    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[:type] = :keep_modal_open

      self
    end

    def close_modal
      response[:type] = :close_modal

      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, **args, &block)
      response[:type] = :redirect
      response[:redirect_args] = args
      response[:path] = if block.present?
        block
      else
        path
      end

      self
    end

    def reload
      response[:type] = :reload

      self
    end

    def navigate_to_action(action, **kwargs)
      response[:type] = :navigate_to_action
      response[:action] = action
      response[:navigate_to_action_args] = kwargs

      self
    end

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

      self
    end

    def authorized?
      Avo::ExecutionContext.new(
        target: authorize,
        action: self,
        resource: @resource,
        view: @view,
        arguments: arguments
      ).handle
    end

    private

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