lib/avo/fields/base_field.rb

Summary

Maintainability
B
5 hrs
Test Coverage
module Avo
  module Fields
    class BaseField
      extend ActiveSupport::DescendantsTracker

      prepend Avo::Concerns::HasItemType
      prepend Avo::Concerns::IsResourceItem
      include Avo::Concerns::IsVisible
      include Avo::Concerns::VisibleInDifferentViews
      include Avo::Concerns::HasHelpers
      include Avo::Fields::Concerns::HasFieldName
      include Avo::Fields::Concerns::HasDefault
      include Avo::Fields::Concerns::HasHTMLAttributes
      include Avo::Fields::Concerns::HandlesFieldArgs
      include Avo::Fields::Concerns::IsReadonly
      include Avo::Fields::Concerns::IsDisabled
      include Avo::Fields::Concerns::IsRequired
      include Avo::Fields::Concerns::UseViewComponents

      include ActionView::Helpers::UrlHelper

      delegate :app, to: ::Avo::Current
      delegate :view_context, to: :app
      delegate :context, to: :app
      delegate :simple_format, :content_tag, to: :view_context
      delegate :main_app, to: :view_context
      delegate :avo, to: :view_context
      delegate :t, to: ::I18n

      attr_reader :id
      attr_reader :block
      attr_reader :required
      attr_reader :readonly
      attr_reader :sortable
      attr_reader :summarizable
      attr_reader :nullable
      attr_reader :null_values
      attr_reader :format_using
      attr_reader :autocomplete
      attr_reader :help
      attr_reader :default
      attr_reader :as_avatar
      attr_reader :stacked
      attr_reader :for_presentation_only
      attr_reader :for_attribute

      # Private options
      attr_reader :computable # if allowed to be computable
      attr_reader :computed # if block is present
      attr_reader :computed_value # the value after computation

      # Hydrated payload
      attr_accessor :record
      attr_accessor :action
      attr_accessor :user
      attr_accessor :panel_name

      class_attribute :field_name_attribute

      def initialize(id, **args, &block)
        @id = id
        @name = args[:name]
        @translation_key = args[:translation_key]
        @block = block
        @required = args.dig(:required) # Value if :required present on args, nil otherwise
        @readonly = args[:readonly] || false
        @disabled = args[:disabled] || false
        @sortable = args[:sortable] || false
        @summarizable = args[:summarizable] || false
        @nullable = args[:nullable] || false
        @null_values = args[:null_values] || [nil, ""]
        @format_using = args[:format_using] || nil
        @update_using = args[:update_using] || nil
        @placeholder = args[:placeholder]
        @autocomplete = args[:autocomplete] || nil
        @help = args[:help] || nil
        @default = args[:default] || nil
        @visible = args[:visible]
        @as_avatar = args[:as_avatar] || false
        @html = args[:html] || nil
        @view = Avo::ViewInquirer.new(args[:view]) || nil
        @value = args[:value] || nil
        @stacked = args[:stacked] || nil
        @for_presentation_only = args[:for_presentation_only] || false
        @resource = args[:resource]
        @action = args[:action]
        @components = args[:components] || {}
        @for_attribute = args[:for_attribute]

        @args = args

        @computable = true
        @computed = block.present?
        @computed_value = nil

        post_initialize if respond_to?(:post_initialize)
      end

      def translation_key
        @translation_key || "avo.field_translations.#{@id}"
      end

      def translated_name(default:)
        t(translation_key, count: 1, default: default).humanize
      end

      def translated_plural_name(default:)
        t(translation_key, count: 2, default: default).humanize
      end

      # Getting the name of the resource (user/users, post/posts)
      # We'll first check to see if the user passed a name
      # Secondly we'll try to find a translation key
      # We'll fallback to humanizing the id
      def name
        return @name if custom_name?

        if translation_key
          translated_name default: default_name
        else
          default_name
        end
      end

      def plural_name
        default = name.pluralize

        if translation_key
          translated_plural_name default: default
        else
          default
        end
      end

      def table_header_label
        @table_header_label ||= name
      end

      def custom_name?
        !@name.nil?
      end

      def default_name
        @id.to_s.humanize(keep_id_suffix: true)
      end

      def placeholder
        Avo::ExecutionContext.new(target: @placeholder || name, record: record, resource: @resource, view: @view).handle
      end

      def value(property = nil)
        return @value if @value.present?

        property ||= @for_attribute || id

        # Get record value
        final_value = record.send(property) if is_model?(record) && record.respond_to?(property)

        # On new views and actions modals we need to prefill the fields with the default value if value is nil
        if final_value.nil? && should_fill_with_default_value? && default.present?
          final_value = computed_default_value
        end

        # Run computable callback block if present
        if computable && block.present?
          final_value = execute_block
        end

        # Run the value through resolver if present
        if format_using.present?
          final_value = Avo::ExecutionContext.new(
            target: format_using,
            value: final_value,
            record: record,
            resource: resource,
            view: view,
            field: self,
            include: self.class.included_modules
          ).handle
        end

        final_value
      end

      def execute_block
        Avo::ExecutionContext.new(
          target: block,
          record: record,
          resource: resource,
          view: view,
          field: self,
          include: self.class.included_modules
        ).handle
      end

      # Fills the record with the received value on create and update actions.
      def fill_field(record, key, value, params)
        key = @for_attribute.to_s if @for_attribute.present?
        return record unless has_attribute?(record, key)

        if @update_using.present?
          value = Avo::ExecutionContext.new(
            target: @update_using,
            record: record,
            key: key,
            value: value,
            resource: resource,
            field: self,
            include: self.class.included_modules
          ).handle
        end

        record.public_send(:"#{key}=", value)

        record
      end

      def has_attribute?(record, attribute)
        record.methods.include? attribute.to_sym
      end

      # Try to see if the field has a different database ID than it's name
      def database_id
        foreign_key
      rescue
        id
      end

      def has_own_panel?
        false
      end

      def resolve_attribute(value)
        value
      end

      def to_permitted_param
        id.to_sym
      end

      def record_errors
        record.nil? ? {} : record.errors
      end

      def type
        self.class.name.demodulize.to_s.underscore.gsub("_field", "")
      end

      def custom?
        !method(:initialize).source_location.first.include?("lib/avo/field")
      rescue
        true
      end

      def visible_in_reflection?
        true
      end

      def hidden_in_reflection?
        !visible_in_reflection?
      end

      def options_for_filter
        options
      end

      def updatable
        !is_disabled? && visible?
      end

      # Used by Avo to fill the record with the default value on :new and :edit views
      def assign_value(record:, value:)
        id = (type == "belongs_to") ? foreign_key : database_id

        if record.send(id).nil?
          record.send(:"#{id}=", value)
        end
      end

      private

      def model_or_class(model)
        model.instance_of?(String) ? "class" : "model"
      end

      def is_model?(model)
        model_or_class(model) == "model"
      end

      def should_fill_with_default_value?
        on_create? || in_action?
      end

      def on_create?
        @view.in?(%w[new create])
      end

      def in_action?
        @action.present?
      end

      def get_resource_by_model_class(model_class)
        resource = Avo.resource_manager.get_resource_by_model_class(model_class)

        resource || (raise Avo::MissingResourceError.new(model_class))
      end
    end
  end
end