# @api description # Root module, containing confguration and pure helpers. For # the setters, create an initializer `config/initializers/compony.rb` and call # them from there. # @see Compony::ViewHelpers Compony::ViewHelpers for helpers that require a view context and render results immediately module Compony ##########=====------- # Configuration writers ##########=====------- # Setter for the global button component class. This allows you to implement a # custom button component and have all Compony button helpers use your custom # button component instead of {Compony::Components::Button}. # @param button_component_class [String] Name of your custom button component class (inherit from {Compony::Components::Button} or {Compony::Component}) def self.button_component_class=(button_component_class) @button_component_class = button_component_class end # Setter for the global field namespaces. This allows you to implement custom # Fields, be it new ones or overrides for existing Compony model fields. # Must give an array of strings of namespaces that contain field classes named after # the field type. The array is queried in order, if the first namespace does not # contain the class we're looking for, the next is considered and so on. # The classes defined in the namespace must inherit from Compony::ModelFields::Base # @param model_field_namespaces [Array] Array of strings, the names of the namespaces in the order they should be searched def self.model_field_namespaces=(model_field_namespaces) @model_field_namespaces = model_field_namespaces end # Setter for the name of the Rails `before_action` that should be called to # ensure that users are authenticated before accessing the component. For # instance, implement a method `def enforce_authentication` in your # `ApplicationController`. In the method, make sure the user has a session and # redirect to the login page if they don't.
The action must be accessible # by {ComponyController} and the easiest way to achieve this is to implement # the action in your `ApplicationController`. If this is never called, # authentication is disabled. # @param authentication_before_action [Symbol] Name of the method you want to call for authentication def self.authentication_before_action=(authentication_before_action) @authentication_before_action = authentication_before_action.to_sym end # Setter for a content block that runs before the root component gets rendered (standalone only). Usage is the same as `content`. # The block runs between `before_render` and `render`, i.e. before the first `content` block. def self.content_before_root_comp(&block) fail('`Compony.content_before` requires a block.') unless block_given? @content_before_root_comp_block = block end # Setter for a content block that runs after the root component gets rendered (standalone only). Usage is the same as `content`. # The block runs after `render`, i.e. after the last `content` block. def self.content_after_root_comp(&block) fail('`Compony.content_after` requires a block.') unless block_given? @content_after_root_comp_block = block end ##########=====------- # Configuration readers ##########=====------- # Getter for the global button component class. # @see Compony#button_component_class= Explanation of button_component_class (documented in the corresponding setter) def self.button_component_class @button_component_class ||= Components::Button @button_component_class = const_get(@button_component_class) if @button_component_class.is_a?(String) return @button_component_class end # Getter for the global field namespaces. # @see Compony#model_field_namespaces= Explanation of model_field_namespaces (documented in the corresponding setter) def self.model_field_namespaces return @model_field_namespaces ||= ['Compony::ModelFields'] end # Getter for the name of the Rails `before_action` that enforces authentication. # @see Compony#authentication_before_action= Explanation of authentication_before_action (documented in the corresponding setter) def self.authentication_before_action @authentication_before_action end # Getter for content_before_root_comp_block # @see Compony#content_before_root_comp def self.content_before_root_comp_block @content_before_root_comp_block end # Getter for content_after_root_comp_block # @see Compony#content_after_root_comp def self.content_after_root_comp_block @content_after_root_comp_block end ##########=====------- # Application-wide available pure helpers ##########=====------- # Generates a Rails path to a component. Examples: `Compony.path(:index, :users)`, `Compony.path(:show, User.first)` # @param comp_name_or_cst [String,Symbol] The component that should be loaded, for instance `ShowForAll`, `'ShowForAll'` or `:show_for_all` # @param model_or_family_name_or_cst [String,Symbol,ApplicationRecord] Either the family that contains the requested component, # or an instance implementing `model_name` from which the family name is auto-generated. Examples: # `Users`, `'Users'`, `:users`, `User.first` # @param args_for_path_helper [Array] Positional arguments passed to the Rails helper # @param kwargs_for_path_helper [Hash] Named arguments passed to the Rails helper. If a model is given to `model_or_family_name_or_cst`, # the param `id` defaults to the passed model's ID. def self.path(comp_name_or_cst, model_or_family_name_or_cst, *args_for_path_helper, **kwargs_for_path_helper) # Extract model if any, to get the ID kwargs_for_path_helper.merge!(id: model_or_family_name_or_cst.id) if model_or_family_name_or_cst.respond_to?(:model_name) return Rails.application.routes.url_helpers.send( "#{path_helper_name(comp_name_or_cst, model_or_family_name_or_cst)}_path", *args_for_path_helper, **kwargs_for_path_helper ) end # Given a component and a family/model, this returns the matching component class if any, or nil if the component does not exist. # @param comp_name_or_cst [String,Symbol] The component that should be loaded, for instance `ShowForAll`, `'ShowForAll'` or `:show_for_all` # @param model_or_family_name_or_cst [String,Symbol,ApplicationRecord] Either the family that contains the requested component, # or an instance implementing `model_name` from which the family name is auto-generated. Examples: # `Users`, `'Users'`, `:users`, `User.first` def self.comp_class_for(comp_name_or_cst, model_or_family_name_or_cst) family_cst_str = family_name_for(model_or_family_name_or_cst).camelize comp_cst_str = comp_name_or_cst.to_s.camelize return nil unless ::Components.const_defined?(family_cst_str) family_constant = ::Components.const_get(family_cst_str) return nil unless family_constant.const_defined?(comp_cst_str) return family_constant.const_get(comp_cst_str) end # Same as Compony#comp_class_for but fails if none found # @see Compony#comp_class_for def self.comp_class_for!(comp_name_or_cst, model_or_family_name_or_cst) comp_class_for(comp_name_or_cst, model_or_family_name_or_cst) || fail( "No component found for [#{comp_name_or_cst.inspect}, #{model_or_family_name_or_cst.inspect}]" ) end # Given a component and a family, this returns the name of the Rails URL helper returning the path to this component.
# The parameters are the same as for {Compony#rails_action_name}.
# Example usage: `send("#{path_helper_name(:index, :users)}_url)` # @see Compony#path # @see Compony#rails_action_name rails_action_name for the accepted params def self.path_helper_name(...) "#{rails_action_name(...)}_comp" end # Given a component and a family, this returns the name of the ComponyController action for this component.
# Optionally can pass a name for extra standalone configs. # @param comp_name_or_cst [String,Symbol] Name of the component the action points to. # @param model_or_family_name_or_cst [String,Symbol] Name of the family the action points to. # @param name [String,Symbol] If referring to an extra standalone entrypoint, specify its name using this param. # @see Compony#path def self.rails_action_name(comp_name_or_cst, model_or_family_name_or_cst, name = nil) [name.presence, comp_name_or_cst.to_s.underscore, family_name_for(model_or_family_name_or_cst)].compact.join('_') end # Given a component and a family/model, this instanciates and returns a button component. # @param comp_name_or_cst [String,Symbol] The component that should be loaded, for instance `ShowForAll`, `'ShowForAll'` or `:show_for_all` # @param model_or_family_name_or_cst [String,Symbol,ApplicationRecord] Either the family that contains the requested component, # or an instance implementing `model_name` from which the family name is auto-generated. Examples: # `Users`, `'Users'`, `:users`, `User.first` # @param label_opts [Hash] Options hash that will be passed to the label method (see {Compony::ComponentMixins::Default::Labelling#label}) # @param params [Hash] GET parameters to be inclued into the path this button points to. Special case: e.g. format: :pdf -> some.url/foo/bar.pdf # @param feasibility_action [Symbol] Name of the feasibility action that should be checked for this button, defaults to the component name # @param feasibility_target [Symbol] Name of the feasibility target (subject) that the feasibility should be checked on, defaults to the model if given # @param override_kwargs [Hash] Override button options, see options for {Compony::Components::Button} # @see Compony::ViewHelpers#compony_button View helper providing a wrapper for this method that immediately renders a button. # @see Compony::Components::Button Compony::Components::Button: the default underlying implementation def self.button(comp_name_or_cst, model_or_family_name_or_cst, label_opts: nil, params: nil, feasibility_action: nil, feasibility_target: nil, method: nil, **override_kwargs) label_opts ||= button_defaults[:label_opts] || {} params ||= button_defaults[:params] || {} model = model_or_family_name_or_cst.respond_to?(:model_name) ? model_or_family_name_or_cst : nil target_comp_instance = Compony.comp_class_for!(comp_name_or_cst, model_or_family_name_or_cst).new(data: model) feasibility_action ||= button_defaults[:feasibility_action] || comp_name_or_cst.to_s.underscore.to_sym feasibility_target ||= button_defaults[:feasibility_target] || model options = { label: target_comp_instance.label(model, **label_opts), icon: target_comp_instance.icon, color: target_comp_instance.color, path: Compony.path(target_comp_instance.comp_name, target_comp_instance.family_name, model, **params), method:, visible: ->(controller) { target_comp_instance.standalone_access_permitted_for?(controller, verb: method) } } if feasibility_target options.merge!({ enabled: feasibility_target.feasible?(feasibility_action), title: feasibility_target.full_feasibility_messages(feasibility_action).presence }) end options.merge!(override_kwargs.symbolize_keys) return Compony.button_component_class.new(**options.symbolize_keys) end # Returns the current root component, if any def self.root_comp RequestStore.store[:compony_root_comp] end # Given a family name or a model-like class, this returns the suitable family name as String. # @param model_or_family_name_or_cst [String,Symbol,ApplicationRecord] Either the family that contains the requested component, # or an instance implementing `model_name` from which the family name is auto-generated. Examples: # `Users`, `'Users'`, `:users`, `User.first` def self.family_name_for(model_or_family_name_or_cst) if model_or_family_name_or_cst.respond_to?(:model_name) return model_or_family_name_or_cst.model_name.plural else return model_or_family_name_or_cst.to_s.underscore end end # Getter for current button defaults # @todo document params def self.button_defaults RequestStore.store[:button_defaults] || {} end # Overwrites the keys of the current button defaults by the ones provided during the execution of a given block and restores them afterwords. # This method is useful when the same set of options is to be given to a multitude of buttons. # @param keys_to_overwrite [Hash] Options that should be given to the buttons within the block, with their values # @param block [Block] Within this block, all omitted button options point to `keys_to_overwrite` def self.with_button_defaults(**keys_to_overwrite, &block) # Lazy initialize butto_defaults store if it hasn't been yet RequestStore.store[:button_defaults] ||= {} keys_to_overwrite.transform_keys!(&:to_sym) old_values = {} newly_defined_keys = keys_to_overwrite.keys - RequestStore.store[:button_defaults].keys keys_to_overwrite.each do |key, new_value| # Assign new value old_values[key] = RequestStore.store[:button_defaults][key] RequestStore.store[:button_defaults][key] = new_value end return_value = block.call # Restore previous value keys_to_overwrite.each do |key, _new_value| RequestStore.store[:button_defaults][key] = old_values[key] end # Undefine keys that were not there previously newly_defined_keys.each { |key| RequestStore.store[:button_defaults].delete(key) } return return_value end # Goes through model_field_namespaces and returns the first hit for the given constant # @param constant [Constant] The constant that is searched, e.g. RichText -> would return e.g. Compony::ModelFields::RichText def self.model_field_class_for(constant) model_field_namespaces.each do |model_field_namespace| model_field_namespace = model_field_namespace.constantize if model_field_namespace.is_a?(::String) if model_field_namespace.const_defined?(constant, false) return model_field_namespace.const_get(constant, false) end end fail("No `model_field_namespace` implements ...::#{constant}. Configured namespaces: #{Compony.model_field_namespaces.inspect}") end end require 'cancancan' require 'dslblend' require 'dyny' require 'request_store' require 'schemacop' require 'simple_form' require 'compony/engine' require 'compony/model_fields/base' require 'compony/model_fields/anchormodel' require 'compony/model_fields/association' require 'compony/model_fields/attachment' require 'compony/model_fields/boolean' require 'compony/model_fields/color' require 'compony/model_fields/currency' require 'compony/model_fields/date' require 'compony/model_fields/datetime' require 'compony/model_fields/decimal' require 'compony/model_fields/email' require 'compony/model_fields/float' require 'compony/model_fields/integer' require 'compony/model_fields/percentage' require 'compony/model_fields/phone' require 'compony/model_fields/rich_text' require 'compony/model_fields/string' require 'compony/model_fields/text' require 'compony/model_fields/time' require 'compony/model_fields/url' require 'compony/component_mixins/default/standalone' require 'compony/component_mixins/default/standalone/standalone_dsl' require 'compony/component_mixins/default/standalone/verb_dsl' require 'compony/component_mixins/default/standalone/resourceful_verb_dsl' require 'compony/component_mixins/default/labelling' require 'compony/component_mixins/resourceful' require 'compony/component' require 'compony/components/button' require 'compony/components/form' require 'compony/components/with_form' require 'compony/components/new' require 'compony/components/edit' require 'compony/components/destroy' require 'compony/method_accessible_hash' require 'compony/natural_ordering' require 'compony/model_mixin' require 'compony/request_context' require 'compony/version' require 'compony/view_helpers' require 'compony/controller_mixin'